mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
Land the OAuth + credentials half of the Fitbit / Google Health provider, cloning the WHOOP provider structure and adapting it to the Google identity flow. - Client OAuth half (src/lib/fitbit/client.ts): Google authorize-URL builder (access_type=offline + prompt=consent), authorization-code exchange, token refresh, and a profile fetch for the connection's external user id. Client credentials ride in an HTTP Basic-auth header on the token endpoint; the four launch Restricted read scopes are pinned in FITBIT_OAUTH_SCOPE. All egress goes through safeFetch. - Non-rotating refresh tokens: getValidToken refreshes at expiry-minus-five- minutes, persists the new access token + expiry, and overwrites the stored refresh token ONLY when the response carries a fresh one — Google keeps the refresh token stable, unlike WHOOP. - BYO credentials route (PUT/GET/DELETE /api/fitbit/credentials): per-user client id/secret stored encrypted on the User columns, built field-by-field under requireAuth. - OAuth state + connect + callback routes: a random base64url nonce backed by a 10-minute FitbitOAuthState ledger row carries the (nonce -> userId) mapping; the callback validates the cookie/URL state with timingSafeEqual, consumes the row atomically (replay/expired/cross-user reason tags), exchanges the code, upserts the encrypted FitbitConnection, clears prior reauth state, and enqueues the self-converging history backfill. - Response classifier mirrors WHOOP's HTTP-status buckets; Google signals a revoked grant with a 401, so there is no invalid_grant body special-case. Tests cover the authorize URL, Basic-auth token exchange, the no-rotation refresh branch (token preserved when omitted, overwritten when present), the classifier status table, and the OAuth-state nonce mint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
39 lines
1.2 KiB
TypeScript
39 lines
1.2 KiB
TypeScript
/**
|
|
* Helper to retrieve per-user Fitbit / Google Health OAuth credentials from the
|
|
* database. Mirrors `src/lib/whoop/credentials.ts`.
|
|
*
|
|
* Fitbit ships per-user BYO-keys: each self-hoster registers their own Google
|
|
* Cloud OAuth client and pastes the client id/secret into Settings, stored
|
|
* encrypted on `User` (the Restricted-scope brand verification + annual CASA
|
|
* assessment is per-OAuth-client, so a single shared app is unworkable for a
|
|
* multi-operator product).
|
|
*/
|
|
import { prisma } from "@/lib/db";
|
|
import { decrypt } from "@/lib/crypto";
|
|
import type { FitbitCredentials } from "./client";
|
|
|
|
/**
|
|
* Fetch and decrypt the user's Fitbit API credentials.
|
|
* Returns null if the user has not configured credentials.
|
|
*/
|
|
export async function getUserFitbitCredentials(
|
|
userId: string,
|
|
): Promise<FitbitCredentials | null> {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: {
|
|
fitbitClientIdEncrypted: true,
|
|
fitbitClientSecretEncrypted: true,
|
|
},
|
|
});
|
|
|
|
if (!user?.fitbitClientIdEncrypted || !user?.fitbitClientSecretEncrypted) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
clientId: decrypt(user.fitbitClientIdEncrypted),
|
|
clientSecret: decrypt(user.fitbitClientSecretEncrypted),
|
|
};
|
|
}
|