Files
dance-lessons-coach/pkg/user/api/auth_handler.go
Gabriel Radureau 40898edc52
Some checks failed
CI/CD Pipeline / CI Pipeline (pull_request) Successful in 7m36s
CI/CD Pipeline / CI Pipeline (push) Has been cancelled
🧪 test: add comprehensive BDD scenarios for authentication system
- Added 18 new authentication test scenarios
- Increased BDD test coverage from 14 to 25 scenarios
- Added input validation for registration and login endpoints
- Added step definitions for new test scenarios
- All authentication edge cases now covered

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-07 00:36:00 +02:00

293 lines
10 KiB
Go

package api
import (
"encoding/json"
"net/http"
"dance-lessons-coach/pkg/user"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
)
// AuthHandler handles authentication-related HTTP requests
type AuthHandler struct {
authService user.AuthService
userService user.UserService
}
// NewAuthHandler creates a new authentication handler
func NewAuthHandler(authService user.AuthService, userService user.UserService) *AuthHandler {
return &AuthHandler{
authService: authService,
userService: userService,
}
}
// RegisterRoutes registers authentication routes
func (h *AuthHandler) RegisterRoutes(router chi.Router) {
router.Post("/login", h.handleLogin)
router.Post("/admin/login", h.handleAdminLogin)
router.Post("/register", h.handleRegister)
router.Post("/password-reset/request", h.handlePasswordResetRequest)
router.Post("/password-reset/complete", h.handlePasswordResetComplete)
}
// LoginRequest represents a login request
type LoginRequest struct {
Username string `json:"username" validate:"required,min=3,max=50"`
Password string `json:"password" validate:"required,min=6"`
}
// LoginResponse represents a login response
type LoginResponse struct {
Token string `json:"token"`
}
// handleLogin godoc
// @Summary User login
// @Description Authenticate user and return JWT token
// @Tags API/v1/User
// @Accept json
// @Produce json
// @Param request body LoginRequest true "Login credentials"
// @Success 200 {object} LoginResponse "Successful authentication"
// @Failure 400 {object} map[string]string "Invalid request"
// @Failure 401 {object} map[string]string "Invalid credentials"
// @Failure 500 {object} map[string]string "Server error"
// @Router /v1/auth/login [post]
func (h *AuthHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid_request","message":"Invalid JSON request body"}`, http.StatusBadRequest)
return
}
// Validate username and password are not empty
if req.Username == "" || req.Password == "" {
http.Error(w, `{"error":"invalid_request","message":"Username and password are required"}`, http.StatusBadRequest)
return
}
// Authenticate user
user, err := h.authService.Authenticate(ctx, req.Username, req.Password)
if err != nil {
log.Trace().Ctx(ctx).Err(err).Str("username", req.Username).Msg("Authentication failed")
http.Error(w, `{"error":"invalid_credentials","message":"Invalid username or password"}`, http.StatusUnauthorized)
return
}
// Generate JWT token
token, err := h.authService.GenerateJWT(ctx, user)
if err != nil {
log.Error().Ctx(ctx).Err(err).Msg("Failed to generate JWT token")
http.Error(w, `{"error":"server_error","message":"Failed to generate authentication token"}`, http.StatusInternalServerError)
return
}
// Return token
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(LoginResponse{Token: token})
}
// RegisterRequest represents a user registration request
type RegisterRequest struct {
Username string `json:"username" validate:"required,min=3,max=50"`
Password string `json:"password" validate:"required,min=6"`
}
// handleRegister godoc
// @Summary User registration
// @Description Register a new user account
// @Tags API/v1/User
// @Accept json
// @Produce json
// @Param request body RegisterRequest true "Registration details"
// @Success 201 {object} map[string]string "User created"
// @Failure 400 {object} map[string]string "Invalid request"
// @Failure 409 {object} map[string]string "Username already taken"
// @Failure 500 {object} map[string]string "Server error"
// @Router /v1/auth/register [post]
func (h *AuthHandler) handleRegister(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid_request","message":"Invalid JSON request body"}`, http.StatusBadRequest)
return
}
// Validate username length (min 3, max 50 characters)
if len(req.Username) < 3 || len(req.Username) > 50 {
http.Error(w, `{"error":"invalid_username","message":"Username must be between 3 and 50 characters"}`, http.StatusBadRequest)
return
}
// Validate password length (min 6 characters)
if len(req.Password) < 6 {
http.Error(w, `{"error":"invalid_password","message":"Password must be at least 6 characters"}`, http.StatusBadRequest)
return
}
// Check if user already exists
exists, err := h.userService.UserExists(ctx, req.Username)
if err != nil {
log.Error().Ctx(ctx).Err(err).Msg("Failed to check if user exists")
http.Error(w, `{"error":"server_error","message":"Failed to process registration"}`, http.StatusInternalServerError)
return
}
if exists {
http.Error(w, `{"error":"user_exists","message":"Username already taken"}`, http.StatusConflict)
return
}
// Hash password
hashedPassword, err := h.userService.HashPassword(ctx, req.Password)
if err != nil {
log.Error().Ctx(ctx).Err(err).Msg("Failed to hash password")
http.Error(w, `{"error":"server_error","message":"Failed to process registration"}`, http.StatusInternalServerError)
return
}
// Create user
newUser := &user.User{
Username: req.Username,
PasswordHash: hashedPassword,
IsAdmin: false,
}
if err := h.userService.CreateUser(ctx, newUser); err != nil {
log.Error().Ctx(ctx).Err(err).Msg("Failed to create user")
http.Error(w, `{"error":"server_error","message":"Failed to create user"}`, http.StatusInternalServerError)
return
}
// Return success
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"message": "User registered successfully"})
}
// PasswordResetRequest represents a password reset request
type PasswordResetRequest struct {
Username string `json:"username" validate:"required,min=3,max=50"`
}
// handlePasswordResetRequest godoc
// @Summary Request password reset
// @Description Initiate password reset process for a user
// @Tags API/v1/User
// @Accept json
// @Produce json
// @Param request body PasswordResetRequest true "Password reset request"
// @Success 200 {object} map[string]string "Reset allowed"
// @Failure 400 {object} map[string]string "Invalid request"
// @Failure 500 {object} map[string]string "Server error"
// @Router /v1/auth/password-reset/request [post]
func (h *AuthHandler) handlePasswordResetRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req PasswordResetRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid_request","message":"Invalid JSON request body"}`, http.StatusBadRequest)
return
}
// Request password reset
if err := h.userService.RequestPasswordReset(ctx, req.Username); err != nil {
log.Error().Ctx(ctx).Err(err).Msg("Failed to request password reset")
http.Error(w, `{"error":"server_error","message":"Failed to process password reset request"}`, http.StatusInternalServerError)
return
}
// Return success
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"message": "Password reset allowed, user can now reset password"})
}
// PasswordResetCompleteRequest represents a password reset completion request
type PasswordResetCompleteRequest struct {
Username string `json:"username" validate:"required,min=3,max=50"`
NewPassword string `json:"new_password" validate:"required,min=6"`
}
// handlePasswordResetComplete godoc
// @Summary Complete password reset
// @Description Complete password reset with new password
// @Tags API/v1/User
// @Accept json
// @Produce json
// @Param request body PasswordResetCompleteRequest true "Password reset completion"
// @Success 200 {object} map[string]string "Password updated"
// @Failure 400 {object} map[string]string "Invalid request"
// @Failure 500 {object} map[string]string "Server error"
// @Router /v1/auth/password-reset/complete [post]
func (h *AuthHandler) handlePasswordResetComplete(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req PasswordResetCompleteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid_request","message":"Invalid JSON request body"}`, http.StatusBadRequest)
return
}
// Complete password reset
if err := h.userService.CompletePasswordReset(ctx, req.Username, req.NewPassword); err != nil {
log.Error().Ctx(ctx).Err(err).Msg("Failed to complete password reset")
http.Error(w, `{"error":"server_error","message":"Failed to complete password reset"}`, http.StatusInternalServerError)
return
}
// Return success
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"message": "Password reset completed successfully"})
}
// handleAdminLogin godoc
// @Summary Admin login
// @Description Authenticate admin user with master password
// @Tags Admin/User
// @Accept json
// @Produce json
// @Param request body LoginRequest true "Admin login credentials"
// @Success 200 {object} LoginResponse "Successful admin authentication"
// @Failure 400 {object} map[string]string "Invalid request"
// @Failure 401 {object} map[string]string "Invalid admin credentials"
// @Failure 500 {object} map[string]string "Server error"
// @Router /v1/auth/admin/login [post]
func (h *AuthHandler) handleAdminLogin(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid_request","message":"Invalid JSON request body"}`, http.StatusBadRequest)
return
}
// Authenticate admin
adminUser, err := h.authService.AdminAuthenticate(ctx, req.Password)
if err != nil {
log.Trace().Ctx(ctx).Err(err).Msg("Admin authentication failed")
http.Error(w, `{"error":"invalid_credentials","message":"Invalid admin credentials"}`, http.StatusUnauthorized)
return
}
// Generate JWT token
token, err := h.authService.GenerateJWT(ctx, adminUser)
if err != nil {
log.Error().Ctx(ctx).Err(err).Msg("Failed to generate JWT token for admin")
http.Error(w, `{"error":"server_error","message":"Failed to generate authentication token"}`, http.StatusInternalServerError)
return
}
// Return token
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(LoginResponse{Token: token})
}