mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
Security + accessibility hardening release. Closes the highest-priority findings from a focused security audit of the v1.4.23 release: - Bearer auth fail-closed when a route declares no scope (api-handler) - Public /api/send Umami proxy now rate-limits 120 req/min/IP - Proxy nonce generation switched to btoa(...) for Edge portability - Session-expiry delete race uses deleteMany + catch (no more 500s) - POST /api/import returns the standard apiError envelope on 422 - Runtime Docker image pins Prisma 7.8 + worker deps to minors - Notifications page renders a real load-error alert - Accessibility: switch aria-labels, htmlFor pairing, mobile admin Overview link, deep-link anchors on notification settings - Chart overlays: per-chart comparisonBaseline pref, BMI/bodyFat/ sleep/steps overlay keys, TrendCard layout tolerates long labels Verification: pnpm typecheck && pnpm lint && pnpm test (2244/2244) && pnpm format:check all green.
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
import { NextRequest } from "next/server";
|
|
import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler";
|
|
import { checkRateLimit } from "@/lib/rate-limit";
|
|
import { requestDeviceCode } from "@/lib/ai/codex-oauth";
|
|
import { cookies } from "next/headers";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { encrypt } from "@/lib/crypto";
|
|
import { apiSuccess } from "@/lib/api-response";
|
|
|
|
/**
|
|
* v1.4.7.1: Device-code flow for ChatGPT-OAuth.
|
|
*
|
|
* The previous redirect-based authorization-code flow fails on hosted
|
|
* deployments because OpenAI's Hydra only allow-lists localhost
|
|
* callbacks for the public Codex CLI client ID. Device-code is the
|
|
* documented alternative — the user goes to chatgpt.com on any device,
|
|
* types a short code, approves there. No redirect to our domain.
|
|
*
|
|
* This endpoint kicks the flow off: it asks Hydra for a `user_code` /
|
|
* `device_auth_id` pair and returns the user-facing pieces. The
|
|
* `device_auth_id` is the secret the poll endpoint needs and is
|
|
* persisted server-side via an encrypted httpOnly cookie scoped to a
|
|
* 15-minute lifetime — the same window Hydra gives the code.
|
|
*/
|
|
export const POST = apiHandler(async (request: NextRequest) => {
|
|
void request;
|
|
const { user } = await requireAuth();
|
|
|
|
const rl = await checkRateLimit(`codex-device-start:${user.id}`, 5, 60_000);
|
|
if (!rl.allowed) throw new HttpError(429, "Too many requests");
|
|
|
|
const code = await requestDeviceCode();
|
|
|
|
const cookieStore = await cookies();
|
|
cookieStore.set(
|
|
"codex_device",
|
|
encrypt(
|
|
JSON.stringify({
|
|
deviceAuthId: code.deviceAuthId,
|
|
userCode: code.userCode,
|
|
}),
|
|
),
|
|
{
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
sameSite: "lax",
|
|
path: "/",
|
|
maxAge: 15 * 60,
|
|
},
|
|
);
|
|
|
|
annotate({
|
|
action: { name: "codex.device.start" },
|
|
meta: { interval_seconds: code.intervalSeconds },
|
|
});
|
|
|
|
return apiSuccess({
|
|
userCode: code.userCode,
|
|
verificationUrl: code.verificationUrl,
|
|
intervalSeconds: code.intervalSeconds,
|
|
});
|
|
});
|