package auth import ( "net/http" "strings" "time" "github.com/atlasos/calypso/internal/common/config" "github.com/atlasos/calypso/internal/common/database" "github.com/atlasos/calypso/internal/common/logger" "github.com/atlasos/calypso/internal/common/password" "github.com/atlasos/calypso/internal/iam" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) // Handler handles authentication requests type Handler struct { db *database.DB config *config.Config logger *logger.Logger } // NewHandler creates a new auth handler func NewHandler(db *database.DB, cfg *config.Config, log *logger.Logger) *Handler { return &Handler{ db: db, config: cfg, logger: log, } } // 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 { Token string `json:"token"` ExpiresAt time.Time `json:"expires_at"` User UserInfo `json:"user"` } // UserInfo represents user information in auth responses type UserInfo struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` FullName string `json:"full_name"` Roles []string `json:"roles"` } // Login handles user login func (h *Handler) Login(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } // Get user from database user, err := iam.GetUserByUsername(h.db, req.Username) if err != nil { h.logger.Warn("Login attempt failed", "username", req.Username, "error", "user not found") c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } // Check if user is active if !user.IsActive { c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled"}) return } // Verify password if !h.verifyPassword(req.Password, user.PasswordHash) { h.logger.Warn("Login attempt failed", "username", req.Username, "error", "invalid password") c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } // Generate JWT token token, expiresAt, err := h.generateToken(user) if err != nil { h.logger.Error("Failed to generate token", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"}) return } // Create session if err := h.createSession(user.ID, token, c.ClientIP(), c.GetHeader("User-Agent"), expiresAt); err != nil { h.logger.Error("Failed to create session", "error", err) // Continue anyway, token is still valid } // Update last login if err := h.updateLastLogin(user.ID); err != nil { h.logger.Warn("Failed to update last login", "error", err) } // Get user roles roles, err := iam.GetUserRoles(h.db, user.ID) if err != nil { h.logger.Warn("Failed to get user roles", "error", err) roles = []string{} } h.logger.Info("User logged in successfully", "username", req.Username, "user_id", user.ID) c.JSON(http.StatusOK, LoginResponse{ Token: token, ExpiresAt: expiresAt, User: UserInfo{ ID: user.ID, Username: user.Username, Email: user.Email, FullName: user.FullName, Roles: roles, }, }) } // Logout handles user logout func (h *Handler) Logout(c *gin.Context) { // Extract token authHeader := c.GetHeader("Authorization") if authHeader != "" { parts := strings.SplitN(authHeader, " ", 2) if len(parts) == 2 && parts[0] == "Bearer" { // Invalidate session (token hash would be stored) // For now, just return success } } c.JSON(http.StatusOK, gin.H{"message": "logged out successfully"}) } // Me returns current user information func (h *Handler) Me(c *gin.Context) { user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"}) return } authUser, ok := user.(*iam.User) if !ok { c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user context"}) return } roles, err := iam.GetUserRoles(h.db, authUser.ID) if err != nil { h.logger.Warn("Failed to get user roles", "error", err) roles = []string{} } c.JSON(http.StatusOK, UserInfo{ ID: authUser.ID, Username: authUser.Username, Email: authUser.Email, FullName: authUser.FullName, Roles: roles, }) } // ValidateToken validates a JWT token and returns the user func (h *Handler) ValidateToken(tokenString string) (*iam.User, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } return []byte(h.config.Auth.JWTSecret), nil }) if err != nil { return nil, err } if !token.Valid { return nil, jwt.ErrSignatureInvalid } claims, ok := token.Claims.(jwt.MapClaims) if !ok { return nil, jwt.ErrInvalidKey } userID, ok := claims["user_id"].(string) if !ok { return nil, jwt.ErrInvalidKey } // Get user from database user, err := iam.GetUserByID(h.db, userID) if err != nil { return nil, err } if !user.IsActive { return nil, jwt.ErrInvalidKey } return user, nil } // verifyPassword verifies a password against an Argon2id hash func (h *Handler) verifyPassword(pwd, hash string) bool { valid, err := password.VerifyPassword(pwd, hash) if err != nil { h.logger.Warn("Password verification error", "error", err) return false } return valid } // generateToken generates a JWT token for a user func (h *Handler) generateToken(user *iam.User) (string, time.Time, error) { expiresAt := time.Now().Add(h.config.Auth.TokenLifetime) claims := jwt.MapClaims{ "user_id": user.ID, "username": user.Username, "exp": expiresAt.Unix(), "iat": time.Now().Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(h.config.Auth.JWTSecret)) if err != nil { return "", time.Time{}, err } return tokenString, expiresAt, nil } // createSession creates a session record in the database func (h *Handler) createSession(userID, token, ipAddress, userAgent string, expiresAt time.Time) error { // Hash the token for storage using SHA-256 tokenHash := HashToken(token) query := ` INSERT INTO sessions (user_id, token_hash, ip_address, user_agent, expires_at) VALUES ($1, $2, $3, $4, $5) ` _, err := h.db.Exec(query, userID, tokenHash, ipAddress, userAgent, expiresAt) return err } // updateLastLogin updates the user's last login timestamp func (h *Handler) updateLastLogin(userID string) error { query := `UPDATE users SET last_login_at = NOW() WHERE id = $1` _, err := h.db.Exec(query, userID) return err }