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 --- internal/auth/auth.go | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 internal/auth/auth.go (limited to 'internal/auth/auth.go') diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..a602dad --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,142 @@ +package auth + +import ( + "database/sql" + "errors" + "time" + + "golang.org/x/crypto/bcrypt" +) + +var ( + ErrInvalidCredentials = errors.New("invalid username or password") + ErrUserNotFound = errors.New("user not found") + ErrUserExists = errors.New("username already exists") +) + +// User represents an authenticated user +type User struct { + ID int64 + Username string + PasswordHash string + CreatedAt time.Time +} + +// Service handles authentication operations +type Service struct { + db *sql.DB +} + +// NewService creates a new auth service +func NewService(db *sql.DB) *Service { + return &Service{db: db} +} + +// Authenticate verifies username and password, returns user ID if valid +func (s *Service) Authenticate(username, password string) (*User, error) { + user, err := s.GetUserByUsername(username) + if err != nil { + if errors.Is(err, ErrUserNotFound) { + return nil, ErrInvalidCredentials + } + return nil, err + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { + return nil, ErrInvalidCredentials + } + + return user, nil +} + +// GetUserByUsername retrieves a user by username +func (s *Service) GetUserByUsername(username string) (*User, error) { + var user User + err := s.db.QueryRow( + `SELECT id, username, password_hash, created_at FROM users WHERE username = ?`, + username, + ).Scan(&user.ID, &user.Username, &user.PasswordHash, &user.CreatedAt) + + if err == sql.ErrNoRows { + return nil, ErrUserNotFound + } + if err != nil { + return nil, err + } + + return &user, nil +} + +// GetUserByID retrieves a user by ID +func (s *Service) GetUserByID(id int64) (*User, error) { + var user User + err := s.db.QueryRow( + `SELECT id, username, password_hash, created_at FROM users WHERE id = ?`, + id, + ).Scan(&user.ID, &user.Username, &user.PasswordHash, &user.CreatedAt) + + if err == sql.ErrNoRows { + return nil, ErrUserNotFound + } + if err != nil { + return nil, err + } + + return &user, nil +} + +// CreateUser creates a new user with the given username and password +func (s *Service) CreateUser(username, password string) (*User, error) { + // Check if user exists + _, err := s.GetUserByUsername(username) + if err == nil { + return nil, ErrUserExists + } + if !errors.Is(err, ErrUserNotFound) { + return nil, err + } + + // Hash password + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + // Insert user + result, err := s.db.Exec( + `INSERT INTO users (username, password_hash) VALUES (?, ?)`, + username, string(hash), + ) + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + return s.GetUserByID(id) +} + +// UserCount returns the number of users in the database +func (s *Service) UserCount() (int, error) { + var count int + err := s.db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&count) + return count, err +} + +// EnsureDefaultUser creates a default admin user if no users exist +func (s *Service) EnsureDefaultUser(username, password string) error { + count, err := s.UserCount() + if err != nil { + return err + } + + if count == 0 { + _, err = s.CreateUser(username, password) + return err + } + + return nil +} -- cgit v1.2.3