diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-01-20 11:34:33 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-01-20 11:34:33 -1000 |
| commit | 08bbcf18b1207153983261652b4a43a9b36f386c (patch) | |
| tree | e6665608c7c8a87d6c789cf8b4c56d466df6bb8b /internal/auth/auth.go | |
| parent | 07ba815e8517ee2d3a5fa531361bbd09bdfcbaa7 (diff) | |
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 <noreply@anthropic.com>
Diffstat (limited to 'internal/auth/auth.go')
| -rw-r--r-- | internal/auth/auth.go | 142 |
1 files changed, 142 insertions, 0 deletions
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 +} |
