Files
WeKnora/internal/handler/auth.go
2025-09-16 15:46:18 +08:00

344 lines
10 KiB
Go

package handler
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/Tencent/WeKnora/internal/errors"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
)
// AuthHandler implements HTTP request handlers for user authentication
// Provides functionality for user registration, login, logout, and token management
// through the REST API endpoints
type AuthHandler struct {
userService interfaces.UserService
tenantService interfaces.TenantService
}
// NewAuthHandler creates a new auth handler instance with the provided services
// Parameters:
// - userService: An implementation of the UserService interface for business logic
// - tenantService: An implementation of the TenantService interface for tenant management
//
// Returns a pointer to the newly created AuthHandler
func NewAuthHandler(userService interfaces.UserService, tenantService interfaces.TenantService) *AuthHandler {
return &AuthHandler{
userService: userService,
tenantService: tenantService,
}
}
// Register handles the HTTP request for user registration
// It deserializes the request body into a registration request object, validates it,
// calls the service to create the user, and returns the result
// Parameters:
// - c: Gin context for the HTTP request
func (h *AuthHandler) Register(c *gin.Context) {
ctx := c.Request.Context()
logger.Info(ctx, "Start user registration")
var req types.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error(ctx, "Failed to parse registration request parameters", err)
appErr := errors.NewValidationError("Invalid registration parameters").WithDetails(err.Error())
c.Error(appErr)
return
}
// Validate required fields
if req.Username == "" || req.Email == "" || req.Password == "" {
logger.Error(ctx, "Missing required registration fields")
appErr := errors.NewValidationError("Username, email and password are required")
c.Error(appErr)
return
}
// Call service to register user
user, err := h.userService.Register(ctx, &req)
if err != nil {
logger.Errorf(ctx, "Failed to register user: %v", err)
appErr := errors.NewBadRequestError("Registration failed").WithDetails(err.Error())
c.Error(appErr)
return
}
// Return success response
response := &types.RegisterResponse{
Success: true,
Message: "Registration successful",
User: user,
}
logger.Infof(ctx, "User registered successfully: %s", user.Email)
c.JSON(http.StatusCreated, response)
}
// Login handles the HTTP request for user login
// It deserializes the request body into a login request object, validates it,
// calls the service to authenticate the user, and returns tokens
// Parameters:
// - c: Gin context for the HTTP request
func (h *AuthHandler) Login(c *gin.Context) {
ctx := c.Request.Context()
logger.Info(ctx, "Start user login")
var req types.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error(ctx, "Failed to parse login request parameters", err)
appErr := errors.NewValidationError("Invalid login parameters").WithDetails(err.Error())
c.Error(appErr)
return
}
// Validate required fields
if req.Email == "" || req.Password == "" {
logger.Error(ctx, "Missing required login fields")
appErr := errors.NewValidationError("Email and password are required")
c.Error(appErr)
return
}
// Call service to authenticate user
response, err := h.userService.Login(ctx, &req)
if err != nil {
logger.Errorf(ctx, "Failed to login user: %v", err)
appErr := errors.NewUnauthorizedError("Login failed").WithDetails(err.Error())
c.Error(appErr)
return
}
// Check if login was successful
if !response.Success {
logger.Warnf(ctx, "Login failed: %s", response.Message)
c.JSON(http.StatusUnauthorized, response)
return
}
// User is already in the correct format from service
logger.Infof(ctx, "User logged in successfully: %s", req.Email)
c.JSON(http.StatusOK, response)
}
// Logout handles the HTTP request for user logout
// It extracts the token from the Authorization header and revokes it
// Parameters:
// - c: Gin context for the HTTP request
func (h *AuthHandler) Logout(c *gin.Context) {
ctx := c.Request.Context()
logger.Info(ctx, "Start user logout")
// Extract token from Authorization header
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
logger.Error(ctx, "Missing Authorization header")
appErr := errors.NewValidationError("Authorization header is required")
c.Error(appErr)
return
}
// Parse Bearer token
tokenParts := strings.Split(authHeader, " ")
if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
logger.Error(ctx, "Invalid Authorization header format")
appErr := errors.NewValidationError("Invalid Authorization header format")
c.Error(appErr)
return
}
token := tokenParts[1]
// Revoke token
err := h.userService.RevokeToken(ctx, token)
if err != nil {
logger.Errorf(ctx, "Failed to revoke token: %v", err)
appErr := errors.NewInternalServerError("Logout failed").WithDetails(err.Error())
c.Error(appErr)
return
}
logger.Info(ctx, "User logged out successfully")
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Logout successful",
})
}
// RefreshToken handles the HTTP request for refreshing access tokens
// It extracts the refresh token from the request body and generates new tokens
// Parameters:
// - c: Gin context for the HTTP request
func (h *AuthHandler) RefreshToken(c *gin.Context) {
ctx := c.Request.Context()
logger.Info(ctx, "Start token refresh")
var req struct {
RefreshToken string `json:"refreshToken" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error(ctx, "Failed to parse refresh token request", err)
appErr := errors.NewValidationError("Invalid refresh token request").WithDetails(err.Error())
c.Error(appErr)
return
}
// Call service to refresh token
accessToken, newRefreshToken, err := h.userService.RefreshToken(ctx, req.RefreshToken)
if err != nil {
logger.Errorf(ctx, "Failed to refresh token: %v", err)
appErr := errors.NewUnauthorizedError("Token refresh failed").WithDetails(err.Error())
c.Error(appErr)
return
}
logger.Info(ctx, "Token refreshed successfully")
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Token refreshed successfully",
"access_token": accessToken,
"refresh_token": newRefreshToken,
})
}
// GetCurrentUser handles the HTTP request for getting current user information
// It extracts the user from the context (set by auth middleware) and returns user info
// Parameters:
// - c: Gin context for the HTTP request
func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
ctx := c.Request.Context()
logger.Debugf(ctx, "Get current user info")
// Get current user from service (which extracts from context)
user, err := h.userService.GetCurrentUser(ctx)
if err != nil {
logger.Errorf(ctx, "Failed to get current user: %v", err)
appErr := errors.NewUnauthorizedError("Failed to get user information").WithDetails(err.Error())
c.Error(appErr)
return
}
// Get tenant information
var tenant *types.Tenant
if user.TenantID > 0 {
tenant, err = h.tenantService.GetTenantByID(ctx, user.TenantID)
if err != nil {
logger.Warnf(ctx, "Failed to get tenant info for user %s, tenant ID %d: %v", user.Email, user.TenantID, err)
// Don't fail the request if tenant info is not available
} else {
logger.Debugf(ctx, "Retrieved tenant info for user %s: %s", user.Email, tenant.Name)
}
}
logger.Debugf(ctx, "Retrieved current user info: %s", user.Email)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"user": user.ToUserInfo(),
"tenant": tenant,
},
})
}
// ChangePassword handles the HTTP request for changing user password
// It extracts the current user and validates the old password before setting new one
// Parameters:
// - c: Gin context for the HTTP request
func (h *AuthHandler) ChangePassword(c *gin.Context) {
ctx := c.Request.Context()
logger.Info(ctx, "Start password change")
var req struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error(ctx, "Failed to parse password change request", err)
appErr := errors.NewValidationError("Invalid password change request").WithDetails(err.Error())
c.Error(appErr)
return
}
// Get current user
user, err := h.userService.GetCurrentUser(ctx)
if err != nil {
logger.Errorf(ctx, "Failed to get current user: %v", err)
appErr := errors.NewUnauthorizedError("Failed to get user information").WithDetails(err.Error())
c.Error(appErr)
return
}
// Change password
err = h.userService.ChangePassword(ctx, user.ID, req.OldPassword, req.NewPassword)
if err != nil {
logger.Errorf(ctx, "Failed to change password: %v", err)
appErr := errors.NewBadRequestError("Password change failed").WithDetails(err.Error())
c.Error(appErr)
return
}
logger.Infof(ctx, "Password changed successfully for user: %s", user.Email)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Password changed successfully",
})
}
// ValidateToken handles the HTTP request for validating access tokens
// It extracts the token from the Authorization header and validates it
// Parameters:
// - c: Gin context for the HTTP request
func (h *AuthHandler) ValidateToken(c *gin.Context) {
ctx := c.Request.Context()
logger.Info(ctx, "Start token validation")
// Extract token from Authorization header
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
logger.Error(ctx, "Missing Authorization header")
appErr := errors.NewValidationError("Authorization header is required")
c.Error(appErr)
return
}
// Parse Bearer token
tokenParts := strings.Split(authHeader, " ")
if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
logger.Error(ctx, "Invalid Authorization header format")
appErr := errors.NewValidationError("Invalid Authorization header format")
c.Error(appErr)
return
}
token := tokenParts[1]
// Validate token
user, err := h.userService.ValidateToken(ctx, token)
if err != nil {
logger.Errorf(ctx, "Failed to validate token: %v", err)
appErr := errors.NewUnauthorizedError("Token validation failed").WithDetails(err.Error())
c.Error(appErr)
return
}
logger.Infof(ctx, "Token validated successfully for user: %s", user.Email)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Token is valid",
"user": user.ToUserInfo(),
})
}