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 }