diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-08-16 00:03:51 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-08-16 00:03:51 +0000 |
| commit | 3660486153a16760d2b980e546bbbd29408fb8d4 (patch) | |
| tree | e29199d88b7365eca0438e41ac23fa0bd533f7c2 /internal/config | |
| parent | b55cfbbd433bed6035dfa228ee700e2cca060ca4 (diff) | |
Replace Google Tasks service-account auth with real OAuth
Service-account auth structurally cannot see a regular user's personal
task lists (no equivalent of Calendar's per-item sharing model) --
confirmed via GetTaskLists returning exactly the service account's own
empty "My Tasks" list, never the real user's three lists. Zero rows
were ever cached in production as a result.
Adds a standard 3-legged OAuth flow: /settings/google-tasks/connect
redirects to Google's consent screen (AccessTypeOffline+ApprovalForce
so a refresh_token is always issued), /callback exchanges the code and
persists the token (new oauth_tokens table), /disconnect clears it.
GoogleTasksClient now takes an option.ClientOption instead of a
credentials file path; NewGoogleTasksOAuthClient wraps it with a
dbTokenSource that reloads/refreshes from the DB on each access-token
expiry and re-persists -- carefully preserving the original
refresh_token when Google's refresh response omits one (it usually
does), which would otherwise silently and permanently break future
refreshes.
Settings page shows connection status and a Connect/Disconnect
button. Calendar keeps using service-account auth (that one actually
works). Requires a one-time manual step: create an OAuth 2.0 Client ID
in Google Cloud Console and set GOOGLE_OAUTH_CLIENT_ID/SECRET in .env
-- documented in .env.example.
Diffstat (limited to 'internal/config')
| -rw-r--r-- | internal/config/config.go | 25 | ||||
| -rw-r--r-- | internal/config/config_test.go | 12 |
2 files changed, 27 insertions, 10 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index e6bc19a..7a80897 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,8 +18,18 @@ type Config struct { GoogleCredentialsFile string GoogleCalendarID string - // Google Tasks - GoogleTasksListID string + // Google Tasks (real 3-legged OAuth -- service-account auth cannot see a + // user's personal task lists, see NewGoogleTasksClient's doc comment) + GoogleTasksListID string + GoogleOAuthClientID string + GoogleOAuthClientSecret string + // GoogleOAuthRedirectURL must exactly match an "Authorized redirect URI" + // on the OAuth 2.0 Client ID in Google Cloud Console -- kept as its own + // explicit var rather than derived from WebAuthnOrigin (which isn't set + // in production; passkeys aren't configured either) so there's exactly + // one place that has to match Console, not two things that have to + // agree with each other. + GoogleOAuthRedirectURL string // Paths DatabasePath string @@ -66,7 +76,10 @@ func Load() (*Config, error) { GoogleCalendarID: getEnvWithDefault("GOOGLE_CALENDAR_ID", "primary"), // Google Tasks - GoogleTasksListID: getEnvWithDefault("GOOGLE_TASKS_LIST_ID", "@default"), + GoogleTasksListID: getEnvWithDefault("GOOGLE_TASKS_LIST_ID", "@default"), + GoogleOAuthClientID: os.Getenv("GOOGLE_OAUTH_CLIENT_ID"), + GoogleOAuthClientSecret: os.Getenv("GOOGLE_OAUTH_CLIENT_SECRET"), + GoogleOAuthRedirectURL: getEnvWithDefault("GOOGLE_OAUTH_REDIRECT_URL", "https://doot.terst.org/settings/google-tasks/callback"), // Paths DatabasePath: getEnvWithDefault("DATABASE_PATH", "./dashboard.db"), @@ -140,9 +153,11 @@ func (c *Config) HasGoogleCalendar() bool { return c.GoogleCredentialsFile != "" } -// HasGoogleTasks checks if Google Tasks is configured +// HasGoogleTasks checks if Google Tasks OAuth is configured (client ID and +// secret from Google Cloud Console) -- doesn't mean a user has actually +// connected yet, that's the oauth_tokens row, checked separately. func (c *Config) HasGoogleTasks() bool { - return c.GoogleCredentialsFile != "" && c.GoogleTasksListID != "" + return c.GoogleOAuthClientID != "" && c.GoogleOAuthClientSecret != "" } // getEnvWithDefault returns environment variable value or default if not set diff --git a/internal/config/config_test.go b/internal/config/config_test.go index b436e93..60277f4 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -48,11 +48,13 @@ func TestConfigValidate(t *testing.T) { func TestConfigHasMethods(t *testing.T) { cfg := Config{ - PlanToEatAPIKey: "pte-key", - TrelloAPIKey: "trello-key", - TrelloToken: "trello-token", - GoogleCredentialsFile: "/path/to/creds.json", - GoogleTasksListID: "@default", + PlanToEatAPIKey: "pte-key", + TrelloAPIKey: "trello-key", + TrelloToken: "trello-token", + GoogleCredentialsFile: "/path/to/creds.json", + GoogleTasksListID: "@default", + GoogleOAuthClientID: "client-id", + GoogleOAuthClientSecret: "client-secret", } if !cfg.HasPlanToEat() { |
