package auth import ( "errors" "net/http" "github.com/gin-gonic/gin" ) // API provides HTTP handlers for authentication endpoints. type API struct { auth *Service middleware *Middleware } // NewAPI creates a new auth API handler. func NewAPI(auth *Service) *API { return &API{ auth: auth, middleware: NewMiddleware(auth), } } // RegisterRoutes registers auth routes on a router group. func (a *API) RegisterRoutes(group *gin.RouterGroup) { // Public routes group.POST("/login", a.handleLogin) group.POST("/logout", a.handleLogout) if a.auth.config.AllowRegistration { group.POST("/register", a.handleRegister) } // Authenticated routes authenticated := group.Group("") authenticated.Use(a.middleware.Required()) { authenticated.GET("/me", a.handleGetCurrentUser) authenticated.PUT("/me", a.handleUpdateCurrentUser) authenticated.PUT("/me/password", a.handleChangePassword) authenticated.POST("/logout/all", a.handleLogoutAll) } } // Middleware returns the auth middleware for use in other routes. func (a *API) Middleware() *Middleware { return a.middleware } // LoginRequest represents a login request. type LoginRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } // LoginResponse represents a successful login response. type LoginResponse struct { Token string `json:"token"` ExpiresAt string `json:"expiresAt"` User UserResponse `json:"user"` } // UserResponse represents user info in API responses. type UserResponse struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email,omitempty"` AuthProvider string `json:"authProvider"` Roles []string `json:"roles,omitempty"` CreatedAt string `json:"createdAt"` LastLogin string `json:"lastLogin,omitempty"` } func (a *API) handleLogin(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } session, user, err := a.auth.Login( c.Request.Context(), req.Username, req.Password, c.ClientIP(), c.GetHeader("User-Agent"), ) if err != nil { status := http.StatusUnauthorized message := "invalid credentials" if errors.Is(err, ErrUserDisabled) { message = "account is disabled" } c.JSON(status, gin.H{"error": message}) return } // Set session cookie a.middleware.SetSessionCookie(c, session) // Get user roles roles, _ := a.auth.db.GetUserRoles(c.Request.Context(), user.ID) roleNames := make([]string, len(roles)) for i, r := range roles { roleNames[i] = r.Name } c.JSON(http.StatusOK, LoginResponse{ Token: session.Token, ExpiresAt: session.ExpiresAt.Format("2006-01-02T15:04:05Z"), User: UserResponse{ ID: user.ID, Username: user.Username, Email: user.Email, AuthProvider: string(user.AuthProvider), Roles: roleNames, CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z"), LastLogin: user.LastLogin.Format("2006-01-02T15:04:05Z"), }, }) } func (a *API) handleLogout(c *gin.Context) { // Try to get token from request token := "" // Check Authorization header if auth := c.GetHeader("Authorization"); len(auth) > 7 && auth[:7] == "Bearer " { token = auth[7:] } // Check cookie if token == "" { if cookie, err := c.Cookie(a.auth.config.SessionCookieName); err == nil { token = cookie } } if token != "" { a.auth.Logout(c.Request.Context(), token) } // Clear session cookie a.middleware.ClearSessionCookie(c) c.JSON(http.StatusOK, gin.H{"message": "logged out"}) } func (a *API) handleLogoutAll(c *gin.Context) { user := MustGetUser(c) if err := a.auth.LogoutAll(c.Request.Context(), user.ID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to logout"}) return } // Clear session cookie a.middleware.ClearSessionCookie(c) c.JSON(http.StatusOK, gin.H{"message": "all sessions logged out"}) } // RegisterRequest represents a registration request. type RegisterRequest struct { Username string `json:"username" binding:"required,min=3,max=32"` Password string `json:"password" binding:"required,min=8"` Email string `json:"email" binding:"omitempty,email"` } func (a *API) handleRegister(c *gin.Context) { var req RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } user, err := a.auth.Register(c.Request.Context(), req.Username, req.Password, req.Email) if err != nil { status := http.StatusInternalServerError message := "registration failed" if errors.Is(err, ErrUsernameExists) { status = http.StatusConflict message = "username already exists" } c.JSON(status, gin.H{"error": message}) return } c.JSON(http.StatusCreated, UserResponse{ ID: user.ID, Username: user.Username, Email: user.Email, AuthProvider: string(user.AuthProvider), CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z"), }) } func (a *API) handleGetCurrentUser(c *gin.Context) { user := MustGetUser(c) // Get user roles roles, _ := a.auth.db.GetUserRoles(c.Request.Context(), user.ID) roleNames := make([]string, len(roles)) for i, r := range roles { roleNames[i] = r.Name } lastLogin := "" if !user.LastLogin.IsZero() { lastLogin = user.LastLogin.Format("2006-01-02T15:04:05Z") } c.JSON(http.StatusOK, UserResponse{ ID: user.ID, Username: user.Username, Email: user.Email, AuthProvider: string(user.AuthProvider), Roles: roleNames, CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z"), LastLogin: lastLogin, }) } // UpdateUserRequest represents a profile update request. type UpdateUserRequest struct { Email string `json:"email" binding:"omitempty,email"` } func (a *API) handleUpdateCurrentUser(c *gin.Context) { user := MustGetUser(c) var req UpdateUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } user.Email = req.Email if err := a.auth.UpdateUser(c.Request.Context(), user); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update profile"}) return } c.JSON(http.StatusOK, gin.H{"message": "profile updated"}) } // ChangePasswordRequest represents a password change request. type ChangePasswordRequest struct { CurrentPassword string `json:"currentPassword" binding:"required"` NewPassword string `json:"newPassword" binding:"required,min=8"` } func (a *API) handleChangePassword(c *gin.Context) { user := MustGetUser(c) var req ChangePasswordRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } err := a.auth.ChangePassword(c.Request.Context(), user.ID, req.CurrentPassword, req.NewPassword) if err != nil { status := http.StatusInternalServerError message := "failed to change password" if errors.Is(err, ErrInvalidCredentials) { status = http.StatusUnauthorized message = "current password is incorrect" } c.JSON(status, gin.H{"error": message}) return } c.JSON(http.StatusOK, gin.H{"message": "password changed"}) }