Add Google Calendar OAuth connect and disconnect
GET /api/v1/integrations/google/connect sends the browser to Google's consent screen (calendar.events and calendar.calendarlist.readonly, access_type=offline, prompt=consent, PKCE S256) with a state carried in a signed __Host-mealquest_google flow cookie bound to the session. GET /api/v1/integrations/google/callback checks the state and session, exchanges the code, refuses a response without a refresh token (reconnect_required) or without both scopes, seals the refresh token with secretbox into integrations_google and sets connected_at. DELETE /api/v1/integrations/google revokes the token at Google and clears it; a revoke failure keeps the connection so it can be retried. - internal/gcal: the connect service; Google endpoints are overridable and internal/gcal/gcaltest fakes the token and revoke endpoints. - internal/auth: the login cookie becomes the exported FlowCookie both flows use (same wire format); CheckRedirect and ExchangeError are exported for reuse. - internal/config: MEALQUEST_GOOGLE_CLIENT_ID and MEALQUEST_GOOGLE_CLIENT_SECRET_FILE, required at boot. - DeleteIntegrationsGoogle query (sqlc regenerated); no migration, the integrations_google table already exists. - deploy: compose mounts google_client_secret; .env.example and RUNBOOK document the Google OAuth client, its redirect URI and rotation. Maestro task 1e9d7850-3a09-4572-b8a1-cb18cd4774cb. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,10 @@ func loadConfig(t *testing.T, keyFile string) config.Config {
|
||||
if err := os.WriteFile(clientSecretPath, []byte("client-secret\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
googleSecretPath := filepath.Join(dir, "google_client_secret")
|
||||
if err := os.WriteFile(googleSecretPath, []byte("google-secret\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv(config.EnvHTTPAddr, "127.0.0.1:0")
|
||||
t.Setenv(config.EnvPublicURL, "http://localhost")
|
||||
t.Setenv(config.EnvDataDir, t.TempDir())
|
||||
@@ -53,6 +57,8 @@ func loadConfig(t *testing.T, keyFile string) config.Config {
|
||||
t.Setenv(config.EnvOIDCClientID, "mealquest")
|
||||
t.Setenv(config.EnvOIDCClientSecretFile, clientSecretPath)
|
||||
t.Setenv(config.EnvOIDCAllowedGroups, "mealquest-admin")
|
||||
t.Setenv(config.EnvGoogleClientID, "mealquest.apps.googleusercontent.com")
|
||||
t.Setenv(config.EnvGoogleClientSecretFile, googleSecretPath)
|
||||
// MEALQUEST_DEFAULT_TZ is left unset; t.Setenv restores any outer value.
|
||||
t.Setenv(config.EnvDefaultTZ, "")
|
||||
if err := os.Unsetenv(config.EnvDefaultTZ); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user