refactor(auth): split IAM module and add access/refresh session flow
This commit is contained in:
192
internal/iam/http_handler.go
Normal file
192
internal/iam/http_handler.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
cfg Config
|
||||
}
|
||||
|
||||
func NewHandler(service *Service, cfg Config) *Handler {
|
||||
return &Handler{service: service, cfg: cfg}
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterRoutes(router *gin.Engine) {
|
||||
auth := router.Group("/api/v1/auth")
|
||||
auth.POST("/register", h.register)
|
||||
auth.POST("/login", h.login)
|
||||
auth.POST("/refresh", h.refresh)
|
||||
|
||||
protected := auth.Group("")
|
||||
protected.Use(h.service.RequireAccess())
|
||||
protected.POST("/logout", h.logout)
|
||||
protected.POST("/logout-all", h.logoutAll)
|
||||
protected.GET("/sessions", h.listSessions)
|
||||
protected.DELETE("/sessions/:sid", h.revokeSession)
|
||||
}
|
||||
|
||||
func (h *Handler) register(c *gin.Context) {
|
||||
var input struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
AutoLogin *bool `json:"auto_login"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
autoLogin := true
|
||||
if input.AutoLogin != nil {
|
||||
autoLogin = *input.AutoLogin
|
||||
}
|
||||
result, refreshToken, err := h.service.Register(c.Request.Context(), input.Email, input.Password, autoLogin, requestMetaFromContext(c))
|
||||
if err != nil {
|
||||
h.writeAuthError(c, err, "registration failed")
|
||||
return
|
||||
}
|
||||
if !autoLogin {
|
||||
c.JSON(http.StatusCreated, gin.H{"email": strings.TrimSpace(strings.ToLower(input.Email))})
|
||||
return
|
||||
}
|
||||
h.setRefreshCookie(c, refreshToken)
|
||||
c.JSON(http.StatusCreated, result)
|
||||
}
|
||||
|
||||
func (h *Handler) login(c *gin.Context) {
|
||||
var input struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
DeviceInfo string `json:"device_info"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
meta := requestMetaFromContext(c)
|
||||
meta.DeviceInfo = input.DeviceInfo
|
||||
result, refreshToken, err := h.service.Login(c.Request.Context(), input.Email, input.Password, meta)
|
||||
if err != nil {
|
||||
h.writeAuthError(c, err, "login failed")
|
||||
return
|
||||
}
|
||||
h.setRefreshCookie(c, refreshToken)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *Handler) refresh(c *gin.Context) {
|
||||
refreshToken, err := c.Cookie(h.cfg.RefreshCookieName)
|
||||
if err != nil || strings.TrimSpace(refreshToken) == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing refresh token"})
|
||||
return
|
||||
}
|
||||
result, newRefreshToken, err := h.service.Refresh(c.Request.Context(), refreshToken)
|
||||
if err != nil {
|
||||
h.writeAuthError(c, err, "refresh failed")
|
||||
return
|
||||
}
|
||||
h.setRefreshCookie(c, newRefreshToken)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *Handler) logout(c *gin.Context) {
|
||||
token := extractBearerToken(c.GetHeader("Authorization"))
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization"})
|
||||
return
|
||||
}
|
||||
if err := h.service.Logout(c.Request.Context(), token); err != nil {
|
||||
h.writeAuthError(c, err, "logout failed")
|
||||
return
|
||||
}
|
||||
h.clearRefreshCookie(c)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) logoutAll(c *gin.Context) {
|
||||
token := extractBearerToken(c.GetHeader("Authorization"))
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization"})
|
||||
return
|
||||
}
|
||||
if err := h.service.LogoutAll(c.Request.Context(), token); err != nil {
|
||||
h.writeAuthError(c, err, "logout-all failed")
|
||||
return
|
||||
}
|
||||
h.clearRefreshCookie(c)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) listSessions(c *gin.Context) {
|
||||
uid := c.GetInt64(ContextUserIDKey)
|
||||
sessions, err := h.service.ListSessions(c.Request.Context(), uid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list sessions"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, sessions)
|
||||
}
|
||||
|
||||
func (h *Handler) revokeSession(c *gin.Context) {
|
||||
uid := c.GetInt64(ContextUserIDKey)
|
||||
sid := strings.TrimSpace(c.Param("sid"))
|
||||
if sid == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid sid"})
|
||||
return
|
||||
}
|
||||
if err := h.service.RevokeSession(c.Request.Context(), uid, sid); err != nil {
|
||||
h.writeAuthError(c, err, "revoke session failed")
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func requestMetaFromContext(c *gin.Context) requestMeta {
|
||||
return requestMeta{
|
||||
IP: c.ClientIP(),
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) writeAuthError(c *gin.Context, err error, fallback string) {
|
||||
switch {
|
||||
case errors.Is(err, errInvalidCredentials):
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
||||
case errors.Is(err, errAlreadyExists):
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "user already exists"})
|
||||
case errors.Is(err, errInvalidToken), errors.Is(err, errTokenExpired), errors.Is(err, errUnauthorized), errors.Is(err, errSessionRevoked):
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||||
case errors.Is(err, errForbidden):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fallback})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) setRefreshCookie(c *gin.Context, token string) {
|
||||
h.applySameSite(c)
|
||||
maxAge := int(h.cfg.RefreshTTL / time.Second)
|
||||
c.SetCookie(h.cfg.RefreshCookieName, token, maxAge, h.cfg.CookiePath, h.cfg.CookieDomain, h.cfg.CookieSecure, true)
|
||||
}
|
||||
|
||||
func (h *Handler) clearRefreshCookie(c *gin.Context) {
|
||||
h.applySameSite(c)
|
||||
c.SetCookie(h.cfg.RefreshCookieName, "", -1, h.cfg.CookiePath, h.cfg.CookieDomain, h.cfg.CookieSecure, true)
|
||||
}
|
||||
|
||||
func (h *Handler) applySameSite(c *gin.Context) {
|
||||
switch strings.ToLower(strings.TrimSpace(h.cfg.CookieSameSite)) {
|
||||
case "strict":
|
||||
c.SetSameSite(http.SameSiteStrictMode)
|
||||
case "none":
|
||||
c.SetSameSite(http.SameSiteNoneMode)
|
||||
default:
|
||||
c.SetSameSite(http.SameSiteLaxMode)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user