summaryrefslogtreecommitdiff
path: root/internal/handlers/settings.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/handlers/settings.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/handlers/settings.go')
-rw-r--r--internal/handlers/settings.go34
1 files changed, 21 insertions, 13 deletions
diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go
index 9ab832c..d140aaf 100644
--- a/internal/handlers/settings.go
+++ b/internal/handlers/settings.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
+ "task-dashboard/internal/api"
"task-dashboard/internal/auth"
"task-dashboard/internal/models"
"task-dashboard/internal/store"
@@ -21,20 +22,28 @@ func (h *Handler) HandleSettingsPage(w http.ResponseWriter, r *http.Request) {
bySource[cfg.Source] = append(bySource[cfg.Source], cfg)
}
+ googleTasksToken, _ := h.store.GetOAuthToken(api.GoogleTasksOAuthSource)
+
data := struct {
- Configs map[string][]models.SourceConfig
- Sources []string
- SyncLog []store.SyncLogEntry
- Agents []models.Agent
- CSRFToken string
- WebAuthnEnabled bool
+ Configs map[string][]models.SourceConfig
+ Sources []string
+ SyncLog []store.SyncLogEntry
+ Agents []models.Agent
+ CSRFToken string
+ WebAuthnEnabled bool
+ GoogleTasksOAuthReady bool
+ GoogleTasksConnected bool
+ GoogleTasksError string
}{
- Configs: bySource,
- Sources: []string{"trello", "gcal", "gtasks"},
- SyncLog: syncLog,
- Agents: agents,
- CSRFToken: auth.GetCSRFTokenFromContext(r.Context()),
- WebAuthnEnabled: h.WebAuthnEnabled,
+ Configs: bySource,
+ Sources: []string{"trello", "gcal", "gtasks"},
+ SyncLog: syncLog,
+ Agents: agents,
+ CSRFToken: auth.GetCSRFTokenFromContext(r.Context()),
+ WebAuthnEnabled: h.WebAuthnEnabled,
+ GoogleTasksOAuthReady: h.googleTasksOAuthConfig != nil,
+ GoogleTasksConnected: googleTasksToken != nil,
+ GoogleTasksError: r.URL.Query().Get("google_tasks_error"),
}
if err := h.renderer.Render(w, "settings.html", data); err != nil {
@@ -148,4 +157,3 @@ func (h *Handler) HandleToggleSourceConfig(w http.ResponseWriter, r *http.Reques
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"enabled": enabled})
}
-