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) }