summaryrefslogtreecommitdiff
path: root/internal/auth/auth.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/auth/auth.go')
-rw-r--r--internal/auth/auth.go142
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
+}