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,145 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/author"
|
||||
domain "RenewCMS/internal/domain/user"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const (
|
||||
emptySocialLinks = "[]"
|
||||
sqliteDialect = "sqlite"
|
||||
)
|
||||
|
||||
var contentionMarkers = []string{
|
||||
"database is locked",
|
||||
"sqlite_busy",
|
||||
"deadlock",
|
||||
"could not serialize",
|
||||
}
|
||||
|
||||
type AccountEraser struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
var _ domain.Eraser = &AccountEraser{}
|
||||
|
||||
func NewAccountEraser(db *gorm.DB) *AccountEraser {
|
||||
return &AccountEraser{db}
|
||||
}
|
||||
|
||||
func (e *AccountEraser) Erase(erasure domain.Erasure) error {
|
||||
err := e.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := e.recheckHolders(tx, erasure); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("user_id = ?", erasure.UserID).Delete(&entity.APIKey{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
ownAuthorID, err := e.releaseAuthor(tx, erasure.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if erasure.AvatarImageID != nil {
|
||||
if err := tx.Delete(&entity.Image{}, *erasure.AvatarImageID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := NewAuditEventRepository(tx).AnonymiseUser(erasure.UserID, ownAuthorID, domain.DeletedUserName); err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.Delete(&entity.User{}, erasure.UserID)
|
||||
return deleteOrNotFound(result, domain.ErrUserNotFound)
|
||||
})
|
||||
if err != nil && len(erasure.GuardRoleIDs) > 0 && isContention(err) {
|
||||
return domain.ErrAccountNotDeletable
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func isContention(err error) bool {
|
||||
message := strings.ToLower(err.Error())
|
||||
for _, marker := range contentionMarkers {
|
||||
if strings.Contains(message, marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *AccountEraser) recheckHolders(tx *gorm.DB, erasure domain.Erasure) error {
|
||||
if len(erasure.GuardRoleIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := lockRoles(tx, erasure.GuardRoleIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
var holders []entity.User
|
||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Select("id").
|
||||
Where("role_id IN ? AND is_verified = ? AND id <> ?", erasure.GuardRoleIDs, true, erasure.UserID).
|
||||
Order("id ASC").
|
||||
Find(&holders).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(holders) == 0 {
|
||||
return domain.ErrAccountNotDeletable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lockRoles(tx *gorm.DB, roleIDs []uint32) error {
|
||||
if tx.Dialector.Name() == sqliteDialect {
|
||||
return tx.Model(&entity.Role{}).Where("id IN ?", roleIDs).UpdateColumn("id", gorm.Expr("id")).Error
|
||||
}
|
||||
var locked []entity.Role
|
||||
return tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id IN ?", roleIDs).Order("id ASC").Find(&locked).Error
|
||||
}
|
||||
|
||||
func (e *AccountEraser) releaseAuthor(tx *gorm.DB, userID uint32) (*uint32, error) {
|
||||
var linked entity.Author
|
||||
found := tx.Where("user_id = ?", userID).Limit(1).Find(&linked)
|
||||
if found.Error != nil {
|
||||
return nil, found.Error
|
||||
}
|
||||
if found.RowsAffected == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
ownID := linked.ID
|
||||
var credits int64
|
||||
if err := tx.Model(&entity.ArticleAuthor{}).Where("author_id = ?", linked.ID).Count(&credits).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if credits == 0 {
|
||||
return &ownID, tx.Delete(&entity.Author{}, linked.ID).Error
|
||||
}
|
||||
slug, err := e.freeDeletedSlug(tx, linked.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ownID, tx.Model(&entity.Author{}).Where("id = ?", linked.ID).Updates(map[string]any{
|
||||
"name": author.DeletedName,
|
||||
"slug": slug,
|
||||
"bio": "",
|
||||
"avatar_id": nil,
|
||||
"website": nil,
|
||||
"social_links": emptySocialLinks,
|
||||
"user_id": nil,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (e *AccountEraser) freeDeletedSlug(tx *gorm.DB, authorID uint32) (string, error) {
|
||||
for attempt := 1; ; attempt++ {
|
||||
candidate := author.DeletedSlugAttempt(authorID, attempt)
|
||||
var taken int64
|
||||
err := tx.Model(&entity.Author{}).Where("slug = ? AND id <> ?", candidate, authorID).Count(&taken).Error
|
||||
if err != nil || taken == 0 {
|
||||
return candidate, err
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user