Files
geo/server/internal/tenant/transport/auth_handler.go
T
root e045e00fbf
Frontend CI / Frontend (push) Successful in 3m8s
Backend CI / Backend (push) Successful in 14m48s
feat(auth,prompt-rule): add password change with session revocation and scope prompt rules by brand
Add self-service password change that re-hashes the credential and bumps a
per-user session version so all existing access/refresh tokens are rejected.
Session version is tracked in Redis with an in-memory fallback and enforced in
middleware and refresh.

Scope prompt rules and rule groups to a brand: new brand_id column (migrated to
a "未归属" fallback brand for existing rows), brand-filtered queries/indexes, and
brand-aware admin-web tabs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:09:53 +08:00

113 lines
2.8 KiB
Go

package transport
import (
"strings"
"github.com/gin-gonic/gin"
"github.com/geo-platform/tenant-api/internal/bootstrap"
"github.com/geo-platform/tenant-api/internal/shared/auth"
"github.com/geo-platform/tenant-api/internal/shared/response"
"github.com/geo-platform/tenant-api/internal/tenant/app"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
type AuthHandler struct {
svc *app.AuthService
}
func NewAuthHandler(a *bootstrap.App) *AuthHandler {
return &AuthHandler{
svc: app.NewAuthService(
repository.NewAuthRepository(a.DB),
repository.NewTenantPlanRepository(a.DB),
a.KolProfiles,
a.JWT,
a.Sessions,
a.LoginGuard,
).WithPasswordCipher(a.PasswordCipher),
}
}
func (h *AuthHandler) PasswordPublicKey(c *gin.Context) {
key := h.svc.PasswordPublicKey()
if key == nil {
response.Error(c, response.ErrNotFound(40420, "password_cipher_disabled", "password cipher is disabled"))
return
}
response.Success(c, key)
}
func (h *AuthHandler) Login(c *gin.Context) {
var req app.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
return
}
req.IP = c.ClientIP()
resp, err := h.svc.Login(c.Request.Context(), req)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, resp)
}
func (h *AuthHandler) Refresh(c *gin.Context) {
var req app.RefreshRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
return
}
resp, err := h.svc.Refresh(c.Request.Context(), req)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, resp)
}
func (h *AuthHandler) Me(c *gin.Context) {
actor, ok := auth.ActorFromCtx(c.Request.Context())
if !ok {
response.Error(c, response.ErrUnauthorized(40100, "not_authenticated", "not authenticated"))
return
}
resp, err := h.svc.Me(c.Request.Context(), actor)
if err != nil {
response.Error(c, err)
return
}
response.Success(c, resp)
}
func (h *AuthHandler) ChangePassword(c *gin.Context) {
actor, ok := auth.ActorFromCtx(c.Request.Context())
if !ok {
response.Error(c, response.ErrUnauthorized(40100, "not_authenticated", "not authenticated"))
return
}
var req app.ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
return
}
if err := h.svc.ChangePassword(c.Request.Context(), actor, req); err != nil {
response.Error(c, err)
return
}
response.Success(c, gin.H{"ok": true})
}
func (h *AuthHandler) Logout(c *gin.Context) {
header := c.GetHeader("Authorization")
parts := strings.SplitN(header, " ", 2)
token := ""
if len(parts) == 2 {
token = parts[1]
}
_ = h.svc.Logout(c.Request.Context(), token)
response.Success(c, nil)
}