mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
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.
59 lines
2.0 KiB
TypeScript
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,
|
|
});
|
|
});
|