Files
telecom-platform/apps/api-server/internal/handlers/auth.go
T
nutcas3 783d5c147b feat: Add XSS protection and HATEOAS links to subscriber and auth handlers with cursor pagination
- Add bluemonday import to auth.go and subscriber.go
- Add UGCPolicy sanitization for username, email, firstName, lastName, and role in Register
- Add conditional XSS sanitization for firstName, lastName, email, and MSISDN in CreateSubscriber and UpdateSubscriber
- Replace Page and PageSize with Cursor and Limit in ListSubscribers query parameters
- Add HATEOAS links to CreateSubscriber, GetSubscriber
2026-05-01 09:06:32 +03:00

353 lines
9.3 KiB
Go

package handlers
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/microcosm-cc/bluemonday"
"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
}
// Sanitize user input to prevent XSS
policy := bluemonday.UGCPolicy()
req.Username = policy.Sanitize(req.Username)
req.Email = policy.Sanitize(req.Email)
req.FirstName = policy.Sanitize(req.FirstName)
req.LastName = policy.Sanitize(req.LastName)
req.Role = policy.Sanitize(req.Role)
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 <token>" 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"})
}