diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index bf296420..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(npm --version)", - "Bash(psql --version)", - "Bash(gh pr:*)", - "Bash(gh issue:*)", - "Bash(/api/auth/callback/github)", - "Bash(gh label:*)", - "Bash(gh repo:*)", - "Bash(metricsController.ts)", - "Bash(git pull:*)", - "Bash(git stash:*)", - "Bash(git push:*)", - "Bash(gh release:*)", - "Bash(git rm:*)" - ] - } -} diff --git a/.gitignore b/.gitignore index 236cc8a3..d7a7de4b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ Thumbs.db .idea/ *.swp +# Claude Code local settings +.claude/ + desktop.ini diff --git a/src/app/api/goals/route.ts b/src/app/api/goals/route.ts index 9f013923..5d24b2e5 100644 --- a/src/app/api/goals/route.ts +++ b/src/app/api/goals/route.ts @@ -19,6 +19,12 @@ interface Goal { type Recurrence = "none" | "weekly" | "monthly"; +const VALID_RECURRENCES = ["none", "weekly", "monthly"] as const; +const MAX_TITLE_LEN = 100; +const MAX_UNIT_LEN = 30; +const MIN_TARGET = 1; +const MAX_TARGET = 10_000; + function getPeriodStart(recurrence: Recurrence): string { const now = new Date(); if (recurrence === "weekly") { @@ -106,21 +112,41 @@ export async function POST(req: Request) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } - const body = (await req.json()) as { - title?: string; - target?: number; - unit?: string; - recurrence?: Recurrence; - }; + let body: unknown; + try { + body = await req.json(); + } catch { + return Response.json({ error: "Invalid JSON" }, { status: 400 }); + } if (!body.title || typeof body.target !== "number" || body.target <= 0) { return Response.json({ error: "title and positive target required" }, { status: 400 }); } - const recurrence: Recurrence = body.recurrence ?? "none"; - if (!["none", "weekly", "monthly"].includes(recurrence)) { - return Response.json({ error: "Invalid recurrence value" }, { status: 400 }); + const { title, target, unit, recurrence } = body as Record; + + if (typeof title !== "string" || title.trim().length === 0) { + return Response.json({ error: "title must be a non-empty string" }, { status: 400 }); } + if (title.length > MAX_TITLE_LEN) { + return Response.json({ error: `title must be ${MAX_TITLE_LEN} characters or fewer` }, { status: 400 }); + } + if ( + typeof target !== "number" || + !Number.isInteger(target) || + target < MIN_TARGET || + target > MAX_TARGET + ) { + return Response.json( + { error: `target must be an integer between ${MIN_TARGET} and ${MAX_TARGET}` }, + { status: 400 } + ); + } + + const safeUnit = typeof unit === "string" ? unit.slice(0, MAX_UNIT_LEN) : "commits"; + const safeRecurrence: Recurrence = VALID_RECURRENCES.includes(recurrence as Recurrence) + ? (recurrence as Recurrence) + : "none"; const user = await resolveAppUser(session.githubId, session.githubLogin); if (!user) return Response.json({ error: "User not found" }, { status: 404 }); @@ -129,11 +155,11 @@ export async function POST(req: Request) { .from("goals") .insert({ user_id: user.id, - title: body.title, - target: body.target, - unit: body.unit ?? "commits", - recurrence, - period_start: getPeriodStart(recurrence), + title: title.trim(), + target, + unit: safeUnit, + recurrence: safeRecurrence, + period_start: getPeriodStart(safeRecurrence), current: 0, }) .select()