diff --git a/pkg/server/server.go b/pkg/server/server.go index 00e67d1..28f8b9b 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -33,14 +33,15 @@ import ( var swaggerJSON embed.FS type Server struct { - router *chi.Mux - readyCtx context.Context - withOTEL bool - config *config.Config - tracerProvider *sdktrace.TracerProvider - validator *validation.Validator - userRepo user.UserRepository - authService user.AuthService + router *chi.Mux + readyCtx context.Context + withOTEL bool + config *config.Config + tracerProvider *sdktrace.TracerProvider + validator *validation.Validator + userRepo user.UserRepository + authService user.AuthService + passwordResetService user.PasswordResetService } func NewServer(cfg *config.Config, readyCtx context.Context) *Server { @@ -53,33 +54,34 @@ func NewServer(cfg *config.Config, readyCtx context.Context) *Server { } // Initialize user repository and services - userRepo, authService, err := initializeUserServices(cfg) + userRepo, authService, passwordResetService, err := initializeUserServices(cfg) if err != nil { log.Warn().Err(err).Msg("Failed to initialize user services, user functionality will be disabled") } s := &Server{ - router: chi.NewRouter(), - readyCtx: readyCtx, - withOTEL: cfg.GetTelemetryEnabled(), - config: cfg, - validator: validator, - userRepo: userRepo, - authService: authService, + router: chi.NewRouter(), + readyCtx: readyCtx, + withOTEL: cfg.GetTelemetryEnabled(), + config: cfg, + validator: validator, + userRepo: userRepo, + authService: authService, + passwordResetService: passwordResetService, } s.setupRoutes() return s } // initializeUserServices initializes the user repository and authentication service -func initializeUserServices(cfg *config.Config) (user.UserRepository, user.AuthService, error) { +func initializeUserServices(cfg *config.Config) (user.UserRepository, user.AuthService, user.PasswordResetService, error) { // Use in-memory SQLite database dbPath := "file::memory:?cache=shared" // Create user repository repo, err := user.NewSQLiteRepository(dbPath) if err != nil { - return nil, nil, fmt.Errorf("failed to create user repository: %w", err) + return nil, nil, nil, fmt.Errorf("failed to create user repository: %w", err) } // Create JWT config @@ -92,7 +94,10 @@ func initializeUserServices(cfg *config.Config) (user.UserRepository, user.AuthS // Create auth service authService := user.NewAuthService(repo, jwtConfig, cfg.GetAdminMasterPassword()) - return repo, authService, nil + // Create password reset service + passwordResetService := user.NewPasswordResetService(repo, authService) + + return repo, authService, passwordResetService, nil } func (s *Server) setupRoutes() { @@ -162,9 +167,16 @@ func (s *Server) registerApiV1Routes(r chi.Router) { // Register user authentication routes if s.authService != nil && s.userRepo != nil { - authHandler := userapi.NewAuthHandler(s.authService, s.userRepo) + // Create separate handlers for better separation of concerns + authHandler := userapi.NewAuthHandler(s.authService) + // Cast authService to PasswordService for user handler + userHandler := userapi.NewUserHandler(s.userRepo, s.authService.(user.PasswordService)) + passwordHandler := userapi.NewPasswordResetHandler(s.passwordResetService) + r.Route("/auth", func(r chi.Router) { authHandler.RegisterRoutes(r) + userHandler.RegisterRoutes(r) + passwordHandler.RegisterRoutes(r) }) } } diff --git a/pkg/user/api/auth_handler.go b/pkg/user/api/auth_handler.go index 4b59136..f815eb2 100644 --- a/pkg/user/api/auth_handler.go +++ b/pkg/user/api/auth_handler.go @@ -12,18 +12,13 @@ import ( // AuthHandler handles authentication-related HTTP requests type AuthHandler struct { - authService user.AuthService - userRepo user.UserRepository - passwordResetService user.PasswordResetService + authService user.AuthService } // NewAuthHandler creates a new authentication handler -func NewAuthHandler(authService user.AuthService, userRepo user.UserRepository) *AuthHandler { - passwordResetService := user.NewPasswordResetService(userRepo, authService.(*user.AuthServiceImpl)) +func NewAuthHandler(authService user.AuthService) *AuthHandler { return &AuthHandler{ - authService: authService, - userRepo: userRepo, - passwordResetService: passwordResetService, + authService: authService, } } @@ -31,9 +26,6 @@ func NewAuthHandler(authService user.AuthService, userRepo user.UserRepository) 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 @@ -110,115 +102,3 @@ func (h *AuthHandler) handleAdminLogin(w http.ResponseWriter, r *http.Request) { 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 handles user registration requests -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 - } - - // Check if user already exists - exists, err := h.userRepo.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.authService.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.userRepo.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 handles password reset requests -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.passwordResetService.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 handles password reset completion requests -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.passwordResetService.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"}) -} diff --git a/pkg/user/auth_service.go b/pkg/user/auth_service.go index 4a43495..19bb142 100644 --- a/pkg/user/auth_service.go +++ b/pkg/user/auth_service.go @@ -17,7 +17,7 @@ type JWTConfig struct { Issuer string } -// AuthServiceImpl implements the AuthService interface +// AuthServiceImpl implements the AuthService and PasswordService interfaces type AuthServiceImpl struct { repo UserRepository jwtConfig JWTConfig @@ -130,7 +130,7 @@ func (s *AuthServiceImpl) ValidateJWT(ctx context.Context, tokenString string) ( return user, nil } -// HashPassword hashes a password using bcrypt +// HashPassword hashes a password using bcrypt (implements PasswordService interface) func (s *AuthServiceImpl) HashPassword(ctx context.Context, password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { diff --git a/pkg/user/user.go b/pkg/user/user.go index cf857dd..32481f8 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -32,12 +32,16 @@ type UserRepository interface { UserExists(ctx context.Context, username string) (bool, error) } +// PasswordService defines the interface for password operations +type PasswordService interface { + HashPassword(ctx context.Context, password string) (string, error) +} + // AuthService defines the interface for authentication type AuthService interface { Authenticate(ctx context.Context, username, password string) (*User, error) GenerateJWT(ctx context.Context, user *User) (string, error) ValidateJWT(ctx context.Context, token string) (*User, error) - HashPassword(ctx context.Context, password string) (string, error) AdminAuthenticate(ctx context.Context, masterPassword string) (*User, error) }