200 lines
5.8 KiB
Go
200 lines
5.8 KiB
Go
package useCases
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/audit"
|
|
"RenewCMS/internal/domain/author"
|
|
"RenewCMS/internal/domain/hash"
|
|
"RenewCMS/internal/domain/image"
|
|
"RenewCMS/internal/domain/mail"
|
|
"RenewCMS/internal/domain/role"
|
|
"RenewCMS/internal/domain/user"
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"slices"
|
|
"time"
|
|
)
|
|
|
|
const accountDeletedTemplateName = "account-deleted"
|
|
|
|
type MailAccountDeletedData struct {
|
|
Host string
|
|
}
|
|
|
|
type sessionRevoker interface {
|
|
Revoke(id string, expiresAt time.Time) error
|
|
}
|
|
|
|
type DeleteOwnAccountUseCase struct {
|
|
users user.Repository
|
|
roles role.Repository
|
|
hasher hash.Hasher
|
|
eraser user.Eraser
|
|
sessions sessionRevoker
|
|
mailRepository mail.Repository
|
|
dispatcher mail.Dispatcher
|
|
publicURL string
|
|
recorder audit.Recorder
|
|
authors author.Repository
|
|
images image.Repository
|
|
usage *GetMediaUsageUseCase
|
|
avatars mediaRemover
|
|
}
|
|
|
|
type DeleteOwnAccountCommand struct {
|
|
UserID uint32
|
|
CurrentPassword string
|
|
SessionID string
|
|
SessionExpiresAt time.Time
|
|
}
|
|
|
|
func NewDeleteOwnAccountUseCase(users user.Repository, roles role.Repository, hasher hash.Hasher, eraser user.Eraser, sessions sessionRevoker, mailRepository mail.Repository, dispatcher mail.Dispatcher, publicURL string, recorder audit.Recorder, authors author.Repository, images image.Repository, usage *GetMediaUsageUseCase, storage image.Storage, variantFiles image.VariantFileRepository) *DeleteOwnAccountUseCase {
|
|
return &DeleteOwnAccountUseCase{users, roles, hasher, eraser, sessions, mailRepository, dispatcher, publicURL, recorder, authors, images, usage, newMediaRemover(images, storage, variantFiles)}
|
|
}
|
|
|
|
func (u *DeleteOwnAccountUseCase) Delete(ctx context.Context, command DeleteOwnAccountCommand) error {
|
|
account, err := u.users.Get(command.UserID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
event := audit.NewEvent(audit.ActionUserDeleteAccount, audit.CategoryUsers)
|
|
event.Target = userTarget(account)
|
|
event.Security = true
|
|
|
|
guard, err := u.check(account, command)
|
|
if err != nil {
|
|
recordFailure(ctx, u.recorder, event, err)
|
|
return err
|
|
}
|
|
avatar, err := u.removableAvatar(account)
|
|
if err != nil {
|
|
recordFailure(ctx, u.recorder, event, err)
|
|
return err
|
|
}
|
|
erasure := user.Erasure{UserID: account.ID, GuardRoleIDs: guard}
|
|
if avatar != nil {
|
|
erasure.AvatarImageID = &avatar.ID
|
|
}
|
|
if err := u.eraser.Erase(erasure); err != nil {
|
|
recordFailure(ctx, u.recorder, event, err)
|
|
return err
|
|
}
|
|
u.discardAvatar(avatar)
|
|
u.revoke(command)
|
|
u.notify(account)
|
|
u.recordSuccess(ctx, account)
|
|
return nil
|
|
}
|
|
|
|
func (u *DeleteOwnAccountUseCase) check(account user.User, command DeleteOwnAccountCommand) ([]uint32, error) {
|
|
if u.hasher.Compare(account.Password, command.CurrentPassword) != nil {
|
|
return nil, user.ErrWrongPassword
|
|
}
|
|
guard, label, err := u.guardedRoles(account)
|
|
if err != nil || len(guard) == 0 {
|
|
return nil, err
|
|
}
|
|
hasOther, err := u.hasOtherHolder(account.ID, guard)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !hasOther {
|
|
return nil, user.LastAdminError{Role: label}
|
|
}
|
|
return guard, nil
|
|
}
|
|
|
|
func (u *DeleteOwnAccountUseCase) guardedRoles(account user.User) ([]uint32, string, error) {
|
|
owner, err := builtInRole(u.roles, role.OwnerName)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if account.RoleID == owner.ID {
|
|
return []uint32{owner.ID}, user.LastRoleOwner, nil
|
|
}
|
|
admin, err := builtInRole(u.roles, role.AdminName)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if account.RoleID == admin.ID {
|
|
return []uint32{owner.ID, admin.ID}, user.LastRoleAdmin, nil
|
|
}
|
|
return nil, "", nil
|
|
}
|
|
|
|
func (u *DeleteOwnAccountUseCase) hasOtherHolder(exceptUserID uint32, roleIDs []uint32) (bool, error) {
|
|
everyone, err := u.users.GetAll()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, candidate := range everyone {
|
|
if candidate.ID != exceptUserID && candidate.IsVerified && slices.Contains(roleIDs, candidate.RoleID) {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (u *DeleteOwnAccountUseCase) removableAvatar(account user.User) (*image.Image, error) {
|
|
own, err := u.authors.GetByUserID(account.ID)
|
|
if errors.Is(err, author.ErrAuthorNotFound) || err == nil && own.AvatarID == nil {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
avatar, err := u.images.Get(*own.AvatarID)
|
|
if errors.Is(err, image.ErrImageNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
usages, err := u.usage.Usage(avatar.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, found := range usages {
|
|
if found.Type != UsageTypeAuthor || found.ID != own.ID {
|
|
return nil, nil
|
|
}
|
|
}
|
|
return &avatar, nil
|
|
}
|
|
|
|
func (u *DeleteOwnAccountUseCase) discardAvatar(avatar *image.Image) {
|
|
if avatar == nil {
|
|
return
|
|
}
|
|
if err := u.avatars.discardFiles(*avatar); err != nil {
|
|
log.Println("error when deleting the stored avatar of a deleted account:", err)
|
|
}
|
|
}
|
|
|
|
func (u *DeleteOwnAccountUseCase) revoke(command DeleteOwnAccountCommand) {
|
|
if command.SessionID == "" {
|
|
return
|
|
}
|
|
if err := u.sessions.Revoke(command.SessionID, command.SessionExpiresAt); err != nil {
|
|
log.Println("error when revoking the session of a deleted account:", err)
|
|
}
|
|
}
|
|
|
|
func (u *DeleteOwnAccountUseCase) notify(account user.User) {
|
|
address := account.Email
|
|
u.dispatcher.Dispatch(func() {
|
|
if err := u.mailRepository.Send(address, accountDeletedTemplateName, MailAccountDeletedData{Host: u.publicURL}); err != nil {
|
|
log.Println("error when Send-ing the account deleted mail:", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (u *DeleteOwnAccountUseCase) recordSuccess(ctx context.Context, account user.User) {
|
|
event := audit.NewEvent(audit.ActionUserDeleteAccount, audit.CategoryUsers)
|
|
id := account.ID
|
|
event.Actor = audit.Actor{Type: audit.ActorUser, ID: &id, Name: user.DeletedUserName}
|
|
event.Target = audit.NewTarget(audit.TargetUser, account.ID, user.DeletedUserName)
|
|
event.Security = true
|
|
u.recorder.Record(ctx, event)
|
|
}
|