146 lines
3.8 KiB
Go
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
|
|
}
|
|
}
|
|
}
|