e045e00fbf
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>
113 lines
2.8 KiB
Go
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)
|
|
}
|