feat(admin): complete settings APIs and password update flow
This commit is contained in:
@@ -38,9 +38,11 @@ func (s *Service) EnsureDefaultAdmin(ctx context.Context) error {
|
||||
}
|
||||
|
||||
record := &adminmodel.Admin{
|
||||
Username: username,
|
||||
Password: string(hashedPassword),
|
||||
Role: "super_admin",
|
||||
Username: username,
|
||||
Password: string(hashedPassword),
|
||||
Role: "super_admin",
|
||||
DisplayName: username,
|
||||
Timezone: "Asia/Shanghai",
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Create(record).Error; err != nil {
|
||||
if isDuplicateError(err) {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
adminmodel "wx_service/internal/admin/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AdminSettings struct {
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
type UpdateAdminProfileInput struct {
|
||||
DisplayName string
|
||||
Email string
|
||||
Phone string
|
||||
Timezone string
|
||||
}
|
||||
|
||||
type SystemConfigPayload struct {
|
||||
SiteName string `json:"site_name"`
|
||||
AllowRegister bool `json:"allow_register"`
|
||||
LoginFailLimit int `json:"login_fail_limit"`
|
||||
DefaultPageSize int `json:"default_page_size"`
|
||||
AuditLogRetentionDays int `json:"audit_log_retention_days"`
|
||||
}
|
||||
|
||||
func (s *Service) GetAdminSettings(ctx context.Context, adminID uint) (*AdminSettings, error) {
|
||||
if adminID == 0 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
var admin adminmodel.Admin
|
||||
if err := s.db.WithContext(ctx).
|
||||
Select("username", "display_name", "email", "phone", "timezone").
|
||||
Where("id = ?", adminID).
|
||||
First(&admin).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrAdminUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AdminSettings{
|
||||
Username: admin.Username,
|
||||
DisplayName: admin.DisplayName,
|
||||
Email: admin.Email,
|
||||
Phone: admin.Phone,
|
||||
Timezone: normalizeTimezone(admin.Timezone),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateAdminProfile(ctx context.Context, adminID uint, input UpdateAdminProfileInput) (*AdminSettings, error) {
|
||||
if adminID == 0 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
displayName := strings.TrimSpace(input.DisplayName)
|
||||
email := strings.TrimSpace(input.Email)
|
||||
phone := strings.TrimSpace(input.Phone)
|
||||
timezone := normalizeTimezone(input.Timezone)
|
||||
|
||||
if displayName == "" {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
if email != "" && !strings.Contains(email, "@") {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"display_name": displayName,
|
||||
"email": email,
|
||||
"phone": phone,
|
||||
"timezone": timezone,
|
||||
}
|
||||
|
||||
result := s.db.WithContext(ctx).
|
||||
Model(&adminmodel.Admin{}).
|
||||
Where("id = ?", adminID).
|
||||
Updates(updates)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil, ErrAdminUserNotFound
|
||||
}
|
||||
|
||||
return s.GetAdminSettings(ctx, adminID)
|
||||
}
|
||||
|
||||
func (s *Service) GetSystemConfig(ctx context.Context) (*SystemConfigPayload, error) {
|
||||
config, err := s.ensureSystemConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SystemConfigPayload{
|
||||
SiteName: config.SiteName,
|
||||
AllowRegister: config.AllowRegister,
|
||||
LoginFailLimit: config.LoginFailLimit,
|
||||
DefaultPageSize: config.DefaultPageSize,
|
||||
AuditLogRetentionDays: config.AuditLogRetentionDays,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateSystemConfig(ctx context.Context, input SystemConfigPayload) (*SystemConfigPayload, error) {
|
||||
siteName := strings.TrimSpace(input.SiteName)
|
||||
if siteName == "" {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
if input.LoginFailLimit < 3 || input.LoginFailLimit > 20 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
if input.DefaultPageSize < 10 || input.DefaultPageSize > 200 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
if input.AuditLogRetentionDays < 7 || input.AuditLogRetentionDays > 3650 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
config, err := s.ensureSystemConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.SiteName = siteName
|
||||
config.AllowRegister = input.AllowRegister
|
||||
config.LoginFailLimit = input.LoginFailLimit
|
||||
config.DefaultPageSize = input.DefaultPageSize
|
||||
config.AuditLogRetentionDays = input.AuditLogRetentionDays
|
||||
|
||||
if err := s.db.WithContext(ctx).Save(config).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SystemConfigPayload{
|
||||
SiteName: config.SiteName,
|
||||
AllowRegister: config.AllowRegister,
|
||||
LoginFailLimit: config.LoginFailLimit,
|
||||
DefaultPageSize: config.DefaultPageSize,
|
||||
AuditLogRetentionDays: config.AuditLogRetentionDays,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) ensureSystemConfig(ctx context.Context) (*adminmodel.SystemConfig, error) {
|
||||
var config adminmodel.SystemConfig
|
||||
err := s.db.WithContext(ctx).Order("id ASC").First(&config).Error
|
||||
if err == nil {
|
||||
return &config, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config = adminmodel.SystemConfig{
|
||||
SiteName: "多小程序管理后台",
|
||||
AllowRegister: false,
|
||||
LoginFailLimit: 5,
|
||||
DefaultPageSize: 20,
|
||||
AuditLogRetentionDays: 180,
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Create(&config).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func normalizeTimezone(value string) string {
|
||||
timezone := strings.TrimSpace(value)
|
||||
if timezone == "" {
|
||||
return "Asia/Shanghai"
|
||||
}
|
||||
return timezone
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
adminmodel "wx_service/internal/admin/model"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *Service) UpdatePassword(ctx context.Context, adminID uint, oldPassword, newPassword string) error {
|
||||
oldPassword = strings.TrimSpace(oldPassword)
|
||||
newPassword = strings.TrimSpace(newPassword)
|
||||
if adminID == 0 || oldPassword == "" || newPassword == "" {
|
||||
return ErrInvalidInput
|
||||
}
|
||||
if len(newPassword) < 6 {
|
||||
return ErrPasswordPolicyViolation
|
||||
}
|
||||
if oldPassword == newPassword {
|
||||
return ErrPasswordPolicyViolation
|
||||
}
|
||||
|
||||
var admin adminmodel.Admin
|
||||
if err := s.db.WithContext(ctx).Where("id = ?", adminID).First(&admin).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrAdminUserNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(oldPassword)); err != nil {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&adminmodel.Admin{}).
|
||||
Where("id = ?", adminID).
|
||||
Update("password", string(hashedPassword)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -19,6 +19,7 @@ var (
|
||||
ErrAdminUserNotFound = errors.New("admin user not found")
|
||||
ErrMembershipRedeemCodeNotFound = errors.New("membership redeem code not found")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrPasswordPolicyViolation = errors.New("password policy violation")
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
|
||||
Reference in New Issue
Block a user