mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
classifyPolarError now lifts a 400 carrying invalid_grant on the token endpoint from persistent to reauth_required, mirroring Oura via the shared isOAuthInvalidGrant helper. A stale or replayed authorization code at connect time now surfaces the reconnect prompt instead of silently parking after 24 h. The BYO-credentials DELETE now audit-logs and parks the integration ledger at disconnected for parity with the dedicated disconnect route, and the PUT audit-logs the credential write. Refresh the connect-route comment that still described Polar as env-only OAuth; it has resolved BYO-first since v1.17.1. Add the invalid_grant classification test plus the previously-missing callback (happy path, CSRF mismatch, cross-user, off-spec token), connect, and credentials route tests.
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
import { prisma } from "@/lib/db";
|
|
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
|
import { auditLog } from "@/lib/auth/audit";
|
|
import { apiSuccess, apiError, safeJson } from "@/lib/api-response";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { markDisconnected } from "@/lib/integrations/status";
|
|
import {
|
|
storePolarClientCredentials,
|
|
clearPolarClientCredentials,
|
|
} from "@/lib/polar/credentials";
|
|
import { polarCredentialsSchema } from "@/lib/validations/polar";
|
|
import { NextRequest } from "next/server";
|
|
import { z } from "zod/v4";
|
|
|
|
/**
|
|
* v1.17.1 — per-user Polar BYO-key credentials.
|
|
*
|
|
* Whether the user has their own Polar AccessLink client id/secret stored. The
|
|
* connect/callback/status routes resolve credentials DB-first then env, so an
|
|
* unset pair simply falls back to the shared env app.
|
|
*/
|
|
export const GET = apiHandler(async () => {
|
|
const { user } = await requireAuth();
|
|
annotate({ action: { name: "polar.credentials.get" } });
|
|
|
|
const dbUser = await prisma.user.findUnique({
|
|
where: { id: user.id },
|
|
select: {
|
|
polarClientIdEncrypted: true,
|
|
polarClientSecretEncrypted: true,
|
|
},
|
|
});
|
|
|
|
return apiSuccess({
|
|
hasCredentials:
|
|
!!dbUser?.polarClientIdEncrypted && !!dbUser?.polarClientSecretEncrypted,
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Save Polar OAuth client credentials (encrypted at rest).
|
|
*/
|
|
export const PUT = apiHandler(async (request: NextRequest) => {
|
|
const { user } = await requireAuth();
|
|
annotate({ action: { name: "polar.credentials.update" } });
|
|
|
|
const { data: body, error: jsonError } = await safeJson(request, {
|
|
maxBytes: 64 * 1024,
|
|
});
|
|
if (jsonError) return jsonError;
|
|
|
|
const result = z.safeParse(polarCredentialsSchema, body);
|
|
if (!result.success) {
|
|
return apiError("Client ID and Client Secret are required", 422);
|
|
}
|
|
|
|
await storePolarClientCredentials(
|
|
user.id,
|
|
result.data.clientId,
|
|
result.data.clientSecret,
|
|
);
|
|
await auditLog("polar.credentials.update", { userId: user.id });
|
|
|
|
return apiSuccess({ updated: true });
|
|
});
|
|
|
|
/**
|
|
* Delete Polar credentials and the active connection.
|
|
*
|
|
* Removing the BYO client id/secret also drops the live grant: a token minted
|
|
* under the user's own AccessLink app would be orphaned once the keys are gone.
|
|
* Parks the integration ledger at `disconnected` for parity with the dedicated
|
|
* disconnect route so the status snapshot does not linger at a stale state.
|
|
*/
|
|
export const DELETE = apiHandler(async () => {
|
|
const { user } = await requireAuth();
|
|
annotate({ action: { name: "polar.credentials.delete" } });
|
|
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: {
|
|
polarAccessTokenEncrypted: null,
|
|
polarUserIdEncrypted: null,
|
|
},
|
|
});
|
|
await clearPolarClientCredentials(user.id);
|
|
await auditLog("polar.credentials.delete", { userId: user.id });
|
|
await markDisconnected(user.id, "polar");
|
|
|
|
return apiSuccess({ deleted: true });
|
|
});
|