mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
fix(google-health): read the account id back for connections that lost it
Migration 0491 cleared the shared "profile" placeholder to null. Those connections sync normally, but the one-account-per-record check cannot see them, so the same Google account could still be connected to a second record until the first one reconnected. After a clean sync cycle, a connection without an id now reads it the way the callback does (profile, then identity) and stores it, guarded on the id still being null so a reconnect landing meanwhile wins. An id another record already holds is not written, and a failed read leaves the id for the next cycle without touching the cycle's verdict. The outcome is on the wide event as googleHealth.accountId.heal.
This commit is contained in:
1 parent
eb47f04a66
commit
5306ba7614
3 files changed
+198
-1
No files matched your search
@@ -0,0 +1,72 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { annotate, getEvent } from "@/lib/logging/context";
|
||||
import { accountLinkedElsewhere } from "@/lib/integrations/oauth-kit/account-link";
|
||||
import { accountIdFor, fetchProfile } from "./client";
|
||||
import { getValidToken, runWithGoogleHealthSyncCycle } from "./sync-core";
|
||||
|
||||
/** How a cycle's look at a connection without an account id ended. */
|
||||
export type GoogleHealthAccountIdHeal =
|
||||
"healed" | "unreadable" | "linked_elsewhere" | "failed";
|
||||
|
||||
/**
|
||||
* Fill in the Google account id of a connection that has none.
|
||||
*
|
||||
* Until v1.46.4 every connection stored the constant `profile` as its id, and
|
||||
* migration 0491 cleared it to null. Such a connection syncs normally, but the
|
||||
* one-account-per-record check (`accountLinkedElsewhere`) cannot see it, so the
|
||||
* same Google account could be connected to a second record. This reads the
|
||||
* id with the connection's own token the way the callback does and stores it,
|
||||
* which closes that gap without asking anyone to reconnect.
|
||||
*
|
||||
* Best-effort and never in the sync's way: a failed read leaves the id null
|
||||
* for the next cycle, and an id another record already holds is not written
|
||||
* (two records would then claim one account; the sync itself carries on, and
|
||||
* the next connect on either record meets the check). The write is guarded on
|
||||
* the id still being null, so a reconnect landing meanwhile wins.
|
||||
*/
|
||||
export async function healGoogleHealthAccountId(
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
): Promise<GoogleHealthAccountIdHeal> {
|
||||
let outcome: GoogleHealthAccountIdHeal;
|
||||
try {
|
||||
const profile = await fetchProfile(accessToken);
|
||||
const googleUserId = await accountIdFor(profile, accessToken);
|
||||
if (!googleUserId) {
|
||||
outcome = "unreadable";
|
||||
} else if (
|
||||
await accountLinkedElsewhere("google-health", googleUserId, userId)
|
||||
) {
|
||||
outcome = "linked_elsewhere";
|
||||
} else {
|
||||
await prisma.googleHealthConnection.updateMany({
|
||||
where: { userId, googleUserId: null },
|
||||
data: { googleUserId },
|
||||
});
|
||||
outcome = "healed";
|
||||
}
|
||||
} catch (err) {
|
||||
getEvent()?.addWarning(
|
||||
`google-health: account id read failed for ${userId}: ${err}`,
|
||||
);
|
||||
outcome = "failed";
|
||||
}
|
||||
annotate({ meta: { "googleHealth.accountId.heal": outcome } });
|
||||
return outcome;
|
||||
}
|
||||
|
||||
/**
|
||||
* The sync cycle's hook: after a clean cycle, a connection from before
|
||||
* v1.46.4 (no account id since migration 0491) reads its id back. A
|
||||
* connection that has one is left alone, so the profile is read once.
|
||||
*/
|
||||
export async function healUnknownGoogleHealthAccountId(
|
||||
userId: string,
|
||||
googleUserId: string | null,
|
||||
): Promise<void> {
|
||||
if (googleUserId !== null) return;
|
||||
const token = await runWithGoogleHealthSyncCycle(() => getValidToken(userId));
|
||||
if (token.result) {
|
||||
await healGoogleHealthAccountId(userId, token.result.accessToken);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { syncUserMetrics } from "./sync-metrics";
|
||||
import { syncUserSleep } from "./sync-sleep";
|
||||
import { syncUserWorkout } from "./sync-workout";
|
||||
import { withGoogleHealthSyncLock } from "./sync-lock";
|
||||
import { healUnknownGoogleHealthAccountId } from "./account-id-heal";
|
||||
import {
|
||||
GOOGLE_HEALTH_INTEGRATION_KEY,
|
||||
getValidToken,
|
||||
@@ -235,7 +236,7 @@ async function runGoogleHealthSyncCycle(
|
||||
const cycleStartedAt = new Date();
|
||||
const connection = await prisma.googleHealthConnection.findUnique({
|
||||
where: { userId },
|
||||
select: { lastSyncedAt: true },
|
||||
select: { lastSyncedAt: true, googleUserId: true },
|
||||
});
|
||||
if (!connection) {
|
||||
return { state: "failed", imported: 0, failed: true, resources: [] };
|
||||
@@ -389,6 +390,7 @@ async function runGoogleHealthSyncCycle(
|
||||
if (!failed) {
|
||||
await markSynced(userId, cycleStartedAt);
|
||||
await recordSyncSuccess(userId, GOOGLE_HEALTH_INTEGRATION_KEY);
|
||||
await healUnknownGoogleHealthAccountId(userId, connection.googleUserId);
|
||||
}
|
||||
|
||||
// Background-sync posture (mirrors the Fitbit tail): mark the per-user
|
||||
|
||||
Reference in new issue
Block a user