package handlers import ( "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/imc/backend/internal/auth" "github.com/imc/backend/internal/db" ) const MaxLoginAttempts = 5 type AuthHandler struct { db *db.DB jwtManager *auth.JWTManager trustedProxies []string } func NewAuthHandler(database *db.DB, jwtManager *auth.JWTManager, trustedProxies []string) *AuthHandler { return &AuthHandler{ db: database, jwtManager: jwtManager, trustedProxies: trustedProxies, } } type LoginRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } type ChangePasswordRequest struct { OldPassword string `json:"oldPassword" binding:"required"` NewPassword string `json:"newPassword" binding:"required,min=8"` } type UserResponse struct { ID uint `json:"id"` Username string `json:"username"` Role string `json:"role"` Domains []string `json:"domains"` } func (h *AuthHandler) Login(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { Error(c, http.StatusBadRequest, "invalid request body") return } h.cleanupOldAttempts() ip := h.getClientIP(c) if isLockedOut(ip, req.Username, h.db) { Error(c, http.StatusTooManyRequests, "too many failed attempts, try again later") return } user, err := h.db.GetImcUserByUsername(req.Username) if err != nil || user == nil || !auth.CheckPassword(req.Password, user.PasswordHash) { recordFailedAttempt(req.Username, ip, h.db) Error(c, http.StatusUnauthorized, "invalid credentials") return } clearFailedAttempts(req.Username, ip, h.db) domains, _ := h.db.GetUserAccessibleDomains(user.ID, user.Role == "admin") domainNames := make([]string, len(domains)) for i, d := range domains { domainNames[i] = d.Name } token, err := h.jwtManager.GenerateToken(user.ID, user.Username, user.Role, 24*time.Hour) if err != nil { Error(c, http.StatusInternalServerError, "failed to generate token") return } Success(c, map[string]interface{}{ "token": token, "user": UserResponse{ ID: user.ID, Username: user.Username, Role: user.Role, Domains: domainNames, }, }) } func (h *AuthHandler) Me(c *gin.Context) { authCtx := GetAuthContext(c) if authCtx == nil { Error(c, http.StatusUnauthorized, "not authenticated") return } user, err := h.db.GetImcUserByID(authCtx.UserID) if err != nil || user == nil { Error(c, http.StatusNotFound, "user not found") return } domains, _ := h.db.GetUserAccessibleDomains(user.ID, user.Role == "admin") domainNames := make([]string, len(domains)) for i, d := range domains { domainNames[i] = d.Name } Success(c, UserResponse{ ID: user.ID, Username: user.Username, Role: user.Role, Domains: domainNames, }) } func (h *AuthHandler) ChangePassword(c *gin.Context) { authCtx := GetAuthContext(c) if authCtx == nil { Error(c, http.StatusUnauthorized, "authentication required") return } var req ChangePasswordRequest if err := c.ShouldBindJSON(&req); err != nil { Error(c, http.StatusBadRequest, "invalid request") return } user, err := h.db.GetImcUserByID(authCtx.UserID) if err != nil || user == nil { Error(c, http.StatusNotFound, "user not found") return } if !auth.CheckPassword(req.OldPassword, user.PasswordHash) { Error(c, http.StatusUnauthorized, "current password is incorrect") return } newHash, err := auth.HashPassword(req.NewPassword) if err != nil { Error(c, http.StatusInternalServerError, "failed to hash password") return } err = h.db.UpdateImcUserPassword(user.ID, newHash) if err != nil { Error(c, http.StatusInternalServerError, "failed to update password") return } Success(c, map[string]string{"message": "password updated successfully"}) } func (h *AuthHandler) Logout(c *gin.Context) { Success(c, map[string]string{"message": "logged out"}) } func isLockedOut(ip, identifier string, database *db.DB) bool { var count int64 cutoff := time.Now().Add(-15 * time.Minute) database.Model(&db.ImcLoginAttempt{}). Where("ip_address = ? AND attempted_at > ? AND successful = false", ip, cutoff). Count(&count) if count >= MaxLoginAttempts { return true } database.Model(&db.ImcLoginAttempt{}). Where("email = ? AND attempted_at > ? AND successful = false", identifier, cutoff). Count(&count) return count >= MaxLoginAttempts } func recordFailedAttempt(identifier, ip string, database *db.DB) { database.Create(&db.ImcLoginAttempt{ Email: identifier, IPAddress: ip, Successful: false, }) } func clearFailedAttempts(identifier, ip string, database *db.DB) { database.Model(&db.ImcLoginAttempt{}). Where("email = ? OR ip_address = ?", identifier, ip). Update("successful", true) } func (h *AuthHandler) cleanupOldAttempts() { cutoff := time.Now().Add(-24 * time.Hour) h.db.Where("attempted_at < ? AND successful = false", cutoff).Delete(&db.ImcLoginAttempt{}) } func (h *AuthHandler) getClientIP(c *gin.Context) string { remoteIP := c.ClientIP() if len(h.trustedProxies) > 0 { for _, proxy := range h.trustedProxies { if remoteIP == proxy { forwarded := c.GetHeader("X-Forwarded-For") if forwarded != "" { return strings.Split(forwarded, ",")[0] } } } } return remoteIP }