mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(coach): enforce the module gate on the about-me routes
The structured self-context endpoints — about-me GET/PUT, the adopt POST, and the clarifying-questions GET/DELETE — read and wrote Coach self-context with no module check. Each handler now runs the two-layer requireModuleEnabled(userId, "coach") gate right after auth, returning 403 module.disabled when the operator availability layer or the per-user disableCoach opt-out has Coach turned off. The adopt route test mocks the gate to stay focused on the locked read-modify-write.
This commit is contained in:
1 parent
2b5974cf4a
commit
064ed92071
4 files changed
+21
No files matched your search
@@ -43,6 +43,9 @@ vi.mock("@/lib/ai/coach/bytes-codec", () => ({
|
||||
vi.mock("@/lib/ai/coach/about-me", () => ({
|
||||
getSelfContextForUser: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/modules/gate", () => ({
|
||||
requireModuleEnabled: vi.fn(async () => ({ enabled: true })),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api-response", () => ({
|
||||
apiSuccess: (data: unknown) => ({ data, error: null, status: 200 }),
|
||||
|
||||
@@ -41,6 +41,7 @@ import { prisma } from "@/lib/db";
|
||||
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
|
||||
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
import { getSelfContextForUser } from "@/lib/ai/coach/about-me";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import {
|
||||
ABOUT_ME_FIELD_MAX_CHARS,
|
||||
ABOUT_ME_MAX_CHARS,
|
||||
@@ -93,6 +94,9 @@ function normalise(s: string): string {
|
||||
|
||||
export const POST = apiHandler(async (req: Request) => {
|
||||
const { user } = await requireAuth();
|
||||
// v1.18.0 — Coach module gate (operator availability + disableCoach).
|
||||
const gate = await requireModuleEnabled(user.id, "coach");
|
||||
if (!gate.enabled) return gate.response;
|
||||
|
||||
const rl = await checkRateLimit(
|
||||
`coach-about-me:adopt:${user.id}`,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
PENDING_QUESTION_MAX_CHARS,
|
||||
setPendingQuestionsForUser,
|
||||
} from "@/lib/ai/coach/about-me";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
|
||||
const dismissSchema = z.object({
|
||||
/** Exact question text to dismiss. Omitted = dismiss all. */
|
||||
@@ -32,6 +33,9 @@ const dismissSchema = z.object({
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
const { user } = await requireAuth();
|
||||
// v1.18.0 — Coach module gate (operator availability + disableCoach).
|
||||
const gate = await requireModuleEnabled(user.id, "coach");
|
||||
if (!gate.enabled) return gate.response;
|
||||
const questions = await getPendingQuestionsForUser(user.id);
|
||||
annotate({
|
||||
action: { name: "coach.about_me.questions.get" },
|
||||
@@ -42,6 +46,9 @@ export const GET = apiHandler(async () => {
|
||||
|
||||
export const DELETE = apiHandler(async (req: Request) => {
|
||||
const { user } = await requireAuth();
|
||||
// v1.18.0 — Coach module gate (operator availability + disableCoach).
|
||||
const gate = await requireModuleEnabled(user.id, "coach");
|
||||
if (!gate.enabled) return gate.response;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(req, {
|
||||
maxBytes: 4 * 1024,
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
setPendingQuestionsForUser,
|
||||
} from "@/lib/ai/coach/about-me";
|
||||
import { deriveClarifyingQuestions } from "@/lib/ai/coach/self-context-questions";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import {
|
||||
ABOUT_ME_FIELD_MAX_CHARS,
|
||||
ABOUT_ME_MAX_CHARS,
|
||||
@@ -54,6 +55,9 @@ const PUT_WINDOW_MS = 60_000;
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
const { user } = await requireAuth();
|
||||
// v1.18.0 — Coach module gate (operator availability + disableCoach).
|
||||
const gate = await requireModuleEnabled(user.id, "coach");
|
||||
if (!gate.enabled) return gate.response;
|
||||
|
||||
const [ctx, pendingQuestions, row] = await Promise.all([
|
||||
getSelfContextForUser(user.id),
|
||||
@@ -89,6 +93,9 @@ export const GET = apiHandler(async () => {
|
||||
|
||||
export const PUT = apiHandler(async (req: Request) => {
|
||||
const { user } = await requireAuth();
|
||||
// v1.18.0 — Coach module gate (operator availability + disableCoach).
|
||||
const gate = await requireModuleEnabled(user.id, "coach");
|
||||
if (!gate.enabled) return gate.response;
|
||||
|
||||
const rl = await checkRateLimit(
|
||||
`coach-about-me:put:${user.id}`,
|
||||
|
||||
Reference in new issue
Block a user