fix(google-health): store the account's id, not the word "profile"

The profile's resource name is `users/{user}/profile`, and the connect
stored its last segment, the same constant for every account. Since the
one-account-per-record check, a second record on an instance could not
connect or reconnect Google Health: it was refused as `already_linked`.

The connect now stores the `{user}` segment, reads `users/me/identity`
when the profile answers through the `me` alias, and stores the id as
unknown when neither names the account. A migration makes the column
nullable and clears the placeholder, so the next connect stores the
real id.
This commit is contained in:
Marc-André Bombeck committed 2026-10-30 13:17:54 +01:00
1 parent 8f8f98eac7
commit cd6cbc06f5
8 files changed
+326 -27

No files matched your search

@@ -0,0 +1,30 @@
-- Google Health user id repair.
--
-- `google_health_connections.google_user_id` held the last path segment of
-- the profile's resource name `users/{user}/profile`, which is the constant
-- `profile` for every account. Since the one-account-per-record check every
-- connection therefore looked like the same Google account, and a second
-- record on the instance could not connect or reconnect (`already_linked`).
--
-- The column becomes nullable (null = id unknown, which the check treats as
-- no evidence of a second holder) and the placeholder values are cleared. The
-- next connect of each record stores the real id. The table holds one row per
-- connected record, so the update is small; the lock wait is bounded.
--
-- Reversibility: the cleared values were the same constant on every row and
-- identified nothing; restoring NOT NULL needs a value on every row first.
--
-- Both statements are repeatable, so a run stopped by the lock timeout is
-- safe to retry.
-- @retry-on-lock-timeout
DO $migration$
BEGIN
PERFORM set_config('lock_timeout', '5s', true);
ALTER TABLE "google_health_connections"
ALTER COLUMN "google_user_id" DROP NOT NULL;
UPDATE "google_health_connections"
SET "google_user_id" = NULL
WHERE "google_user_id" IN ('profile', 'me', '');
END
$migration$;
+1 -1
View File
@@ -4614,7 +4614,7 @@ model FitbitOAuthState {
model GoogleHealthConnection {
id String @id @default(cuid())
userId String @unique @map("user_id")
googleUserId String @map("google_user_id") // Google external account id
googleUserId String? @map("google_user_id") // Google Health user id; null when the connect could not read it
accessToken String @map("access_token") // encrypted at app level
refreshToken String @map("refresh_token") // encrypted at app level
tokenExpiresAt DateTime @map("token_expires_at")