第 5 阶段:纠纷、通知与后台-2
This commit is contained in:
+1
-1
@@ -7,6 +7,7 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/redis/go-redis/v9 v9.17.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
gorm.io/datatypes v1.2.7
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
@@ -44,7 +45,6 @@ require (
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ContextUserID = "user_id"
|
||||
ContextPhone = "phone"
|
||||
ContextUserID = "user_id"
|
||||
ContextPhone = "phone"
|
||||
ContextAdminID = "admin_id"
|
||||
ContextUsername = "username"
|
||||
)
|
||||
|
||||
func Auth(jwtManager *auth.JWTManager) gin.HandlerFunc {
|
||||
@@ -24,7 +26,7 @@ func Auth(jwtManager *auth.JWTManager) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := jwtManager.Parse(tokenText, "access")
|
||||
claims, err := jwtManager.ParseSubject(tokenText, "access", "user")
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "访问令牌无效或已过期")
|
||||
c.Abort()
|
||||
@@ -36,3 +38,26 @@ func Auth(jwtManager *auth.JWTManager) gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func AdminAuth(jwtManager *auth.JWTManager) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
header := c.GetHeader("Authorization")
|
||||
tokenText := strings.TrimSpace(strings.TrimPrefix(header, "Bearer "))
|
||||
if tokenText == "" || tokenText == header {
|
||||
response.Unauthorized(c, "缺少后台访问令牌")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := jwtManager.ParseSubject(tokenText, "access", "admin")
|
||||
if err != nil {
|
||||
response.Unauthorized(c, "后台访问令牌无效或已过期")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(ContextAdminID, claims.UserID)
|
||||
c.Set(ContextUsername, claims.Phone)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type AdminUser struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"size:64;not null;uniqueIndex" json:"username"`
|
||||
PasswordHash string `gorm:"size:255;not null" json:"-"`
|
||||
Nickname string `gorm:"size:64;not null;default:''" json:"nickname"`
|
||||
Status string `gorm:"size:32;not null;default:'active'" json:"status"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (AdminUser) TableName() string {
|
||||
return "admin_users"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package adminauth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/modules/auth"
|
||||
)
|
||||
|
||||
type AdminDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status string `json:"status"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type LoginResult struct {
|
||||
Admin AdminDTO `json:"admin"`
|
||||
Tokens auth.TokenPair `json:"tokens"`
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package adminauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"hfb_sys/backend/internal/middleware"
|
||||
"hfb_sys/backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
}
|
||||
|
||||
func NewHandler(service *Service) *Handler {
|
||||
return &Handler{service: service}
|
||||
}
|
||||
|
||||
func (h *Handler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "用户名和密码不能为空")
|
||||
return
|
||||
}
|
||||
result, err := h.service.Login(strings.TrimSpace(req.Username), req.Password)
|
||||
if err != nil {
|
||||
writeAdminAuthError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *Handler) Me(c *gin.Context) {
|
||||
value, ok := c.Get(middleware.ContextAdminID)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
adminID, ok := value.(uint64)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "管理员上下文无效")
|
||||
return
|
||||
}
|
||||
admin, err := h.service.Me(adminID)
|
||||
if err != nil {
|
||||
writeAdminAuthError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, admin)
|
||||
}
|
||||
|
||||
func (h *Handler) Logout(c *gin.Context) {
|
||||
response.OK(c, gin.H{"logged_out": true})
|
||||
}
|
||||
|
||||
func writeAdminAuthError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, ErrDependencyUnavailable):
|
||||
response.ServiceUnavailable(c, "数据库未连接")
|
||||
case errors.Is(err, ErrInvalidCredential):
|
||||
response.BadRequest(c, "用户名或密码错误")
|
||||
case errors.Is(err, ErrAdminDisabled):
|
||||
response.Error(c, http.StatusForbidden, "admin_disabled", "管理员已禁用")
|
||||
default:
|
||||
response.Error(c, http.StatusInternalServerError, "internal_error", "后台认证服务暂时不可用")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package adminauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/auth"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAdminUsername = "admin"
|
||||
defaultAdminPassword = "admin123456"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
jwt *auth.JWTManager
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB, jwt *auth.JWTManager) *Repository {
|
||||
return &Repository{db: db, jwt: jwt}
|
||||
}
|
||||
|
||||
func (r *Repository) Login(username string, password string) (LoginResult, error) {
|
||||
if err := r.ensureDefaultAdmin(); err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
var admin model.AdminUser
|
||||
if err := r.db.Where("username = ?", username).First(&admin).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return LoginResult{}, ErrInvalidCredential
|
||||
}
|
||||
return LoginResult{}, err
|
||||
}
|
||||
if admin.Status != "active" {
|
||||
return LoginResult{}, ErrAdminDisabled
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(admin.PasswordHash), []byte(password)); err != nil {
|
||||
return LoginResult{}, ErrInvalidCredential
|
||||
}
|
||||
now := time.Now()
|
||||
admin.LastLoginAt = &now
|
||||
if err := r.db.Save(&admin).Error; err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
tokens, err := r.jwt.GenerateSubjectPair(admin.ID, admin.Username, "admin")
|
||||
if err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
return LoginResult{Admin: toDTO(admin), Tokens: tokens}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) FindByID(id uint64) (*AdminDTO, error) {
|
||||
var admin model.AdminUser
|
||||
if err := r.db.First(&admin, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if admin.Status != "active" {
|
||||
return nil, ErrAdminDisabled
|
||||
}
|
||||
dto := toDTO(admin)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureDefaultAdmin() error {
|
||||
var count int64
|
||||
if err := r.db.Model(&model.AdminUser{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(defaultAdminPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
admin := model.AdminUser{
|
||||
Username: defaultAdminUsername,
|
||||
PasswordHash: string(hash),
|
||||
Nickname: "超级管理员",
|
||||
Status: "active",
|
||||
}
|
||||
return r.db.Create(&admin).Error
|
||||
}
|
||||
|
||||
func toDTO(admin model.AdminUser) AdminDTO {
|
||||
return AdminDTO{
|
||||
ID: admin.ID,
|
||||
Username: admin.Username,
|
||||
Nickname: admin.Nickname,
|
||||
Status: admin.Status,
|
||||
LastLoginAt: admin.LastLoginAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package adminauth
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||
ErrInvalidCredential = errors.New("invalid credential")
|
||||
ErrAdminDisabled = errors.New("admin disabled")
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *Repository
|
||||
}
|
||||
|
||||
func NewService(repo *Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *Service) Login(username string, password string) (LoginResult, error) {
|
||||
if s.repo == nil {
|
||||
return LoginResult{}, ErrDependencyUnavailable
|
||||
}
|
||||
if username == "" || password == "" {
|
||||
return LoginResult{}, ErrInvalidCredential
|
||||
}
|
||||
return s.repo.Login(username, password)
|
||||
}
|
||||
|
||||
func (s *Service) Me(adminID uint64) (*AdminDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.FindByID(adminID)
|
||||
}
|
||||
@@ -22,9 +22,10 @@ type JWTManager struct {
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
UserID uint64 `json:"uid"`
|
||||
Phone string `json:"phone"`
|
||||
TokenType string `json:"typ"`
|
||||
UserID uint64 `json:"uid"`
|
||||
Phone string `json:"phone"`
|
||||
TokenType string `json:"typ"`
|
||||
SubjectType string `json:"sub_type"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -45,11 +46,15 @@ func NewJWTManager(secret string) *JWTManager {
|
||||
}
|
||||
|
||||
func (m *JWTManager) GeneratePair(userID uint64, phone string) (TokenPair, error) {
|
||||
accessToken, err := m.generate(userID, phone, tokenTypeAccess, m.accessTTL)
|
||||
return m.GenerateSubjectPair(userID, phone, "user")
|
||||
}
|
||||
|
||||
func (m *JWTManager) GenerateSubjectPair(userID uint64, subject string, subjectType string) (TokenPair, error) {
|
||||
accessToken, err := m.generate(userID, subject, subjectType, tokenTypeAccess, m.accessTTL)
|
||||
if err != nil {
|
||||
return TokenPair{}, err
|
||||
}
|
||||
refreshToken, err := m.generate(userID, phone, tokenTypeRefresh, m.refreshTTL)
|
||||
refreshToken, err := m.generate(userID, subject, subjectType, tokenTypeRefresh, m.refreshTTL)
|
||||
if err != nil {
|
||||
return TokenPair{}, err
|
||||
}
|
||||
@@ -62,6 +67,10 @@ func (m *JWTManager) GeneratePair(userID uint64, phone string) (TokenPair, error
|
||||
}
|
||||
|
||||
func (m *JWTManager) Parse(tokenText, expectedType string) (*Claims, error) {
|
||||
return m.ParseSubject(tokenText, expectedType, "")
|
||||
}
|
||||
|
||||
func (m *JWTManager) ParseSubject(tokenText, expectedType string, expectedSubjectType string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenText, claims, func(token *jwt.Token) (any, error) {
|
||||
if token.Method != m.signingMethod {
|
||||
@@ -75,17 +84,21 @@ func (m *JWTManager) Parse(tokenText, expectedType string) (*Claims, error) {
|
||||
if claims.TokenType != expectedType {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
if expectedSubjectType != "" && claims.SubjectType != expectedSubjectType {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (m *JWTManager) generate(userID uint64, phone string, tokenType string, ttl time.Duration) (string, error) {
|
||||
func (m *JWTManager) generate(userID uint64, subject string, subjectType string, tokenType string, ttl time.Duration) (string, error) {
|
||||
now := time.Now()
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Phone: phone,
|
||||
TokenType: tokenType,
|
||||
UserID: userID,
|
||||
Phone: subject,
|
||||
TokenType: tokenType,
|
||||
SubjectType: subjectType,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: phone,
|
||||
Subject: subject,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
|
||||
},
|
||||
|
||||
@@ -109,7 +109,7 @@ func (s *Service) LoginWithSMS(ctx context.Context, phone string, code string) (
|
||||
}
|
||||
|
||||
func (s *Service) RefreshToken(refreshToken string) (TokenPair, error) {
|
||||
claims, err := s.jwt.Parse(refreshToken, tokenTypeRefresh)
|
||||
claims, err := s.jwt.ParseSubject(refreshToken, tokenTypeRefresh, "user")
|
||||
if err != nil {
|
||||
return TokenPair{}, err
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ func (h *Handler) AdminList(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Handler) AdminArbitrate(c *gin.Context) {
|
||||
adminID, ok := currentUserID(c)
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
@@ -106,6 +106,15 @@ func (h *Handler) AdminArbitrate(c *gin.Context) {
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func currentAdminID(c *gin.Context) (uint64, bool) {
|
||||
value, ok := c.Get(middleware.ContextAdminID)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
adminID, ok := value.(uint64)
|
||||
return adminID, ok
|
||||
}
|
||||
|
||||
func currentUserID(c *gin.Context) (uint64, bool) {
|
||||
value, ok := c.Get(middleware.ContextUserID)
|
||||
if !ok {
|
||||
|
||||
@@ -28,9 +28,9 @@ func (h *Handler) List(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Handler) Update(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
key := c.Param("key")
|
||||
@@ -39,7 +39,7 @@ func (h *Handler) Update(c *gin.Context) {
|
||||
response.BadRequest(c, "配置值不能为空")
|
||||
return
|
||||
}
|
||||
item, err := h.service.Update(userID, key, req, AuditMeta{
|
||||
item, err := h.service.Update(adminID, key, req, AuditMeta{
|
||||
IP: c.ClientIP(),
|
||||
UserAgent: c.GetHeader("User-Agent"),
|
||||
})
|
||||
@@ -50,13 +50,13 @@ func (h *Handler) Update(c *gin.Context) {
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func currentUserID(c *gin.Context) (uint64, bool) {
|
||||
value, ok := c.Get(middleware.ContextUserID)
|
||||
func currentAdminID(c *gin.Context) (uint64, bool) {
|
||||
value, ok := c.Get(middleware.ContextAdminID)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
userID, ok := value.(uint64)
|
||||
return userID, ok
|
||||
adminID, ok := value.(uint64)
|
||||
return adminID, ok
|
||||
}
|
||||
|
||||
func writeConfigError(c *gin.Context, err error) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"hfb_sys/backend/internal/config"
|
||||
"hfb_sys/backend/internal/handler"
|
||||
"hfb_sys/backend/internal/middleware"
|
||||
"hfb_sys/backend/internal/modules/adminauth"
|
||||
"hfb_sys/backend/internal/modules/auth"
|
||||
"hfb_sys/backend/internal/modules/dispute"
|
||||
"hfb_sys/backend/internal/modules/listing"
|
||||
@@ -37,6 +38,12 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
}
|
||||
authService := auth.NewService(userRepo, deps.Redis, jwtManager, logger)
|
||||
authHandler := auth.NewHandler(authService)
|
||||
var adminAuthRepo *adminauth.Repository
|
||||
if deps.DB != nil {
|
||||
adminAuthRepo = adminauth.NewRepository(deps.DB, jwtManager)
|
||||
}
|
||||
adminAuthService := adminauth.NewService(adminAuthRepo)
|
||||
adminAuthHandler := adminauth.NewHandler(adminAuthService)
|
||||
userHandler := user.NewHandler(userRepo)
|
||||
var realnameRepo *realname.Repository
|
||||
if deps.DB != nil {
|
||||
@@ -81,6 +88,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
systemConfigService := systemconfig.NewService(systemConfigRepo)
|
||||
systemConfigHandler := systemconfig.NewHandler(systemConfigService)
|
||||
requireAuth := middleware.Auth(jwtManager)
|
||||
requireAdmin := middleware.AdminAuth(jwtManager)
|
||||
requireRealname := middleware.RequireRealname(userRepo)
|
||||
|
||||
api := engine.Group("/api")
|
||||
@@ -151,8 +159,15 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
realnameRoutes.GET("/status", realnameHandler.Status)
|
||||
}
|
||||
|
||||
adminRoutes := api.Group("/admin", requireAuth)
|
||||
adminAuthRoutes := api.Group("/admin/auth")
|
||||
{
|
||||
adminAuthRoutes.POST("/login", adminAuthHandler.Login)
|
||||
}
|
||||
|
||||
adminRoutes := api.Group("/admin", requireAdmin)
|
||||
{
|
||||
adminRoutes.GET("/me", adminAuthHandler.Me)
|
||||
adminRoutes.POST("/auth/logout", adminAuthHandler.Logout)
|
||||
adminRoutes.GET("/disputes", disputeHandler.AdminList)
|
||||
adminRoutes.POST("/disputes/:id/arbitrate", disputeHandler.AdminArbitrate)
|
||||
adminRoutes.GET("/system-configs", systemConfigHandler.List)
|
||||
|
||||
Reference in New Issue
Block a user