mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
docs(integrations): pin exact WHOOP scopes + webhook URL, add Google Health setup
This commit is contained in:
1 parent
056581b428
commit
2a8be9fc4a
3 files changed
+106
-6
No files matched your search
@@ -0,0 +1,79 @@
|
||||
# Google Health integration (Fitbit & Pixel)
|
||||
|
||||
> **Beta.** Field mapping is still being verified against live Google Health
|
||||
> accounts, so some values may be missing or change in a future update.
|
||||
|
||||
HealthLog reads Fitbit and Pixel Watch data through the **Google Health API**.
|
||||
The OAuth app is therefore a **Google Cloud** OAuth client — not a Fitbit
|
||||
developer-console app. Like WHOOP, credentials are brought per user: each user
|
||||
registers their own Google Cloud OAuth client and pastes the client id/secret
|
||||
into Settings. The internal integration key is `fitbit`.
|
||||
|
||||
## 1. Create a Google Cloud OAuth client
|
||||
|
||||
1. In <https://console.cloud.google.com> create (or pick) a project and, under
|
||||
**APIs & Services**, enable the **Google Health API**.
|
||||
2. Configure the OAuth consent screen for the project.
|
||||
3. Under **APIs & Services → Credentials**, create an **OAuth 2.0 Client ID** of
|
||||
type **Web application**.
|
||||
4. **Authorized redirect URI:**
|
||||
`https://your-instance.example.com/api/fitbit/callback`
|
||||
5. Save. Google issues a **Client ID** and **Client Secret** — keep the secret
|
||||
out of any chat log.
|
||||
|
||||
## 2. Scopes
|
||||
|
||||
HealthLog requests exactly these read-only scopes:
|
||||
|
||||
```
|
||||
https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly
|
||||
https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly
|
||||
https://www.googleapis.com/auth/googlehealth.sleep.readonly
|
||||
https://www.googleapis.com/auth/googlehealth.profile.readonly
|
||||
```
|
||||
|
||||
Location and intraday scopes are deliberately omitted to keep the
|
||||
Restricted-scope review surface minimal. The authorize request sends
|
||||
`access_type=offline` + `prompt=consent` — Google's requirement to issue a
|
||||
refresh token (the equivalent of WHOOP's `offline` scope).
|
||||
|
||||
## 3. Wire the credentials in Settings → Integrations
|
||||
|
||||
Credentials are stored per user, not in `.env`. Sign in as the target user, open
|
||||
**Settings → Integrations → Google Health**, and paste the client ID and secret.
|
||||
They are encrypted AES-256-GCM at rest.
|
||||
|
||||
## 4. Connect
|
||||
|
||||
Click **Connect with Google Health**. HealthLog redirects to Google's OAuth
|
||||
consent screen, exchanges the returned code for an access + refresh token, stores
|
||||
them encrypted, and runs an initial sync. There is **no webhook** — Google Health
|
||||
syncs on the safety-net cron pull.
|
||||
|
||||
## Redirect URI is validated strictly
|
||||
|
||||
The resolved `redirect_uri` is asserted at connect time to be absolute, `https`
|
||||
(or `http` on localhost), to target exactly `/api/fitbit/callback`, and — when
|
||||
`FITBIT_REDIRECT_URI` is set alongside `NEXT_PUBLIC_APP_URL` — to share its
|
||||
origin. A misconfigured value fails closed rather than redirecting elsewhere.
|
||||
|
||||
## Optional instance env var
|
||||
|
||||
```env
|
||||
# Only when the callback origin differs from NEXT_PUBLIC_APP_URL:
|
||||
FITBIT_REDIRECT_URI="https://your-instance.example.com/api/fitbit/callback"
|
||||
```
|
||||
|
||||
The Google Cloud client id/secret are per-user (Settings), not env vars.
|
||||
|
||||
## Source overlap
|
||||
|
||||
If Google Health and another source (Withings, WHOOP, Apple Health) both supply
|
||||
the same vital — resting heart rate, blood oxygen, body temperature or
|
||||
respiratory rate — you may see both values until a future update settles on a
|
||||
single preferred source.
|
||||
|
||||
## Disconnect
|
||||
|
||||
The Settings **Disconnect** button revokes the stored tokens and removes the
|
||||
connection. Previously synced measurements stay in the database.
|
||||
@@ -21,8 +21,16 @@ grant scoped to credentials they control.
|
||||
2. **Redirect URI:** `https://your-instance.example.com/api/whoop/callback`
|
||||
(matches `WHOOP_REDIRECT_URI`, below). Add one entry per
|
||||
environment you connect from.
|
||||
3. Request the scopes for recovery, cycle/strain, sleep, and the
|
||||
workout/heart-rate reads the sync uses.
|
||||
3. Request these scopes (the exact set HealthLog asks for):
|
||||
|
||||
```
|
||||
offline read:recovery read:sleep read:workout read:cycles read:profile read:body_measurement
|
||||
```
|
||||
|
||||
`offline` is **mandatory** — without it WHOOP issues no refresh token
|
||||
and the connection dies when the first access token expires (~1 hour).
|
||||
The remaining `read:*` scopes are read-only; request all of them so the
|
||||
user grants once and every sync resource is covered.
|
||||
4. Save the app. WHOOP issues a **Client ID** and **Client Secret** —
|
||||
keep the secret out of any chat log; it grants read access to the
|
||||
linked account's full WHOOP history.
|
||||
@@ -59,10 +67,22 @@ WHOOP_WEBHOOK_SECRET="$(openssl rand -hex 32)"
|
||||
|
||||
- `WHOOP_REDIRECT_URI` — the OAuth callback URL handed to WHOOP. Must
|
||||
match a redirect URI registered in the WHOOP developer app.
|
||||
- `WHOOP_WEBHOOK_SECRET` — the secret HealthLog verifies the WHOOP
|
||||
webhook HMAC signature against. WHOOP signs each webhook body; the
|
||||
handler validates the signature before processing. Generate a fresh
|
||||
random value and restart the `app` container after setting it.
|
||||
- `WHOOP_WEBHOOK_SECRET` — used twice: as the **trailing path segment**
|
||||
of the webhook URL you register in the WHOOP console, and as the key
|
||||
HealthLog verifies the WHOOP HMAC body signature against. Set the
|
||||
webhook URL in the WHOOP console to:
|
||||
|
||||
```
|
||||
https://your-instance.example.com/api/whoop/webhook/<WHOOP_WEBHOOK_SECRET>
|
||||
```
|
||||
|
||||
WHOOP signs each body (`X-WHOOP-Signature` + `X-WHOOP-Signature-Timestamp`,
|
||||
HMAC-SHA256); the handler rejects deliveries older than five minutes and
|
||||
validates the signature before processing. The body signing key on WHOOP's
|
||||
side must equal `WHOOP_WEBHOOK_SECRET`. Generate a fresh random value and
|
||||
restart the `app` container after setting it. Without a webhook, WHOOP
|
||||
still syncs on the safety-net cron pull — only the near-real-time push is
|
||||
lost.
|
||||
|
||||
## Disconnect
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false pnpm build
|
||||
| Encryption-key rotation procedure | `docs/ops/encryption-key-rotation.md` |
|
||||
| Withings device sync | `docs/integrations/withings.md` |
|
||||
| WHOOP device sync (per-user BYO-keys) | `docs/integrations/whoop.md` |
|
||||
| Google Health sync — Fitbit & Pixel (per-user BYO-keys) | `docs/integrations/google-health.md` |
|
||||
| Apple Health `export.zip` import | `docs/integrations/apple-health.md` |
|
||||
| AI provider setup (OpenAI / Anthropic / local / ChatGPT OAuth) | `docs/integrations/ai-providers.md` |
|
||||
|
||||
|
||||
Reference in new issue
Block a user