feat(admin): add super admin initialization endpoint
All checks were successful
Build Backend / build (push) Successful in 2m42s
Build Backend / build-docker (push) Successful in 2m52s

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:
lafay
2026-04-24 15:14:44 +08:00
parent 05c7f8a5eb
commit 813e54e5b2
12 changed files with 153 additions and 3 deletions

View File

@@ -110,6 +110,7 @@ jobs:
-e APP_GRPC_ENABLED=true \
-e APP_GRPC_PORT=50051 \
-e APP_SERVER_MODE=release \
-e "APP_SETUP_SECRET=${{ secrets.SETUP_SECRET }}" \
${IMAGE}
echo "=== Waiting for startup ==="

View File

@@ -55,6 +55,7 @@ func ProvideRouter(
verificationHandler *handler.VerificationHandler,
adminVerificationHandler *handler.AdminVerificationHandler,
adminProfileAuditHandler *handler.AdminProfileAuditHandler,
setupHandler *handler.SetupHandler,
logService *service.LogService,
activityService service.UserActivityService,
casbinService service.CasbinService,
@@ -91,6 +92,7 @@ func ProvideRouter(
verificationHandler,
adminVerificationHandler,
adminProfileAuditHandler,
setupHandler,
logService,
activityService,
casbinService,

View File

@@ -138,8 +138,10 @@ func InitializeApp() (*App, error) {
adminVerificationService := wire.ProvideAdminVerificationService(verificationRepository, userRepository)
adminVerificationHandler := wire.ProvideAdminVerificationHandler(adminVerificationService, userRepository)
adminProfileAuditHandler := handler.NewAdminProfileAuditHandler(userProfileAuditService)
setupService := wire.ProvideSetupService(userRepository, roleRepository, casbinService, config)
setupHandler := wire.ProvideSetupHandler(setupService)
wsHandler := wire.ProvideWSHandler(hub, chatService, groupService, jwtService, callService, userRepository)
router := ProvideRouter(userRepository, userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, voteHandler, channelHandler, scheduleHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, qrCodeHandler, materialHandler, callHandler, reportHandler, adminReportHandler, verificationHandler, adminVerificationHandler, adminProfileAuditHandler, logService, userActivityService, casbinService, wsHandler)
router := ProvideRouter(userRepository, userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, voteHandler, channelHandler, scheduleHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, qrCodeHandler, materialHandler, callHandler, reportHandler, adminReportHandler, verificationHandler, adminVerificationHandler, adminProfileAuditHandler, setupHandler, logService, userActivityService, casbinService, wsHandler)
hotRankWorker := wire.ProvideHotRankWorker(config, postRepository, cache)
app := NewApp(config, db, router, pushService, hotRankWorker, server, logger)
return app, nil
@@ -179,6 +181,7 @@ func ProvideRouter(
verificationHandler *handler.VerificationHandler,
adminVerificationHandler *handler.AdminVerificationHandler,
adminProfileAuditHandler *handler.AdminProfileAuditHandler,
setupHandler *handler.SetupHandler,
logService *service.LogService,
activityService service.UserActivityService,
casbinService service.CasbinService,
@@ -215,6 +218,7 @@ func ProvideRouter(
verificationHandler,
adminVerificationHandler,
adminProfileAuditHandler,
setupHandler,
logService,
activityService,
casbinService,

View File

@@ -262,3 +262,9 @@ casbin:
# 环境变量: APP_REPORT_AUTO_HIDE_THRESHOLD
report:
auto_hide_threshold: 3 # 自动隐藏阈值,举报次数达到此数值时自动隐藏内容
# 初始化超级管理员密钥
# 环境变量: APP_SETUP_SECRET
# 设置后可使用 POST /api/v1/admin/setup-super-admin 接口初始化第一位超级管理员
# 该接口只能使用一次,一旦系统中存在超级管理员,此接口将永久拒绝请求
setup_secret: ""

View File

@@ -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" {

View File

@@ -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 包装错误

View 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)
}

View File

@@ -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

View File

@@ -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)

View 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)
}

View File

@@ -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)
}

View File

@@ -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)
}