domain/context.go: extend WithUser/UserFromContext with session ID handler/response.go: respondJSON/respondError with domain→HTTP status mapping (404/403/401/409/400/415/500) handler/middleware.go: Bearer JWT extraction, ParseAccessToken, domain.WithUser injection; aborts with 401 JSON on failure handler/auth_handler.go: Login, Refresh, Logout, ListSessions, TerminateSession handler/router.go: /health, /api/v1/auth routes; login and refresh are public, session routes protected by AuthMiddleware Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
141 lines
3.5 KiB
Go
141 lines
3.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"tanabata/backend/internal/domain"
|
|
"tanabata/backend/internal/service"
|
|
)
|
|
|
|
// AuthHandler handles all /auth endpoints.
|
|
type AuthHandler struct {
|
|
authSvc *service.AuthService
|
|
}
|
|
|
|
// NewAuthHandler creates an AuthHandler backed by authSvc.
|
|
func NewAuthHandler(authSvc *service.AuthService) *AuthHandler {
|
|
return &AuthHandler{authSvc: authSvc}
|
|
}
|
|
|
|
// Login handles POST /auth/login.
|
|
func (h *AuthHandler) Login(c *gin.Context) {
|
|
var req struct {
|
|
Name string `json:"name" binding:"required"`
|
|
Password string `json:"password" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
|
|
pair, err := h.authSvc.Login(c.Request.Context(), req.Name, req.Password, c.GetHeader("User-Agent"))
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
|
|
respondJSON(c, http.StatusOK, gin.H{
|
|
"access_token": pair.AccessToken,
|
|
"refresh_token": pair.RefreshToken,
|
|
"expires_in": pair.ExpiresIn,
|
|
})
|
|
}
|
|
|
|
// Refresh handles POST /auth/refresh.
|
|
func (h *AuthHandler) Refresh(c *gin.Context) {
|
|
var req struct {
|
|
RefreshToken string `json:"refresh_token" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
|
|
pair, err := h.authSvc.Refresh(c.Request.Context(), req.RefreshToken, c.GetHeader("User-Agent"))
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
|
|
respondJSON(c, http.StatusOK, gin.H{
|
|
"access_token": pair.AccessToken,
|
|
"refresh_token": pair.RefreshToken,
|
|
"expires_in": pair.ExpiresIn,
|
|
})
|
|
}
|
|
|
|
// Logout handles POST /auth/logout. Requires authentication.
|
|
func (h *AuthHandler) Logout(c *gin.Context) {
|
|
_, _, sessionID := domain.UserFromContext(c.Request.Context())
|
|
|
|
if err := h.authSvc.Logout(c.Request.Context(), sessionID); err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// ListSessions handles GET /auth/sessions. Requires authentication.
|
|
func (h *AuthHandler) ListSessions(c *gin.Context) {
|
|
userID, _, sessionID := domain.UserFromContext(c.Request.Context())
|
|
|
|
list, err := h.authSvc.ListSessions(c.Request.Context(), userID, sessionID)
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
|
|
type sessionItem struct {
|
|
ID int `json:"id"`
|
|
UserAgent string `json:"user_agent"`
|
|
StartedAt string `json:"started_at"`
|
|
ExpiresAt any `json:"expires_at"`
|
|
LastActivity string `json:"last_activity"`
|
|
IsCurrent bool `json:"is_current"`
|
|
}
|
|
|
|
items := make([]sessionItem, len(list.Items))
|
|
for i, s := range list.Items {
|
|
var expiresAt any
|
|
if s.ExpiresAt != nil {
|
|
expiresAt = s.ExpiresAt.Format("2006-01-02T15:04:05Z07:00")
|
|
}
|
|
items[i] = sessionItem{
|
|
ID: s.ID,
|
|
UserAgent: s.UserAgent,
|
|
StartedAt: s.StartedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
ExpiresAt: expiresAt,
|
|
LastActivity: s.LastActivity.Format("2006-01-02T15:04:05Z07:00"),
|
|
IsCurrent: s.IsCurrent,
|
|
}
|
|
}
|
|
|
|
respondJSON(c, http.StatusOK, gin.H{
|
|
"items": items,
|
|
"total": list.Total,
|
|
})
|
|
}
|
|
|
|
// TerminateSession handles DELETE /auth/sessions/:id. Requires authentication.
|
|
func (h *AuthHandler) TerminateSession(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
|
|
userID, isAdmin, _ := domain.UserFromContext(c.Request.Context())
|
|
|
|
if err := h.authSvc.TerminateSession(c.Request.Context(), userID, isAdmin, id); err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusNoContent)
|
|
}
|