From c39ea181bfafd8e7ed58ff0e48dc8b01ba829557 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Wed, 22 Apr 2026 20:57:34 +0300 Subject: [PATCH] feat: Add authentication handler with login, registration, user management, and JWT token operations - Add AuthHandler struct with authService dependency and NewAuthHandler constructor - Add LoginRequest/LoginResponse/RegisterRequest/ChangePasswordRequest structs with validation tags - Add Login method with username/password validation, token generation, and user response - Add Register method with email/password/role validation and user creation - Add RefreshToken method with JWT refresh token validation --- apps/api-server/internal/handlers/auth.go | 343 ++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 apps/api-server/internal/handlers/auth.go diff --git a/apps/api-server/internal/handlers/auth.go b/apps/api-server/internal/handlers/auth.go new file mode 100644 index 0000000..94b7b5d --- /dev/null +++ b/apps/api-server/internal/handlers/auth.go @@ -0,0 +1,343 @@ +package handlers + +import ( + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" + "github.com/nutcas3/telecom-platform/apps/api-server/internal/services" +) + +// AuthHandler handles authentication endpoints +type AuthHandler struct { + authService *services.AuthService +} + +// NewAuthHandler creates a new auth handler +func NewAuthHandler(authService *services.AuthService) *AuthHandler { + return &AuthHandler{ + authService: authService, + } +} + +// LoginRequest represents a login request +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// LoginResponse represents a login response +type LoginResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + User *models.User `json:"user"` +} + +// RegisterRequest represents a registration request +type RegisterRequest struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` + FirstName string `json:"first_name" binding:"required"` + LastName string `json:"last_name" binding:"required"` + Role string `json:"role" binding:"required,oneof=admin operator viewer"` +} + +// ChangePasswordRequest represents a password change request +type ChangePasswordRequest struct { + OldPassword string `json:"old_password" binding:"required"` + NewPassword string `json:"new_password" binding:"required,min=6"` +} + +// Login handles user login +func (h *AuthHandler) Login(c *gin.Context) { + var req LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + accessToken, refreshToken, user, err := h.authService.Login(req.Username, req.Password) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + response := LoginResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + User: user, + } + + c.JSON(http.StatusOK, response) +} + +// Register handles user registration +func (h *AuthHandler) Register(c *gin.Context) { + var req RegisterRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user, err := h.authService.Register(req.Username, req.Email, req.Password, req.FirstName, req.LastName, req.Role) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Don't return password hash + user.Password = "" + + c.JSON(http.StatusCreated, gin.H{ + "message": "User registered successfully", + "user": user, + }) +} + +// RefreshToken handles token refresh +func (h *AuthHandler) RefreshToken(c *gin.Context) { + var req struct { + RefreshToken string `json:"refresh_token" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + accessToken, refreshToken, err := h.authService.GetJWTService().RefreshToken(req.RefreshToken) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "access_token": accessToken, + "refresh_token": refreshToken, + }) +} + +// Logout handles user logout +func (h *AuthHandler) Logout(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Authorization header required"}) + return + } + + // Extract token from "Bearer " format + tokenParts := strings.Split(authHeader, " ") + if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid authorization header format"}) + return + } + + token := tokenParts[1] + if err := h.authService.GetJWTService().InvalidateSession(token); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to logout"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"}) +} + +// GetProfile handles getting the current user's profile +func (h *AuthHandler) GetProfile(c *gin.Context) { + userID, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + return + } + + user, err := h.authService.GetUserByID(userID.(uint)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + // Don't return password hash + user.Password = "" + + c.JSON(http.StatusOK, user) +} + +// ChangePassword handles password change +func (h *AuthHandler) ChangePassword(c *gin.Context) { + userID, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + return + } + + var req ChangePasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.authService.ChangePassword(userID.(uint), req.OldPassword, req.NewPassword); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Password changed successfully"}) +} + +// GetUsers handles listing users (admin only) +func (h *AuthHandler) GetUsers(c *gin.Context) { + // Parse pagination parameters + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) + if page < 1 { + page = 1 + } + if limit < 1 || limit > 100 { + limit = 20 + } + + offset := (page - 1) * limit + + var users []models.User + var total int64 + + // Get total count + if err := h.authService.GetDB().Model(&models.User{}).Count(&total).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count users"}) + return + } + + // Get users with pagination + if err := h.authService.GetDB().Offset(offset).Limit(limit).Find(&users).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"}) + return + } + + // Remove password hashes + for i := range users { + users[i].Password = "" + } + + c.JSON(http.StatusOK, gin.H{ + "users": users, + "pagination": gin.H{ + "page": page, + "limit": limit, + "total": total, + "total_pages": (total + int64(limit) - 1) / int64(limit), + }, + }) +} + +// CreateUser handles creating a new user (admin only) +func (h *AuthHandler) CreateUser(c *gin.Context) { + var req RegisterRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user, err := h.authService.Register(req.Username, req.Email, req.Password, req.FirstName, req.LastName, req.Role) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Don't return password hash + user.Password = "" + + c.JSON(http.StatusCreated, gin.H{ + "message": "User created successfully", + "user": user, + }) +} + +// UpdateUser handles updating a user (admin only) +func (h *AuthHandler) UpdateUser(c *gin.Context) { + userIDStr := c.Param("id") + userID, err := strconv.ParseUint(userIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + return + } + + var req struct { + FirstName *string `json:"first_name"` + LastName *string `json:"last_name"` + Email *string `json:"email"` + Role *string `json:"role"` + IsActive *bool `json:"is_active"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Get user + var user models.User + if err := h.authService.GetDB().First(&user, userID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + // Update fields if provided + if req.FirstName != nil { + user.FirstName = *req.FirstName + } + if req.LastName != nil { + user.LastName = *req.LastName + } + if req.Email != nil { + user.Email = *req.Email + } + if req.Role != nil { + user.Role = *req.Role + } + if req.IsActive != nil { + user.IsActive = *req.IsActive + } + + if err := h.authService.GetDB().Save(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"}) + return + } + + // Don't return password hash + user.Password = "" + + c.JSON(http.StatusOK, user) +} + +// DeleteUser handles deleting a user (admin only) +func (h *AuthHandler) DeleteUser(c *gin.Context) { + userIDStr := c.Param("id") + userID, err := strconv.ParseUint(userIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + return + } + + // Get current user + currentUserID, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + return + } + + // Prevent self-deletion + if currentUserID.(uint) == uint(userID) { + c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete your own account"}) + return + } + + // Delete user + if err := h.authService.GetDB().Delete(&models.User{}, userID).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"}) +}