Files
RenewCMS/internal/infrastructure/persistence/repositories/accountEraser.go
T

146 lines
3.8 KiB
Go

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