Files
HealthLog/src/lib/nightscout/credentials.ts
T
Marc-André Bombeck f5d0b2f696 feat(integrations): add Nightscout CGM glucose sync
Pull continuous glucose readings from a self-hosted Nightscout instance
and store them as immutable BLOOD_GLUCOSE mg/dL measurements
(source=NIGHTSCOUT), giving the glucose panel real CGM density.

- client over safeFetch: public-host pin by default, relaxed only on the
  per-user private-network opt-in; redirect pinned manual + timeout
- credentials encrypted at rest on User; URL is the connect marker
- sync maps SGV entries first-write-wins on (userId,type,source,externalId)
- connect/disconnect/status routes: apiHandler + requireAuth + rate limit;
  connect validates by a live test fetch with clear errors
- hourly poll-cohort job registered in the all-queues registry
- settings card with URL, token, and the private-network toggle
- i18n across all six locales
2026-06-14 19:06:56 +02:00

47 lines
1.5 KiB
TypeScript

/**
* Per-user Nightscout connection credentials (v1.17.0).
*
* Unlike WHOOP / Fitbit (OAuth, with a dedicated `*Connection` row), Nightscout
* is a URL + token the user pastes once. Both are stored encrypted on `User`
* (`nightscoutUrlEncrypted` / `nightscoutTokenEncrypted`) via the fail-closed
* `*Encrypted` convention; the private-host opt-in is the plaintext boolean
* `nightscoutAllowPrivateHost`. This helper is the single read+decrypt path the
* sync + status + connect surfaces share.
*/
import { prisma } from "@/lib/db";
import { decrypt } from "@/lib/crypto";
export interface NightscoutCredentials {
baseUrl: string;
token: string;
allowPrivateHost: boolean;
}
/**
* Fetch and decrypt the user's Nightscout connection. Returns null when the
* URL is not configured (a token-less, fully-public instance still stores a
* URL, so the URL is the configured-marker, not the token).
*/
export async function getUserNightscoutCredentials(
userId: string,
): Promise<NightscoutCredentials | null> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
nightscoutUrlEncrypted: true,
nightscoutTokenEncrypted: true,
nightscoutAllowPrivateHost: true,
},
});
if (!user?.nightscoutUrlEncrypted) return null;
return {
baseUrl: decrypt(user.nightscoutUrlEncrypted),
token: user.nightscoutTokenEncrypted
? decrypt(user.nightscoutTokenEncrypted)
: "",
allowPrivateHost: user.nightscoutAllowPrivateHost,
};
}