feat: change email, export my data, delete account (with data anonymization)
This commit is contained in:
1 parent
bb191c389a
commit
10a66cd979
123 files changed
+10789
-76
No files matched your search
@@ -0,0 +1,199 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user