summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-02-07 18:35:43 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-02-07 18:35:43 -1000
commita241995734cecff39f57732916a73052caf4c175 (patch)
tree712cb8c15c1139c3562747d857aed7de1ede360c
parentd793e16f336189d38a9d43310713dd13e3b7c438 (diff)
Fix passkey registration showing broken UI when WebAuthn not configured
Root cause: WEBAUTHN_RP_ID and WEBAUTHN_ORIGIN env vars not set in production, so WebAuthn is nil and all passkey endpoints return 404. The settings page was unconditionally showing the passkey registration card, leading to confusing "Failed to start registration" errors. Fix: Pass WebAuthnEnabled flag from main.go through Handler to the settings template, which now conditionally renders the passkey card only when WebAuthn is properly configured. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
-rw-r--r--cmd/dashboard/main.go2
-rw-r--r--internal/handlers/handlers.go4
-rw-r--r--internal/handlers/handlers_test.go35
-rw-r--r--internal/handlers/settings.go18
-rw-r--r--test/acceptance_test.go2
-rw-r--r--web/templates/settings.html7
6 files changed, 55 insertions, 13 deletions
diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go
index 073364f..e44d533 100644
--- a/cmd/dashboard/main.go
+++ b/cmd/dashboard/main.go
@@ -146,7 +146,7 @@ func main() {
}
// Initialize handlers
- h := handlers.New(db, todoistClient, trelloClient, planToEatClient, googleCalendarClient, googleTasksClient, cfg, buildCommit)
+ h := handlers.New(db, todoistClient, trelloClient, planToEatClient, googleCalendarClient, googleTasksClient, cfg, buildCommit, wa != nil)
// Set up router
r := chi.NewRouter()
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 226f117..876326e 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -30,10 +30,11 @@ type Handler struct {
config *config.Config
renderer Renderer
BuildVersion string
+ WebAuthnEnabled bool
}
// New creates a new Handler instance
-func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, googleCalendar api.GoogleCalendarAPI, googleTasks api.GoogleTasksAPI, cfg *config.Config, buildVersion string) *Handler {
+func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, googleCalendar api.GoogleCalendarAPI, googleTasks api.GoogleTasksAPI, cfg *config.Config, buildVersion string, webAuthnEnabled bool) *Handler {
// Template functions
funcMap := template.FuncMap{
"subtract": func(a, b int) int { return a - b },
@@ -61,6 +62,7 @@ func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat
config: cfg,
renderer: NewTemplateRenderer(tmpl),
BuildVersion: buildVersion,
+ WebAuthnEnabled: webAuthnEnabled,
}
}
diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go
index 0ad36b2..b338aa2 100644
--- a/internal/handlers/handlers_test.go
+++ b/internal/handlers/handlers_test.go
@@ -1971,6 +1971,41 @@ func TestSettingsTemplate_HasCSRFToken(t *testing.T) {
"settings.html must expose .CSRFToken (e.g. via meta tag) for JavaScript passkey registration")
}
+// TestSettingsTemplate_HidesPasskeysWhenDisabled verifies that the settings
+// template conditionally shows the passkeys section based on WebAuthnEnabled.
+func TestSettingsTemplate_HidesPasskeysWhenDisabled(t *testing.T) {
+ assertTemplateContains(t, "../../web/templates/settings.html",
+ ".WebAuthnEnabled",
+ "settings.html must check .WebAuthnEnabled to conditionally show passkeys section")
+}
+
+// TestHandleSettingsPage_PassesWebAuthnEnabled verifies the settings handler
+// includes WebAuthnEnabled in template data.
+func TestHandleSettingsPage_PassesWebAuthnEnabled(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ req := httptest.NewRequest("GET", "/settings", nil)
+ w := httptest.NewRecorder()
+
+ h.HandleSettingsPage(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("Expected status 200, got %d", w.Code)
+ }
+
+ mock := h.renderer.(*MockRenderer)
+ if len(mock.Calls) == 0 {
+ t.Fatal("Expected renderer to be called")
+ }
+
+ lastCall := mock.Calls[len(mock.Calls)-1]
+ dataStr := fmt.Sprintf("%+v", lastCall.Data)
+ if !strings.Contains(dataStr, "WebAuthnEnabled") {
+ t.Error("Settings page template data must include WebAuthnEnabled field")
+ }
+}
+
// TestHandleSettingsPage_PassesCSRFToken verifies the settings handler includes
// a CSRFToken in its template data so the passkey registration JS can use it.
func TestHandleSettingsPage_PassesCSRFToken(t *testing.T) {
diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go
index 60fc6be..0ad362b 100644
--- a/internal/handlers/settings.go
+++ b/internal/handlers/settings.go
@@ -22,15 +22,17 @@ func (h *Handler) HandleSettingsPage(w http.ResponseWriter, r *http.Request) {
}
data := struct {
- Configs map[string][]models.SourceConfig
- Sources []string
- Toggles []models.FeatureToggle
- CSRFToken string
+ Configs map[string][]models.SourceConfig
+ Sources []string
+ Toggles []models.FeatureToggle
+ CSRFToken string
+ WebAuthnEnabled bool
}{
- Configs: bySource,
- Sources: []string{"trello", "todoist", "gcal", "gtasks"},
- Toggles: toggles,
- CSRFToken: auth.GetCSRFTokenFromContext(r.Context()),
+ Configs: bySource,
+ Sources: []string{"trello", "todoist", "gcal", "gtasks"},
+ Toggles: toggles,
+ CSRFToken: auth.GetCSRFTokenFromContext(r.Context()),
+ WebAuthnEnabled: h.WebAuthnEnabled,
}
if err := h.renderer.Render(w, "settings.html", data); err != nil {
diff --git a/test/acceptance_test.go b/test/acceptance_test.go
index 0bcf974..e561e98 100644
--- a/test/acceptance_test.go
+++ b/test/acceptance_test.go
@@ -82,7 +82,7 @@ func setupTestServer(t *testing.T) (*httptest.Server, *store.Store, *http.Client
}
// Initialize handlers
- h := handlers.New(db, todoistClient, trelloClient, nil, nil, nil, cfg, "test")
+ h := handlers.New(db, todoistClient, trelloClient, nil, nil, nil, cfg, "test", false)
// Set up router (same as main.go)
r := chi.NewRouter()
diff --git a/web/templates/settings.html b/web/templates/settings.html
index 0803ae3..964d319 100644
--- a/web/templates/settings.html
+++ b/web/templates/settings.html
@@ -206,6 +206,7 @@
</div>
<!-- Passkeys Section -->
+ {{if .WebAuthnEnabled}}
<div class="card" id="passkeys-card">
<div class="card-header">
<div class="card-title">Passkeys</div>
@@ -219,10 +220,12 @@
</div>
<p id="passkey-status" style="color: var(--text-secondary); font-size: 0.85em; margin-top: 8px; display: none;"></p>
</div>
+ {{end}}
<script>
(function() {
- if (!window.PublicKeyCredential) {
- document.getElementById('passkeys-card').style.display = 'none';
+ if (!document.getElementById('passkeys-card') || !window.PublicKeyCredential) {
+ var card = document.getElementById('passkeys-card');
+ if (card) card.style.display = 'none';
return;
}