mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
Deleting a user's stored Oura BYO credentials drops a live connection (a token minted against the deleted app is orphaned), but the credentials DELETE route skipped the auditLog + markDisconnected the disconnect route performs, leaving the integration ledger parked at its last state with no audit trail. Mirror the disconnect route when a token was present. Drop two unused classifier type aliases and unexport the internal invalid-grant helper, and refresh the stale 'full OAuth from env' headers on the connect route and the env-credentials helper to the v1.17.1 DB-first framing. Add the missing route tests for the BYO surface this release touched: credentials GET/PUT/DELETE (422 reject, encrypt-on-write, the audit/ledger branch) and the callback CSRF / cross-user / nocreds / success paths.
64 lines
2.1 KiB
TypeScript
64 lines
2.1 KiB
TypeScript
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
|
import { apiError } from "@/lib/api-response";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { shouldEmitSecureCookie } from "@/lib/auth/secure-cookie";
|
|
import { checkRateLimit } from "@/lib/rate-limit";
|
|
import { getAuthorizationUrl } from "@/lib/oura/client";
|
|
import { getOuraClientCredentials } from "@/lib/oura/credentials";
|
|
import {
|
|
OAUTH_STATE_TTL_MS,
|
|
mintSignedState,
|
|
oauthStateCookieName,
|
|
} from "@/lib/oauth/signed-state";
|
|
import { NextResponse } from "next/server";
|
|
|
|
/**
|
|
* v1.17.0 (F4) — redirect the user to the Oura consent screen.
|
|
*
|
|
* Credentials resolve DB-first then env (`getOuraClientCredentials`): a user's
|
|
* BYO Oura client id/secret wins, falling back to the shared env app
|
|
* (`OURA_CLIENT_ID` / `OURA_CLIENT_SECRET`) when none is stored. CSRF via the
|
|
* stateless signed state (`src/lib/oauth/signed-state.ts`): the same token is
|
|
* the `state` URL param AND an httpOnly cookie. Rate-limited per user.
|
|
*/
|
|
const CONNECT_RATE_LIMIT = 10;
|
|
const CONNECT_WINDOW_MS = 60_000;
|
|
|
|
export const GET = apiHandler(async () => {
|
|
const { user } = await requireAuth();
|
|
annotate({ action: { name: "oura.connect" } });
|
|
|
|
const rl = await checkRateLimit(
|
|
`oura:connect:${user.id}`,
|
|
CONNECT_RATE_LIMIT,
|
|
CONNECT_WINDOW_MS,
|
|
);
|
|
if (!rl.allowed) {
|
|
annotate({ action: { name: "oura.connect.rate_limited" } });
|
|
return NextResponse.redirect(
|
|
new URL(
|
|
"/settings/integrations?oura=error&reason=rate_limited",
|
|
process.env.NEXT_PUBLIC_APP_URL!,
|
|
),
|
|
);
|
|
}
|
|
|
|
const creds = await getOuraClientCredentials(user.id);
|
|
if (!creds) {
|
|
return apiError("Oura integration is not configured on this server.", 400);
|
|
}
|
|
|
|
const state = mintSignedState("oura", user.id);
|
|
const url = getAuthorizationUrl(state, creds);
|
|
|
|
const response = NextResponse.redirect(url);
|
|
response.cookies.set(oauthStateCookieName("oura"), state, {
|
|
httpOnly: true,
|
|
secure: shouldEmitSecureCookie(),
|
|
sameSite: "lax",
|
|
maxAge: Math.floor(OAUTH_STATE_TTL_MS / 1000),
|
|
path: "/",
|
|
});
|
|
return response;
|
|
});
|