package adminauth import ( "context" "errors" "strings" "unicode" "hfb_sys/backend/internal/model" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) type BootstrapConfig struct { AppEnv string Username string Password string Nickname string } // BootstrapAdmin 在管理员表为空时按部署配置创建首个超级管理员。 func BootstrapAdmin(ctx context.Context, db *gorm.DB, cfg BootstrapConfig) error { if db == nil { return nil } var count int64 if err := db.WithContext(ctx).Model(&model.AdminUser{}).Count(&count).Error; err != nil { if isAdminTableMissing(err) { return nil } return err } if count > 0 { return nil } username := strings.TrimSpace(cfg.Username) password := cfg.Password if username == "" || password == "" { if strings.EqualFold(strings.TrimSpace(cfg.AppEnv), "production") { return errors.New("admin bootstrap credentials are required when no admin exists in production") } return nil } if strings.EqualFold(strings.TrimSpace(cfg.AppEnv), "production") && !bootstrapPasswordStrong(password) { return errors.New("ADMIN_BOOTSTRAP_PASSWORD must be at least 12 chars and include letters and digits in production") } nickname := strings.TrimSpace(cfg.Nickname) if nickname == "" { nickname = username } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err } return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { admin := model.AdminUser{ Username: username, PasswordHash: string(hash), Nickname: nickname, Status: "active", TokenVersion: 1, PasswordMustChange: true, } if err := tx.Create(&admin).Error; err != nil { return err } var superAdminRole model.Role if err := tx.Where("code = ?", "super_admin").First(&superAdminRole).Error; err != nil { return err } return tx.Create(&model.AdminUserRole{ AdminID: admin.ID, RoleID: superAdminRole.ID, }).Error }) } func bootstrapPasswordStrong(value string) bool { if len([]rune(value)) < 12 { return false } hasLetter := false hasDigit := false for _, r := range value { if unicode.IsLetter(r) { hasLetter = true } if unicode.IsDigit(r) { hasDigit = true } } return hasLetter && hasDigit } func isAdminTableMissing(err error) bool { message := strings.ToLower(err.Error()) return strings.Contains(message, "admin_users") && (strings.Contains(message, "doesn't exist") || strings.Contains(message, "no such table")) }