diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 495a575..95a36a9 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -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 ===" diff --git a/cmd/server/wire.go b/cmd/server/wire.go index c77b4b1..a236d68 100644 --- a/cmd/server/wire.go +++ b/cmd/server/wire.go @@ -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, diff --git a/cmd/server/wire_gen.go b/cmd/server/wire_gen.go index 34119c2..60ea44f 100644 --- a/cmd/server/wire_gen.go +++ b/cmd/server/wire_gen.go @@ -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, diff --git a/configs/config.yaml b/configs/config.yaml index 824ab5c..4fbd1db 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -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: "" diff --git a/internal/config/config.go b/internal/config/config.go index 256bce1..13e2d40 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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" { diff --git a/internal/errors/app_errors.go b/internal/errors/app_errors.go index 48694f7..626e37e 100644 --- a/internal/errors/app_errors.go +++ b/internal/errors/app_errors.go @@ -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 包装错误 diff --git a/internal/handler/setup_handler.go b/internal/handler/setup_handler.go new file mode 100644 index 0000000..270d552 --- /dev/null +++ b/internal/handler/setup_handler.go @@ -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) +} diff --git a/internal/pkg/response/response.go b/internal/pkg/response/response.go index cdafcbd..c677bf4 100644 --- a/internal/pkg/response/response.go +++ b/internal/pkg/response/response.go @@ -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 diff --git a/internal/router/router.go b/internal/router/router.go index 9110cfe..358be75 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -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) diff --git a/internal/service/setup_service.go b/internal/service/setup_service.go new file mode 100644 index 0000000..b12d74f --- /dev/null +++ b/internal/service/setup_service.go @@ -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) +} diff --git a/internal/wire/handler.go b/internal/wire/handler.go index 2b088f7..a20434e 100644 --- a/internal/wire/handler.go +++ b/internal/wire/handler.go @@ -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) +} diff --git a/internal/wire/service.go b/internal/wire/service.go index dd16706..2732e4c 100644 --- a/internal/wire/service.go +++ b/internal/wire/service.go @@ -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) +}