mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
One-shot prettier pass so the new format:check gate starts from a clean baseline. Formatting only — no behaviour change. Generator-owned and vendored artifacts stay untouched via .prettierignore.
104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
/**
|
|
* v1.17.0 (F4) — per-user Polar token accessors.
|
|
* v1.17.1 — per-user BYO client id/secret resolver (DB-first then env).
|
|
*
|
|
* Like WHOOP / Fitbit, Polar is now a per-user BYO-key integration: each user
|
|
* may register their own AccessLink app and store the client id/secret encrypted
|
|
* on `User`, with a fall-back to the shared env app for existing single-app
|
|
* deploys. Also per-user is the granted token + Polar member id. Polar access
|
|
* tokens do not expire and carry no refresh token, so there is no refresh path —
|
|
* a revoked grant surfaces as a 401 on the next read.
|
|
*/
|
|
import { prisma } from "@/lib/db";
|
|
import { decrypt, encrypt } from "@/lib/crypto";
|
|
import { getPolarCredentials, type PolarCredentials } from "./client";
|
|
|
|
/**
|
|
* Resolve the user's Polar OAuth client id/secret, DB-first then env.
|
|
*
|
|
* v1.17.1 makes Polar a per-user BYO-key integration (mirroring WHOOP / Fitbit):
|
|
* a self-hoster registers their own Polar AccessLink app and pastes the client
|
|
* id/secret into Settings, stored encrypted on `User`. When the user has not
|
|
* configured per-user keys we fall back to the shared env app
|
|
* (`POLAR_CLIENT_ID` / `POLAR_CLIENT_SECRET`) so existing single-app deploys
|
|
* keep working. Returns null when neither source is configured.
|
|
*/
|
|
export async function getPolarClientCredentials(
|
|
userId: string,
|
|
): Promise<PolarCredentials | null> {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: {
|
|
polarClientIdEncrypted: true,
|
|
polarClientSecretEncrypted: true,
|
|
},
|
|
});
|
|
|
|
if (user?.polarClientIdEncrypted && user?.polarClientSecretEncrypted) {
|
|
return {
|
|
clientId: decrypt(user.polarClientIdEncrypted),
|
|
clientSecret: decrypt(user.polarClientSecretEncrypted),
|
|
};
|
|
}
|
|
|
|
// Fall back to the shared env-configured OAuth app.
|
|
return getPolarCredentials();
|
|
}
|
|
|
|
/** Persist the user's Polar OAuth client id/secret, encrypted at rest. */
|
|
export async function storePolarClientCredentials(
|
|
userId: string,
|
|
clientId: string,
|
|
clientSecret: string,
|
|
): Promise<void> {
|
|
await prisma.user.update({
|
|
where: { id: userId },
|
|
data: {
|
|
polarClientIdEncrypted: encrypt(clientId),
|
|
polarClientSecretEncrypted: encrypt(clientSecret),
|
|
},
|
|
});
|
|
}
|
|
|
|
/** Clear the user's stored Polar OAuth client id/secret. */
|
|
export async function clearPolarClientCredentials(
|
|
userId: string,
|
|
): Promise<void> {
|
|
await prisma.user.update({
|
|
where: { id: userId },
|
|
data: {
|
|
polarClientIdEncrypted: null,
|
|
polarClientSecretEncrypted: null,
|
|
},
|
|
});
|
|
}
|
|
|
|
export interface PolarConnection {
|
|
accessToken: string;
|
|
/** Polar numeric member id (`x_user_id`), needed for every data path. */
|
|
polarUserId: string;
|
|
}
|
|
|
|
/**
|
|
* Fetch + decrypt the user's stored Polar token. Returns null when the user has
|
|
* not connected Polar (no access token) or the member id is missing.
|
|
*/
|
|
export async function getPolarConnection(
|
|
userId: string,
|
|
): Promise<PolarConnection | null> {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: {
|
|
polarAccessTokenEncrypted: true,
|
|
polarUserIdEncrypted: true,
|
|
},
|
|
});
|
|
if (!user?.polarAccessTokenEncrypted || !user?.polarUserIdEncrypted) {
|
|
return null;
|
|
}
|
|
return {
|
|
accessToken: decrypt(user.polarAccessTokenEncrypted),
|
|
polarUserId: decrypt(user.polarUserIdEncrypted),
|
|
};
|
|
}
|