summaryrefslogtreecommitdiff
path: root/internal/config/config.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-08-16 00:03:51 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-08-16 00:03:51 +0000
commit3660486153a16760d2b980e546bbbd29408fb8d4 (patch)
treee29199d88b7365eca0438e41ac23fa0bd533f7c2 /internal/config/config.go
parentb55cfbbd433bed6035dfa228ee700e2cca060ca4 (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/config.go')
-rw-r--r--internal/config/config.go25
1 files changed, 20 insertions, 5 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