From 08bbcf18b1207153983261652b4a43a9b36f386c Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Tue, 20 Jan 2026 11:34:33 -1000 Subject: Add session-based authentication Implement secure authentication using scs session manager with SQLite backing store and bcrypt password hashing. - Add users and sessions tables (migration 004) - Create internal/auth package with Service, Middleware, and Handlers - Protect all routes except /login, /logout, /static/* - Add login page template and logout button to dashboard - Default credentials: admin/changeme (configurable via env vars) Co-Authored-By: Claude Opus 4.5 --- cmd/dashboard/main.go | 106 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 77 insertions(+), 29 deletions(-) (limited to 'cmd') diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 6e013e8..14664fc 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -2,18 +2,23 @@ package main import ( "context" + "html/template" "log" "net/http" "os" "os/signal" + "path/filepath" "syscall" "time" + "github.com/alexedwards/scs/sqlite3store" + "github.com/alexedwards/scs/v2" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/joho/godotenv" "task-dashboard/internal/api" + "task-dashboard/internal/auth" "task-dashboard/internal/config" "task-dashboard/internal/handlers" "task-dashboard/internal/store" @@ -36,6 +41,38 @@ func main() { } defer db.Close() + // Initialize session manager + sessionManager := scs.New() + sessionManager.Store = sqlite3store.New(db.DB()) + sessionManager.Lifetime = 24 * time.Hour + sessionManager.Cookie.Secure = false // Set to true in production with HTTPS + sessionManager.Cookie.SameSite = http.SameSiteLaxMode + + // Initialize auth service + authService := auth.NewService(db.DB()) + + // Ensure default admin user exists (use env vars for credentials) + defaultUser := os.Getenv("DEFAULT_USER") + defaultPass := os.Getenv("DEFAULT_PASS") + if defaultUser == "" { + defaultUser = "admin" + } + if defaultPass == "" { + defaultPass = "changeme" + } + if err := authService.EnsureDefaultUser(defaultUser, defaultPass); err != nil { + log.Printf("Warning: failed to ensure default user: %v", err) + } + + // Parse templates for auth handlers + authTemplates, err := template.ParseGlob(filepath.Join(cfg.TemplateDir, "*.html")) + if err != nil { + log.Printf("Warning: failed to parse auth templates: %v", err) + } + + // Initialize auth handlers + authHandlers := auth.NewHandlers(authService, sessionManager, authTemplates) + // Initialize API clients todoistClient := api.NewTodoistClient(cfg.TodoistAPIKey) trelloClient := api.NewTrelloClient(cfg.TrelloAPIKey, cfg.TrelloToken) @@ -52,43 +89,54 @@ func main() { // Set up router r := chi.NewRouter() - // Middleware + // Global middleware r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.Timeout(60 * time.Second)) + r.Use(sessionManager.LoadAndSave) // Session middleware must be applied globally - // Routes - r.Get("/", h.HandleDashboard) - r.Post("/api/refresh", h.HandleRefresh) - r.Get("/api/tasks", h.HandleGetTasks) - r.Get("/api/meals", h.HandleGetMeals) - r.Get("/api/boards", h.HandleGetBoards) - - // Tab routes for HTMX (using new TabsHandler) - r.Get("/tabs/tasks", tabsHandler.HandleTasks) - r.Get("/tabs/planning", tabsHandler.HandlePlanning) - r.Get("/tabs/meals", tabsHandler.HandleMeals) - r.Post("/tabs/refresh", h.HandleRefreshTab) - - // Trello card operations - r.Post("/cards", h.HandleCreateCard) - r.Post("/cards/complete", h.HandleCompleteCard) - - // Todoist task operations - r.Post("/tasks", h.HandleCreateTask) - r.Post("/tasks/complete", h.HandleCompleteTask) + // Public routes (no auth required) + r.Get("/login", authHandlers.HandleLoginPage) + r.Post("/login", authHandlers.HandleLogin) + r.Post("/logout", authHandlers.HandleLogout) - // Unified task completion (for Tasks tab Atoms) - r.Post("/complete-atom", h.HandleCompleteAtom) - - // Unified Quick Add (for Tasks tab) - r.Post("/unified-add", h.HandleUnifiedAdd) - r.Get("/partials/lists", h.HandleGetListsOptions) - - // Serve static files + // Serve static files (public) fileServer := http.FileServer(http.Dir("web/static")) r.Handle("/static/*", http.StripPrefix("/static/", fileServer)) + // Protected routes (auth required) + r.Group(func(r chi.Router) { + r.Use(authHandlers.Middleware().RequireAuth) + + // Dashboard + r.Get("/", h.HandleDashboard) + r.Post("/api/refresh", h.HandleRefresh) + r.Get("/api/tasks", h.HandleGetTasks) + r.Get("/api/meals", h.HandleGetMeals) + r.Get("/api/boards", h.HandleGetBoards) + + // Tab routes for HTMX + r.Get("/tabs/tasks", tabsHandler.HandleTasks) + r.Get("/tabs/planning", tabsHandler.HandlePlanning) + r.Get("/tabs/meals", tabsHandler.HandleMeals) + r.Post("/tabs/refresh", h.HandleRefreshTab) + + // Trello card operations + r.Post("/cards", h.HandleCreateCard) + r.Post("/cards/complete", h.HandleCompleteCard) + + // Todoist task operations + r.Post("/tasks", h.HandleCreateTask) + r.Post("/tasks/complete", h.HandleCompleteTask) + + // Unified task completion (for Tasks tab Atoms) + r.Post("/complete-atom", h.HandleCompleteAtom) + + // Unified Quick Add (for Tasks tab) + r.Post("/unified-add", h.HandleUnifiedAdd) + r.Get("/partials/lists", h.HandleGetListsOptions) + }) + // Start server addr := ":" + cfg.Port srv := &http.Server{ -- cgit v1.2.3