mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 19:12:54 +00:00
- 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
353 lines
9.3 KiB
Go
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"})
|
|
}
|