feat(admin): add super admin initialization endpoint
Add POST /api/v1/admin/setup-super-admin endpoint to initialize the first super admin. The endpoint can only be used once - after a super admin exists, subsequent requests are rejected. Requires valid setup_secret configured via APP_SETUP_SECRET environment variable or setup_secret in config file.
This commit is contained in:
@@ -33,6 +33,7 @@ type Config struct {
|
||||
WebRTC WebRTCConfig `mapstructure:"webrtc"`
|
||||
Report ReportConfig `mapstructure:"report"`
|
||||
Sensitive SensitiveConfig `mapstructure:"sensitive"`
|
||||
SetupSecret string `mapstructure:"setup_secret"`
|
||||
}
|
||||
|
||||
// Load 加载配置文件
|
||||
@@ -170,6 +171,7 @@ func Load(configPath string) (*Config, error) {
|
||||
})
|
||||
// Report 默认值
|
||||
viper.SetDefault("report.auto_hide_threshold", 3)
|
||||
viper.SetDefault("setup_secret", "")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config: %w", err)
|
||||
@@ -269,6 +271,8 @@ func Load(configPath string) (*Config, error) {
|
||||
cfg.Sensitive.LoadFromRedis, _ = strconv.ParseBool(getEnvOrDefault("APP_SENSITIVE_LOAD_FROM_REDIS", fmt.Sprintf("%t", cfg.Sensitive.LoadFromRedis)))
|
||||
cfg.Sensitive.RedisKeyPrefix = getEnvOrDefault("APP_SENSITIVE_REDIS_KEY_PREFIX", cfg.Sensitive.RedisKeyPrefix)
|
||||
|
||||
cfg.SetupSecret = getEnvOrDefault("APP_SETUP_SECRET", cfg.SetupSecret)
|
||||
|
||||
// 安全检查:生产模式必须设置JWT密钥
|
||||
if cfg.Server.Mode != "debug" {
|
||||
if cfg.JWT.Secret == "" || cfg.JWT.Secret == "your-jwt-secret-key-change-in-production" {
|
||||
|
||||
@@ -135,6 +135,11 @@ var (
|
||||
ErrAlreadyVerified = &AppError{Code: "ALREADY_VERIFIED", Message: "已完成身份认证,无需重复申请"}
|
||||
ErrVerificationAlreadyHandled = &AppError{Code: "VERIFICATION_ALREADY_HANDLED", Message: "该认证申请已被处理"}
|
||||
ErrVerificationRequired = &AppError{Code: "VERIFICATION_REQUIRED", Message: "需要完成身份认证"}
|
||||
|
||||
// 初始化相关错误
|
||||
ErrSetupAlreadyCompleted = &AppError{Code: "SETUP_ALREADY_COMPLETED", Message: "超级管理员已初始化,无法重复设置"}
|
||||
ErrInvalidSetupSecret = &AppError{Code: "INVALID_SETUP_SECRET", Message: "初始化密钥无效"}
|
||||
ErrSetupSecretNotConfigured = &AppError{Code: "SETUP_SECRET_NOT_CONFIGURED", Message: "未配置初始化密钥,无法使用此接口"}
|
||||
)
|
||||
|
||||
// Wrap 包装错误
|
||||
|
||||
38
internal/handler/setup_handler.go
Normal file
38
internal/handler/setup_handler.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"with_you/internal/pkg/response"
|
||||
"with_you/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SetupHandler struct {
|
||||
setupService service.SetupService
|
||||
}
|
||||
|
||||
func NewSetupHandler(setupService service.SetupService) *SetupHandler {
|
||||
return &SetupHandler{
|
||||
setupService: setupService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SetupHandler) SetupSuperAdmin(c *gin.Context) {
|
||||
type SetupSuperAdminRequest struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
SetupSecret string `json:"setup_secret" binding:"required"`
|
||||
}
|
||||
|
||||
var req SetupSuperAdminRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "user_id and setup_secret are required")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.setupService.SetupSuperAdmin(c.Request.Context(), req.UserID, req.SetupSecret); err != nil {
|
||||
response.HandleError(c, err, "failed to setup super admin")
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "超级管理员设置成功", nil)
|
||||
}
|
||||
@@ -161,7 +161,8 @@ func statusCodeFromAppErrorCode(code string) int {
|
||||
return http.StatusBadRequest
|
||||
case "FORBIDDEN", "NOT_GROUP_MEMBER", "NOT_GROUP_ADMIN", "NOT_GROUP_OWNER",
|
||||
"USER_BANNED", "USER_BLOCKED", "MUTED", "MUTE_ALL_ENABLED",
|
||||
"SCHEDULE_FORBIDDEN", "UNAUTHORIZED_NOTIFICATION":
|
||||
"SCHEDULE_FORBIDDEN", "UNAUTHORIZED_NOTIFICATION",
|
||||
"SETUP_ALREADY_COMPLETED":
|
||||
return http.StatusForbidden
|
||||
case "BAD_REQUEST", "INVALID_USERNAME", "INVALID_EMAIL", "INVALID_PHONE",
|
||||
"WEAK_PASSWORD", "USERNAME_EXISTS", "EMAIL_EXISTS", "PHONE_EXISTS",
|
||||
@@ -171,7 +172,8 @@ func statusCodeFromAppErrorCode(code string) int {
|
||||
"CANNOT_JOIN", "JOIN_REQUEST_PENDING", "GROUP_REQUEST_HANDLED",
|
||||
"NOT_REQUEST_TARGET", "NO_ELIGIBLE_INVITEE", "NOT_MUTUAL_FOLLOW",
|
||||
"INVITE_NOT_ACCEPTED", "STICKER_ALREADY_EXISTS", "INVALID_STICKER_URL",
|
||||
"INVALID_SCHEDULE_PAYLOAD", "SCHEDULE_COLOR_DUPLICATED":
|
||||
"INVALID_SCHEDULE_PAYLOAD", "SCHEDULE_COLOR_DUPLICATED",
|
||||
"INVALID_SETUP_SECRET", "SETUP_SECRET_NOT_CONFIGURED":
|
||||
return http.StatusBadRequest
|
||||
case "VERIFICATION_CODE_TOO_FREQUENT":
|
||||
return http.StatusTooManyRequests
|
||||
|
||||
@@ -42,6 +42,7 @@ type Router struct {
|
||||
verificationHandler *handler.VerificationHandler
|
||||
adminVerificationHandler *handler.AdminVerificationHandler
|
||||
adminProfileAuditHandler *handler.AdminProfileAuditHandler
|
||||
setupHandler *handler.SetupHandler
|
||||
wsHandler *handler.WSHandler
|
||||
logService *service.LogService
|
||||
jwtService *service.JWTService
|
||||
@@ -80,6 +81,7 @@ func New(
|
||||
verificationHandler *handler.VerificationHandler,
|
||||
adminVerificationHandler *handler.AdminVerificationHandler,
|
||||
adminProfileAuditHandler *handler.AdminProfileAuditHandler,
|
||||
setupHandler *handler.SetupHandler,
|
||||
logService *service.LogService,
|
||||
activityService service.UserActivityService,
|
||||
casbinService service.CasbinService,
|
||||
@@ -121,6 +123,7 @@ func New(
|
||||
verificationHandler: verificationHandler,
|
||||
adminVerificationHandler: adminVerificationHandler,
|
||||
adminProfileAuditHandler: adminProfileAuditHandler,
|
||||
setupHandler: setupHandler,
|
||||
logService: logService,
|
||||
jwtService: jwtService,
|
||||
casbinService: casbinService,
|
||||
@@ -157,6 +160,11 @@ func (r *Router) setupRoutes() {
|
||||
auth.POST("/refresh", r.userHandler.RefreshToken)
|
||||
}
|
||||
|
||||
// 初始化超级管理员(公开接口,只能使用一次)
|
||||
if r.setupHandler != nil {
|
||||
v1.POST("/admin/setup-super-admin", r.setupHandler.SetupSuperAdmin)
|
||||
}
|
||||
|
||||
// 需要认证的路由
|
||||
authMiddleware := middleware.Auth(r.jwtService)
|
||||
|
||||
|
||||
63
internal/service/setup_service.go
Normal file
63
internal/service/setup_service.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apperrors "with_you/internal/errors"
|
||||
"with_you/internal/model"
|
||||
"with_you/internal/repository"
|
||||
)
|
||||
|
||||
type SetupService interface {
|
||||
SetupSuperAdmin(ctx context.Context, userID, setupSecret string) error
|
||||
}
|
||||
|
||||
type setupService struct {
|
||||
userRepo repository.UserRepository
|
||||
roleRepo repository.RoleRepository
|
||||
casbinSvc CasbinService
|
||||
setupSecret string
|
||||
}
|
||||
|
||||
func NewSetupService(
|
||||
userRepo repository.UserRepository,
|
||||
roleRepo repository.RoleRepository,
|
||||
casbinSvc CasbinService,
|
||||
setupSecret string,
|
||||
) SetupService {
|
||||
return &setupService{
|
||||
userRepo: userRepo,
|
||||
roleRepo: roleRepo,
|
||||
casbinSvc: casbinSvc,
|
||||
setupSecret: setupSecret,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *setupService) SetupSuperAdmin(ctx context.Context, userID, setupSecret string) error {
|
||||
if s.setupSecret == "" {
|
||||
return apperrors.ErrSetupSecretNotConfigured
|
||||
}
|
||||
|
||||
if setupSecret != s.setupSecret {
|
||||
return apperrors.ErrInvalidSetupSecret
|
||||
}
|
||||
|
||||
count, err := s.roleRepo.GetRoleUserCount(ctx, model.RoleSuperAdmin)
|
||||
if err != nil {
|
||||
return apperrors.ErrInternal
|
||||
}
|
||||
if count > 0 {
|
||||
return apperrors.ErrSetupAlreadyCompleted
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
return apperrors.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.Status == model.UserStatusBanned {
|
||||
return apperrors.ErrUserBanned
|
||||
}
|
||||
|
||||
return s.casbinSvc.AddRoleForUser(ctx, user.ID, model.RoleSuperAdmin)
|
||||
}
|
||||
@@ -43,6 +43,7 @@ var HandlerSet = wire.NewSet(
|
||||
ProvideScheduleHandler,
|
||||
ProvideWSHandler,
|
||||
ProvideAdminVerificationHandler,
|
||||
ProvideSetupHandler,
|
||||
)
|
||||
|
||||
func ProvideUserHandler(
|
||||
@@ -108,3 +109,9 @@ func ProvideAdminVerificationHandler(
|
||||
) *handler.AdminVerificationHandler {
|
||||
return handler.NewAdminVerificationHandler(adminVerificationService, userRepo)
|
||||
}
|
||||
|
||||
func ProvideSetupHandler(
|
||||
setupService service.SetupService,
|
||||
) *handler.SetupHandler {
|
||||
return handler.NewSetupHandler(setupService)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ var ServiceSet = wire.NewSet(
|
||||
ProvideAdminVerificationService,
|
||||
ProvideSensitiveService,
|
||||
ProvideUserProfileAuditService,
|
||||
ProvideSetupService,
|
||||
|
||||
// 日志服务
|
||||
ProvideAsyncLogManager,
|
||||
@@ -476,3 +477,12 @@ func ProvideUserProfileAuditService(
|
||||
) service.UserProfileAuditService {
|
||||
return service.NewUserProfileAuditService(auditRepo, userRepo, hookManager, moderationHooks)
|
||||
}
|
||||
|
||||
func ProvideSetupService(
|
||||
userRepo repository.UserRepository,
|
||||
roleRepo repository.RoleRepository,
|
||||
casbinSvc service.CasbinService,
|
||||
cfg *config.Config,
|
||||
) service.SetupService {
|
||||
return service.NewSetupService(userRepo, roleRepo, casbinSvc, cfg.SetupSecret)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user