feat(backend): 支持外部邮箱启用/删除接口及用户账号级联删除接口
Some checks are pending
Security Checks / security (push) Waiting to run
Some checks are pending
Security Checks / security (push) Waiting to run
This commit is contained in:
parent
00f3283a4a
commit
583b9e88b7
@ -1168,6 +1168,39 @@ func VerifyOwnExternalEmail(ctx *context.Context, w http.ResponseWriter, req *ht
|
||||
response.NewSuccessResponse(map[string]any{"verified": true}).FPrint(w)
|
||||
}
|
||||
|
||||
func DeleteExternalEmail(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
runtime := currentSecurityRuntime()
|
||||
if ctx == nil || ctx.UserID <= 0 || runtime.external == nil {
|
||||
writeSecurityError(w, response.NeedLogin, "请先登录")
|
||||
return
|
||||
}
|
||||
var input externalEmailRequest
|
||||
if err := decodeJSONRequest(w, req, &input); err != nil || input.ID <= 0 {
|
||||
writeSecurityError(w, response.ParamsError, "请求无效")
|
||||
return
|
||||
}
|
||||
if err := runtime.external.Delete(ctx.UserID, input.ID); err != nil {
|
||||
writeSecurityError(w, response.ParamsError, "外部邮箱删除失败")
|
||||
return
|
||||
}
|
||||
if runtime.verification != nil && runtime.verification.Store != nil {
|
||||
runtime.verification.Store.InvalidateResource(input.ID, verification.PurposeExternalEmailBind)
|
||||
runtime.verification.Store.InvalidateResource(input.ID, verification.PurposeExternalEmailChange)
|
||||
runtime.verification.Store.InvalidateAccount(ctx.UserAccount)
|
||||
}
|
||||
if runtime.recovery != nil {
|
||||
runtime.recovery.InvalidateUser(ctx.UserID)
|
||||
}
|
||||
loginVerificationState.Lock()
|
||||
for id, item := range loginVerificationState.items {
|
||||
if item.UserID == ctx.UserID {
|
||||
delete(loginVerificationState.items, id)
|
||||
}
|
||||
}
|
||||
loginVerificationState.Unlock()
|
||||
response.NewSuccessResponse(map[string]any{"deleted": true}).FPrint(w)
|
||||
}
|
||||
|
||||
func DisableExternalEmail(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
runtime := currentSecurityRuntime()
|
||||
if ctx == nil || ctx.UserID <= 0 || runtime.external == nil {
|
||||
|
||||
@ -603,3 +603,139 @@ func writeUserAuthorizationError(w http.ResponseWriter, err error, recoveryMessa
|
||||
}
|
||||
writeSecurityError(w, response.NoAccessPrivileges, "无权执行该用户管理操作")
|
||||
}
|
||||
|
||||
type userDeleteRequest struct {
|
||||
Id int `json:"id"`
|
||||
Account string `json:"account"`
|
||||
}
|
||||
|
||||
func DeleteUser(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
var reqData userDeleteRequest
|
||||
if err := decodeUserRequest(w, req, &reqData); err != nil {
|
||||
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
if reqData.Id == 0 && reqData.Account == "" {
|
||||
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
var found bool
|
||||
var err error
|
||||
if reqData.Id != 0 {
|
||||
found, err = db.Instance.Where("id=?", reqData.Id).Get(&user)
|
||||
} else {
|
||||
account, validationErr := validation.NormalizeAccount(reqData.Account)
|
||||
if validationErr != nil {
|
||||
response.NewErrorResponse(response.ParamsError, "参数错误", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
found, err = db.Instance.Where("account=?", account).Get(&user)
|
||||
}
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("SQL Error: %+v", err)
|
||||
response.NewErrorResponse(response.ServerError, "查询用户失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
if !found || user.ID == 0 {
|
||||
response.NewErrorResponse(response.ParamsError, "用户不存在", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx != nil && ctx.UserID == user.ID {
|
||||
response.NewErrorResponse(response.ParamsError, "不能删除当前登录的账号", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
targetRole := user.EffectiveRole()
|
||||
if !requireAccountScope(ctx, w, user.ID) {
|
||||
return
|
||||
}
|
||||
if err := authorizeCapability(ctx, access.CapabilityUserManage, targetRole, false); err != nil {
|
||||
response.NewErrorResponse(response.NoAccessPrivileges, "无权删除该用户", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
if targetRole == access.RoleSuperAdmin {
|
||||
var superAdminCount int64
|
||||
superAdminCount, err = db.Instance.Where("role = ? OR is_admin = 1", string(access.RoleSuperAdmin)).Count(&models.User{})
|
||||
if err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("count super admin failed")
|
||||
response.NewErrorResponse(response.ServerError, "检查超级管理员数量失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
if superAdminCount <= 1 {
|
||||
response.NewErrorResponse(response.ParamsError, "系统必须保留至少一个超级管理员,无法删除", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
writeGuard := accountWriteGuard(ctx, user)
|
||||
tx := db.Instance.NewSession()
|
||||
if tx == nil {
|
||||
response.NewErrorResponse(response.ServerError, "删除用户失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
defer tx.Close()
|
||||
if err := tx.Begin(); err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("begin user delete transaction failed")
|
||||
response.NewErrorResponse(response.ServerError, "删除用户失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := accountgroup.CheckWrite(tx, writeGuard); err != nil {
|
||||
_ = tx.Rollback()
|
||||
writeAccountGroupError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
credentialService := credentialRuntime()
|
||||
if credentialService != nil {
|
||||
_ = credentialService.RevokeAllWithSession(tx, user.ID)
|
||||
}
|
||||
|
||||
_, _ = tx.Where("user_id = ?", user.ID).Delete(&models.AccountGroupMember{})
|
||||
_, _ = tx.Where("user_id = ?", user.ID).Delete(&models.ExternalEmail{})
|
||||
_, _ = tx.Where("user_id = ?", user.ID).Delete(&models.RecoveryCode{})
|
||||
_, _ = tx.Where("user_id = ?", user.ID).Delete(&models.WebAuthnCredential{})
|
||||
_, _ = tx.Where("user_id = ?", user.ID).Delete(&models.ClientCredential{})
|
||||
_, _ = tx.Where("user_id = ?", user.ID).Delete(&models.ProtocolCredential{})
|
||||
_, _ = tx.Where("user_id = ?", user.ID).Delete(&models.NotificationGrant{})
|
||||
_, _ = tx.Where("user_id = ?", user.ID).Delete(&models.UserEmail{})
|
||||
_, _ = tx.Where("user_id = ?", user.ID).Delete(&models.UserContact{})
|
||||
_, _ = tx.Where("user_id = ?", user.ID).Delete(&models.Rule{})
|
||||
_, _ = tx.Where("user_id = ?", user.ID).Delete(&models.Group{})
|
||||
|
||||
deleteCond := tx.ID(user.ID)
|
||||
accountgroup.ApplyScope(deleteCond, ctx.UserID, ctx.EffectiveRole())
|
||||
num, err := deleteCond.Delete(&models.User{})
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
log.WithContext(ctx).WithError(err).Error("delete user failed")
|
||||
response.NewErrorResponse(response.ServerError, "删除用户失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
if num == 0 {
|
||||
_ = tx.Rollback()
|
||||
response.NewErrorResponse(response.ServerError, "用户删除未生效", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
_ = tx.Rollback()
|
||||
log.WithContext(ctx).WithError(err).Error("commit user delete failed")
|
||||
response.NewErrorResponse(response.ServerError, "删除用户失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
committed = true
|
||||
|
||||
InvalidateSecurityRuntimeUser(user.ID, user.Account)
|
||||
response.NewSuccessResponse(map[string]any{"deleted": true, "id": user.ID, "account": user.Account}).FPrint(w)
|
||||
}
|
||||
|
||||
@ -54,6 +54,7 @@ func registerV1Routes(mux *http.ServeMux) {
|
||||
registerV1MethodRoute(mux, "/api/v1/security/external-email/change", "/api/security/external-email/change", http.MethodPost, 16<<10, controllers.ChangeOwnExternalEmail)
|
||||
registerV1MethodRoute(mux, "/api/v1/security/external-email/change/verify", "/api/security/external-email/change/verify", http.MethodPost, 16<<10, controllers.VerifyOwnExternalEmail)
|
||||
registerV1MethodRoute(mux, "/api/v1/security/external-email/disable", "/api/security/external-email/disable", http.MethodPost, 16<<10, controllers.DisableExternalEmail)
|
||||
registerV1MethodRoute(mux, "/api/v1/security/external-email/delete", "/api/security/external-email/delete", http.MethodPost, 16<<10, controllers.DeleteExternalEmail)
|
||||
registerV1MethodRoute(mux, "/api/v1/logout", "/api/logout", http.MethodPost, 4<<10, controllers.Logout)
|
||||
registerV1MethodRoute(mux, "/api/v1/system/restart", "/api/v1/system/restart", http.MethodPost, 4<<10, controllers.RestartSmail)
|
||||
registerV1MethodRoute(mux, "/api/v1/system/restart/preview", "/api/v1/system/restart/preview", http.MethodPost, 4<<10, controllers.PreviewRestartSmail)
|
||||
@ -83,6 +84,7 @@ func registerV1Routes(mux *http.ServeMux) {
|
||||
registerV1MethodRoute(mux, "/api/v1/rule/del", "/api/rule/del", http.MethodPost, 64<<10, controllers.DelRule)
|
||||
registerV1MethodRoute(mux, "/api/v1/user/create", "/api/user/create", http.MethodPost, 64<<10, controllers.CreateUser)
|
||||
registerV1MethodRoute(mux, "/api/v1/user/edit", "/api/user/edit", http.MethodPost, 64<<10, controllers.EditUser)
|
||||
registerV1MethodRoute(mux, "/api/v1/user/delete", "/api/user/delete", http.MethodPost, 64<<10, controllers.DeleteUser)
|
||||
registerV1MethodRoute(mux, "/api/v1/user/profile", "/api/user/profile", http.MethodPost, 64<<10, controllers.UpdateProfile)
|
||||
registerV1MethodRoute(mux, "/api/v1/user/info", "/api/user/info", http.MethodPost, 64<<10, controllers.Info)
|
||||
registerV1MethodRoute(mux, "/api/v1/user/list", "/api/user/list", http.MethodPost, 64<<10, controllers.UserList)
|
||||
|
||||
@ -96,6 +96,7 @@ func router(mux *http.ServeMux) error {
|
||||
mux.HandleFunc("/api/security/external-email/change", onlyMethod(http.MethodPost, limitBody(16<<10, contextIterceptor(controllers.ChangeOwnExternalEmail))))
|
||||
mux.HandleFunc("/api/security/external-email/change/verify", onlyMethod(http.MethodPost, limitBody(16<<10, contextIterceptor(controllers.VerifyOwnExternalEmail))))
|
||||
mux.HandleFunc("/api/security/external-email/disable", onlyMethod(http.MethodPost, limitBody(16<<10, contextIterceptor(controllers.DisableExternalEmail))))
|
||||
mux.HandleFunc("/api/security/external-email/delete", onlyMethod(http.MethodPost, limitBody(16<<10, contextIterceptor(controllers.DeleteExternalEmail))))
|
||||
mux.HandleFunc("/api/logout", onlyMethod(http.MethodPost, limitBody(4<<10, contextIterceptor(controllers.Logout))))
|
||||
mux.HandleFunc("/api/group", onlyMethod(http.MethodGet, contextIterceptor(controllers.GetUserGroup)))
|
||||
mux.HandleFunc("/api/group/list", onlyMethod(http.MethodGet, contextIterceptor(controllers.GetUserGroupList)))
|
||||
@ -136,6 +137,7 @@ func router(mux *http.ServeMux) error {
|
||||
mux.HandleFunc("/attachments/download/", onlyMethod(http.MethodGet, contextIterceptor(controllers.Download)))
|
||||
mux.HandleFunc("/api/user/create", onlyMethod(http.MethodPost, limitBody(64<<10, contextIterceptor(controllers.CreateUser))))
|
||||
mux.HandleFunc("/api/user/edit", onlyMethod(http.MethodPost, limitBody(64<<10, contextIterceptor(controllers.EditUser))))
|
||||
mux.HandleFunc("/api/user/delete", onlyMethod(http.MethodPost, limitBody(64<<10, contextIterceptor(controllers.DeleteUser))))
|
||||
mux.HandleFunc("/api/user/profile", onlyMethod(http.MethodPost, limitBody(64<<10, contextIterceptor(controllers.UpdateProfile))))
|
||||
mux.HandleFunc("/api/user/info", onlyMethod(http.MethodPost, limitBody(64<<10, contextIterceptor(controllers.Info))))
|
||||
mux.HandleFunc("/api/user/list", onlyMethod(http.MethodPost, limitBody(64<<10, contextIterceptor(controllers.UserList))))
|
||||
|
||||
@ -23,6 +23,7 @@ var managementEndpoints = []Endpoint{
|
||||
{Method: "POST", Path: "/api/user/create", Capability: CapabilityUserCreate},
|
||||
{Method: "POST", Path: "/api/user/profile", Capability: CapabilityProfileWriteOwn},
|
||||
{Method: "POST", Path: "/api/user/edit", Capability: CapabilityUserManage},
|
||||
{Method: "POST", Path: "/api/user/delete", Capability: CapabilityUserManage},
|
||||
{Method: "*", Path: "/api/user/security/", Capability: CapabilityUserSecurityManage},
|
||||
{Method: "POST", Path: "/api/user/info", Capability: CapabilityUserRead},
|
||||
{Method: "POST", Path: "/api/user/list", Capability: CapabilityUserRead},
|
||||
@ -51,6 +52,7 @@ var managementEndpoints = []Endpoint{
|
||||
{Method: "POST", Path: "/api/v1/user/create", Capability: CapabilityUserCreate},
|
||||
{Method: "POST", Path: "/api/v1/user/profile", Capability: CapabilityProfileWriteOwn},
|
||||
{Method: "POST", Path: "/api/v1/user/edit", Capability: CapabilityUserManage},
|
||||
{Method: "POST", Path: "/api/v1/user/delete", Capability: CapabilityUserManage},
|
||||
{Method: "*", Path: "/api/v1/user/security/", Capability: CapabilityUserSecurityManage},
|
||||
{Method: "POST", Path: "/api/v1/user/info", Capability: CapabilityUserRead},
|
||||
{Method: "POST", Path: "/api/v1/user/list", Capability: CapabilityUserRead},
|
||||
|
||||
@ -411,6 +411,20 @@ func (r *Repository) AbandonPending(userID, id int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) Delete(userID, id int) error {
|
||||
if r == nil || r.engine == nil || userID <= 0 || id <= 0 {
|
||||
return ErrInvalidBinding
|
||||
}
|
||||
affected, err := r.engine.Where("id = ? AND user_id = ?", id, userID).Delete(&models.ExternalEmail{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete external email binding: %w", err)
|
||||
}
|
||||
if affected == 0 {
|
||||
return ErrBindingOwnership
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) Disable(userID, id int) error {
|
||||
if r == nil || r.engine == nil || userID <= 0 || id <= 0 {
|
||||
return ErrInvalidBinding
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span class="settings-label">{{ lang.settings }}</span>
|
||||
</div>
|
||||
<span class="app-version-badge">V2.11.1</span>
|
||||
<span class="app-version-badge">V2.12.0</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user