import path from "node:path"; import { composeUp } from "./docker"; import { readEnvFile, readEnvValue, upsertEnvValues } from "./env-file"; import type { ResolvedGoogleCalendarSettings } from "./global-settings"; import { randomHex } from "./random"; import { listBusinessStacks, stackDir } from "./stacks"; export interface GoogleCalendarRolloutResult { slug: string; ok: boolean; error?: string; } /** * Pushes the global Google Calendar OAuth client into every business's * own `.env` and force-recreates its container — same mechanism as * email-rollout.ts/push-rollout.ts/super-admin-rollout.ts. * * GOOGLE_CALENDAR_REDIRECT_URI is deliberately never touched here: it's * derived from each business's own domain (see provisioning.ts's * updateBusiness), not from this global setting, and must keep pointing * at that business's own /api/staff/calendar/google/callback regardless * of which shared OAuth client is doing the authorizing. * * CALENDAR_TOKEN_ENCRYPTION_KEY is backfilled here too, but only when a * business doesn't already have one — every business generates its own * at provisioning time now (see compose-template.ts), but a business * provisioned before that existed would otherwise have a blank key and * hard-fail the moment a staff member tries to connect a calendar (see * src/lib/security/token-encryption.ts). Unlike the OAuth client itself, * this key must never be shared across businesses — it encrypts each * business's own stored refresh tokens in its own database — so a * business that already has one keeps it untouched forever, the same way * EMAIL_REPLY_TO is only ever filled in as a fallback, never overwritten. */ export async function applyGoogleCalendarSettingsToAllBusinesses( settings: ResolvedGoogleCalendarSettings, ): Promise { const businesses = await listBusinessStacks(); const results: GoogleCalendarRolloutResult[] = []; for (const business of businesses) { try { const dir = stackDir(business.slug); const envPath = path.join(dir, ".env"); const currentEnv = await readEnvFile(envPath); const currentEncryptionKey = currentEnv ? readEnvValue(currentEnv, "CALENDAR_TOKEN_ENCRYPTION_KEY") : null; const businessHasOwnEncryptionKey = Boolean(currentEncryptionKey && currentEncryptionKey.trim()); await upsertEnvValues(envPath, { GOOGLE_CALENDAR_CLIENT_ID: settings.clientId, GOOGLE_CALENDAR_CLIENT_SECRET: settings.clientSecret, ...(businessHasOwnEncryptionKey ? {} : { CALENDAR_TOKEN_ENCRYPTION_KEY: randomHex(32) }), }); await composeUp(dir, { forceRecreate: true }); results.push({ slug: business.slug, ok: true }); } catch (error) { results.push({ slug: business.slug, ok: false, error: error instanceof Error ? error.message : String(error) }); } } return results; }