mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
6a53619dd2
将平台域持久化收敛为 repository 唯一入口,model 去掉 IO。 邮件头写入前清除 CR/LF,防止 header 注入。 httppool 支持可配置 Transport;batchwriter 增加 MinBatchSize/Stats,flush 失败交回批次;任务 PermanentError 作为 SkipRetry 终态。 设置与推送页的确认改为 AlertDialog;axios 去尾斜杠并按 Gin 数组序列化查询参数。 升级共享 Go 依赖(Gin、Asynq、OTel、GORM、Redis 等)。
211 lines
5.2 KiB
Go
211 lines
5.2 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
|
"github.com/Rain-kl/Wavelet/internal/infra/persistence/idgen"
|
|
"github.com/Rain-kl/Wavelet/internal/model"
|
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
|
)
|
|
|
|
func listUsers(ctx context.Context, req listUsersRequest) (int64, []model.User, error) {
|
|
return repository.ListAdminUsers(ctx, repository.AdminUserListFilter{
|
|
UserID: req.UserID,
|
|
Username: strings.TrimSpace(req.Username),
|
|
Email: strings.TrimSpace(req.Email),
|
|
Page: req.Page,
|
|
PageSize: req.PageSize,
|
|
})
|
|
}
|
|
|
|
func getUserDetail(ctx context.Context, id uint64) (model.User, error) {
|
|
return repository.GetAdminUserDetail(ctx, id)
|
|
}
|
|
|
|
func updateUserStatus(ctx context.Context, id uint64, active bool) error {
|
|
flags, err := repository.GetUserAdminFlags(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !active && flags.IsAdmin {
|
|
return errors.New(cannotDisable)
|
|
}
|
|
|
|
var tokens []model.AccessToken
|
|
if !active {
|
|
tokens, _ = repository.ListAccessTokensByUserID(ctx, id)
|
|
}
|
|
|
|
err = repository.UpdateUserActive(ctx, id, active)
|
|
if err == nil {
|
|
oauth.InvalidateCachedUser(ctx, id)
|
|
if !active {
|
|
for _, token := range tokens {
|
|
oauth.InvalidateCachedToken(ctx, token.TokenHash)
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func deleteUser(ctx context.Context, currentUserID, targetID uint64) error {
|
|
if currentUserID == targetID {
|
|
return errors.New(cannotDeleteSelf)
|
|
}
|
|
flags, err := repository.GetUserAdminFlags(ctx, targetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if flags.IsAdmin {
|
|
return errors.New(cannotDelete)
|
|
}
|
|
|
|
tokens, _ := repository.ListAccessTokensByUserID(ctx, targetID)
|
|
|
|
err = repository.DeleteUserWithRelations(ctx, targetID)
|
|
if err == nil {
|
|
oauth.InvalidateCachedUser(ctx, targetID)
|
|
for _, token := range tokens {
|
|
oauth.InvalidateCachedToken(ctx, token.TokenHash)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func createUser(ctx context.Context, req createUserRequest) (model.User, error) {
|
|
req.Username = strings.TrimSpace(req.Username)
|
|
req.Nickname = strings.TrimSpace(req.Nickname)
|
|
req.Password = strings.TrimSpace(req.Password)
|
|
req.Email = strings.TrimSpace(req.Email)
|
|
|
|
if req.Username == "" {
|
|
return model.User{}, errors.New(usernameRequired)
|
|
}
|
|
if req.Email == "" {
|
|
return model.User{}, errors.New(emailRequired)
|
|
}
|
|
if len(req.Password) < minPasswordLength {
|
|
return model.User{}, errors.New(passwordTooShort)
|
|
}
|
|
|
|
count, err := repository.CountUsersByUsername(ctx, req.Username)
|
|
if err != nil {
|
|
return model.User{}, err
|
|
}
|
|
if count > 0 {
|
|
return model.User{}, errors.New(usernameExists)
|
|
}
|
|
|
|
emailCount, err := repository.CountUsersByEmail(ctx, req.Email)
|
|
if err != nil {
|
|
return model.User{}, err
|
|
}
|
|
if emailCount > 0 {
|
|
return model.User{}, errors.New(emailExists)
|
|
}
|
|
|
|
newUser := model.User{
|
|
ID: idgen.NextUint64ID(),
|
|
Username: req.Username,
|
|
Nickname: req.Nickname,
|
|
Email: req.Email,
|
|
IsActive: req.IsActive,
|
|
IsAdmin: req.IsAdmin,
|
|
LastLoginAt: time.Time{},
|
|
}
|
|
if newUser.Nickname == "" {
|
|
newUser.Nickname = req.Username
|
|
}
|
|
if err := newUser.SetEncryptedPassword(req.Password); err != nil {
|
|
return model.User{}, err
|
|
}
|
|
if err := repository.CreateUser(ctx, &newUser); err != nil {
|
|
return model.User{}, err
|
|
}
|
|
return newUser, nil
|
|
}
|
|
|
|
type updateUserParam struct {
|
|
ID uint64
|
|
Nickname string
|
|
Email string
|
|
IsAdmin bool
|
|
Password string
|
|
}
|
|
|
|
func updateUser(ctx context.Context, currentUserID uint64, param updateUserParam) error {
|
|
param.Nickname = strings.TrimSpace(param.Nickname)
|
|
param.Email = strings.TrimSpace(param.Email)
|
|
param.Password = strings.TrimSpace(param.Password)
|
|
|
|
if param.Email == "" {
|
|
return errors.New(emailRequired)
|
|
}
|
|
|
|
targetUser, err := repository.GetAdminUserDetail(ctx, param.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 不能撤销当前登录用户的管理员权限
|
|
if currentUserID == param.ID && !param.IsAdmin && targetUser.IsAdmin {
|
|
return errors.New(cannotRevokeSelfAdmin)
|
|
}
|
|
|
|
// 如果修改了邮箱,检查邮箱是否被其他用户占用
|
|
if targetUser.Email != param.Email {
|
|
count, err := repository.CountUsersByEmail(ctx, param.Email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count > 0 {
|
|
return errors.New(emailExists)
|
|
}
|
|
}
|
|
|
|
// 密码强度校验(如果输入了新密码)
|
|
if param.Password != "" && len(param.Password) < minPasswordLength {
|
|
return errors.New(passwordTooShort)
|
|
}
|
|
|
|
// 是否需要撤销 Token (重置密码或取消管理员)
|
|
needRevokeTokens := (param.Password != "") || (targetUser.IsAdmin && !param.IsAdmin)
|
|
var tokens []model.AccessToken
|
|
if needRevokeTokens {
|
|
tokens, _ = repository.ListAccessTokensByUserID(ctx, param.ID)
|
|
}
|
|
|
|
// 更新字段
|
|
targetUser.Nickname = param.Nickname
|
|
if targetUser.Nickname == "" {
|
|
targetUser.Nickname = targetUser.Username
|
|
}
|
|
targetUser.Email = param.Email
|
|
targetUser.IsAdmin = param.IsAdmin
|
|
|
|
if param.Password != "" {
|
|
if err := targetUser.SetEncryptedPassword(param.Password); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// 执行更新
|
|
err = repository.UpdateUser(ctx, &targetUser)
|
|
if err == nil {
|
|
oauth.InvalidateCachedUser(ctx, param.ID)
|
|
if needRevokeTokens {
|
|
for _, token := range tokens {
|
|
oauth.InvalidateCachedToken(ctx, token.TokenHash)
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|