Files
HealthLog/src/app/api/polar/status/route.ts
T
Marc-André Bombeck 38c7604ce4 feat(integrations): per-user Polar and Oura OAuth credentials
Polar and Oura resolved their OAuth client id/secret from the server
environment only, forcing every user on a deploy to share one app. Both
now read per-user BYO keys first, falling back to the shared env app when
unset, mirroring the WHOOP and Fitbit model.

- Add DB-first-then-env resolvers in src/lib/{polar,oura}/credentials.ts,
  encrypting the client id/secret at rest on User.
- Repoint the connect, callback, status routes and the Oura refresh path
  to the user-scoped resolver.
- Add /api/{polar,oura}/credentials (GET/PUT/DELETE) with Zod-validated
  input and userId narrowed from the session.
- Render an optional 'your OAuth app credentials' form on the Polar and
  Oura cards; the status read exposes hasOwnCredentials for the saved
  placeholder. Existing env-only deploys keep working unchanged.
- Cover both resolvers (DB hit, env fallback, no creds) and the card form
  with unit tests; add the new strings across all six locales.
2026-06-14 23:19:47 +02:00

59 lines
2.0 KiB
TypeScript

import { prisma } from "@/lib/db";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { apiSuccess } from "@/lib/api-response";
import { getPolarClientCredentials } from "@/lib/polar/credentials";
import { getIntegrationStatus } from "@/lib/integrations/status";
/**
* v1.17.0 (F4) — Polar connection status for the current user.
*
* `available` reports whether usable OAuth credentials resolve for this user —
* per-user BYO keys (v1.17.1) first, then the shared env app — so the card can
* grey out the connect button when neither is set. `hasOwnCredentials` reports
* whether the user has stored their own BYO pair (drives the saved-placeholder
* UI). `connected` is whether a token is stored. The ledger snapshot (state /
* lastSuccessAt / lastError) comes off the shared `polar` IntegrationKey. The
* access token, member id, and client secret are NEVER returned.
*/
export const GET = apiHandler(async () => {
const { user } = await requireAuth();
annotate({ action: { name: "polar.status" } });
const dbUser = await prisma.user.findUnique({
where: { id: user.id },
select: {
polarAccessTokenEncrypted: true,
polarClientIdEncrypted: true,
polarClientSecretEncrypted: true,
},
});
const available = !!(await getPolarClientCredentials(user.id));
const connected = !!dbUser?.polarAccessTokenEncrypted;
const hasOwnCredentials =
!!dbUser?.polarClientIdEncrypted && !!dbUser?.polarClientSecretEncrypted;
if (!connected) {
return apiSuccess({
connected: false,
configured: false,
available,
hasOwnCredentials,
});
}
const status = await getIntegrationStatus(user.id, "polar");
return apiSuccess({
connected: true,
configured: true,
available,
hasOwnCredentials,
state: status.state,
lastSuccessAt: status.lastSuccessAt,
lastAttemptAt: status.lastAttemptAt,
lastError: status.lastError,
});
});