package persistence import ( "RenewCMS/internal/domain/role" domainUser "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/persistence/mappers" entity "RenewCMS/internal/infrastructure/persistence/models" "time" "gorm.io/gorm" "gorm.io/gorm/clause" ) type UserRepository struct { db *gorm.DB } func NewUserRepository(db *gorm.DB) *UserRepository { return &UserRepository{db} } func (u *UserRepository) Get(id uint32) (domainUser.User, error) { var localUser entity.User err := u.db.Model(&entity.User{}).First(&localUser, id).Error if err != nil { return domainUser.User{}, mapNotFoundErr(err, domainUser.ErrUserNotFound) } return mappers.UserToDomain(localUser), nil } func (u *UserRepository) Create(user domainUser.User) (domainUser.User, error) { createdUser := entity.User{ Username: user.Username, Password: user.Password, PasswordResetCode: mappers.DomainToNullableString(user.PasswordResetCode), Email: user.Email, IsVerified: user.IsVerified, RoleID: mappers.ZeroAsNullID(user.RoleID), VerificationCode: mappers.DomainToNullableString(user.VerificationCode), VerificationExpiration: user.VerificationExpiration, } if err := u.db.Create(&createdUser).Error; err != nil { return domainUser.User{}, err } return mappers.UserToDomain(createdUser), nil } func (u *UserRepository) Delete(id uint32) error { result := u.db.Delete(&entity.User{}, id) return deleteOrNotFound(result, domainUser.ErrUserNotFound) } func (u *UserRepository) DeleteIfRole(id uint32, roleID uint32) error { return u.db.Transaction(func(tx *gorm.DB) error { result := tx.Where("id = ? AND role_id = ?", id, roleID).Delete(&entity.User{}) if result.Error != nil { return result.Error } if result.RowsAffected > 0 { return nil } var users int64 if err := tx.Model(&entity.User{}).Where("id = ?", id).Count(&users).Error; err != nil { return err } if users == 0 { return domainUser.ErrUserNotFound } return domainUser.ErrRoleChanged }) } func (u *UserRepository) GetAll() ([]domainUser.User, error) { var users []entity.User if err := u.db.Model(&entity.User{}).Find(&users).Error; err != nil { return nil, err } var domainUsers []domainUser.User for _, localUser := range users { domainUsers = append(domainUsers, mappers.UserToDomain(localUser)) } return domainUsers, nil } func (u *UserRepository) FindByIDs(ids []uint32) ([]domainUser.User, error) { var users []entity.User if err := u.db.Select("id", "username", "display_name").Where("id IN ?", ids).Find(&users).Error; err != nil { return nil, err } found := make([]domainUser.User, 0, len(users)) for _, localUser := range users { found = append(found, mappers.UserToDomain(localUser)) } return found, nil } func (u *UserRepository) GetByUsername(username string) (domainUser.User, error) { var localUser entity.User err := u.db.Model(&entity.User{}).Where("username = ?", username).First(&localUser).Error if err != nil { return domainUser.User{}, mapNotFoundErr(err, domainUser.ErrUserNotFound) } return mappers.UserToDomain(localUser), nil } func (u *UserRepository) GetByEmail(email string) (domainUser.User, error) { var localUser entity.User err := u.db.Model(&entity.User{}).Where("email = ?", email).First(&localUser).Error if err != nil { return domainUser.User{}, mapNotFoundErr(err, domainUser.ErrUserNotFound) } return mappers.UserToDomain(localUser), nil } func (u *UserRepository) UpdateVerificationStatus(userId uint32, isVerified bool) (domainUser.User, error) { var localUser entity.User err := u.db.Model(&entity.User{}).First(&localUser, userId).Error if err != nil { return domainUser.User{}, mapNotFoundErr(err, domainUser.ErrUserNotFound) } localUser.IsVerified = isVerified err = u.db.Save(&localUser).Error if err != nil { return domainUser.User{}, err } return mappers.UserToDomain(localUser), nil } func (u *UserRepository) UpdatePassword(userId uint32, password string) (domainUser.User, error) { var localUser entity.User err := u.db.Model(&entity.User{}).First(&localUser, userId).Error if err != nil { return domainUser.User{}, mapNotFoundErr(err, domainUser.ErrUserNotFound) } localUser.Password = password err = u.db.Save(&localUser).Error if err != nil { return domainUser.User{}, err } return mappers.UserToDomain(localUser), nil } func (u *UserRepository) UpdatePasswordResetCode(userId uint32, code string, expiration time.Time) (domainUser.User, error) { var localUser entity.User err := u.db.Model(&entity.User{}).First(&localUser, userId).Error if err != nil { return domainUser.User{}, mapNotFoundErr(err, domainUser.ErrUserNotFound) } localUser.PasswordResetCode = mappers.DomainToNullableString(code) localUser.PasswordResetExpiration = expiration err = u.db.Save(&localUser).Error if err != nil { return domainUser.User{}, err } return mappers.UserToDomain(localUser), nil } func (u *UserRepository) ClearVerificationCode(userId uint32) (domainUser.User, error) { var localUser entity.User err := u.db.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&entity.User{}).First(&localUser, userId).Error; err != nil { return mapNotFoundErr(err, domainUser.ErrUserNotFound) } if err := refuseSecondVerifiedOwner(tx, localUser); err != nil { return err } localUser.IsVerified = true localUser.VerificationCode = mappers.DomainToNullableString("") return tx.Save(&localUser).Error }) if err != nil { return domainUser.User{}, err } return mappers.UserToDomain(localUser), nil } func refuseSecondVerifiedOwner(tx *gorm.DB, candidate entity.User) error { if candidate.RoleID == nil { return nil } var roles []entity.Role err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND built_in = ? AND name = ?", *candidate.RoleID, true, role.OwnerName).Find(&roles).Error if err != nil || len(roles) == 0 { return err } var others int64 err = tx.Model(&entity.User{}).Where("role_id = ? AND is_verified = ? AND id <> ?", *candidate.RoleID, true, candidate.ID).Count(&others).Error if err != nil { return err } if others > 0 { return domainUser.ErrInvalidVerificationCode } return nil } func (u *UserRepository) ResetPassword(userId uint32, hashedPassword string) (domainUser.User, error) { var localUser entity.User err := u.db.Model(&entity.User{}).First(&localUser, userId).Error if err != nil { return domainUser.User{}, mapNotFoundErr(err, domainUser.ErrUserNotFound) } localUser.Password = hashedPassword localUser.PasswordResetCode = mappers.DomainToNullableString("") localUser.PasswordResetExpiration = time.Time{} localUser.TokenVersion++ err = u.db.Save(&localUser).Error if err != nil { return domainUser.User{}, err } return mappers.UserToDomain(localUser), nil } func (u *UserRepository) UpdateDisplayName(userId uint32, displayName string) (domainUser.User, error) { var localUser entity.User err := u.db.Model(&entity.User{}).First(&localUser, userId).Error if err != nil { return domainUser.User{}, mapNotFoundErr(err, domainUser.ErrUserNotFound) } localUser.DisplayName = displayName err = u.db.Save(&localUser).Error if err != nil { return domainUser.User{}, err } return mappers.UserToDomain(localUser), nil } func (u *UserRepository) IsEmailInUse(email string, exceptUserId uint32) (bool, error) { return isEmailInUse(u.db, email, exceptUserId) } func isEmailInUse(db *gorm.DB, email string, exceptUserId uint32) (bool, error) { var holders int64 err := db.Model(&entity.User{}).Where("LOWER(email) = LOWER(?) AND id <> ?", email, exceptUserId).Count(&holders).Error return holders > 0, err } func (u *UserRepository) SetPendingEmailChange(userId uint32, pendingEmail string, code string, expiration time.Time) (domainUser.User, error) { return u.updatePendingEmailChange(userId, map[string]any{ "pending_email": mappers.DomainToNullableString(pendingEmail), "email_change_code": mappers.DomainToNullableString(code), "email_change_expiration": expiration, }) } func (u *UserRepository) ClearPendingEmailChange(userId uint32) (domainUser.User, error) { return u.updatePendingEmailChange(userId, clearedEmailChange()) } func clearedEmailChange() map[string]any { return map[string]any{ "pending_email": nil, "email_change_code": nil, "email_change_expiration": time.Time{}, } } func (u *UserRepository) updatePendingEmailChange(userId uint32, columns map[string]any) (domainUser.User, error) { result := u.db.Model(&entity.User{}).Where("id = ?", userId).Updates(columns) if result.Error != nil { return domainUser.User{}, result.Error } return u.Get(userId) } func (u *UserRepository) ConfirmEmailChange(userId uint32, pendingEmail string, code string, now time.Time) (domainUser.User, error) { taken, err := isEmailInUse(u.db, pendingEmail, userId) if err != nil { return domainUser.User{}, err } columns := clearedEmailChange() if !taken { columns["email"] = pendingEmail } result := pendingChangeOf(u.db, userId, pendingEmail, code).Where("email_change_expiration > ?", now).Updates(columns) if result.Error != nil && isUniqueViolation(result.Error) { return domainUser.User{}, u.refuseTakenAddress(userId, pendingEmail, code) } if result.Error != nil { return domainUser.User{}, result.Error } if result.RowsAffected == 0 { return domainUser.User{}, domainUser.ErrInvalidEmailChangeCode } if taken { return domainUser.User{}, domainUser.ErrEmailUnavailable } return u.Get(userId) } func (u *UserRepository) refuseTakenAddress(userId uint32, pendingEmail string, code string) error { if err := pendingChangeOf(u.db, userId, pendingEmail, code).Updates(clearedEmailChange()).Error; err != nil { return err } return domainUser.ErrEmailUnavailable } func pendingChangeOf(db *gorm.DB, userId uint32, pendingEmail string, code string) *gorm.DB { return db.Model(&entity.User{}).Where("id = ? AND pending_email = ? AND email_change_code = ?", userId, pendingEmail, code) } func (u *UserRepository) UpdateRole(id uint32, from uint32, to uint32, assignedBy uint32) (domainUser.User, error) { err := u.db.Transaction(func(tx *gorm.DB) error { query := tx.Model(&entity.User{}).Where("id = ? AND EXISTS (SELECT 1 FROM roles WHERE id = ?)", id, to) if from == 0 { query = query.Where("(role_id IS NULL OR role_id = 0)") } else { query = query.Where("role_id = ?", from) } result := query.Updates(map[string]any{"role_id": to, "role_assigned_by_id": mappers.ZeroAsNullID(assignedBy)}) if result.Error != nil { return result.Error } if result.RowsAffected > 0 { return nil } return u.explainRoleRefusal(tx, id, to) }) if err != nil { return domainUser.User{}, err } return u.Get(id) } func (u *UserRepository) explainRoleRefusal(tx *gorm.DB, id uint32, to uint32) error { var users int64 if err := tx.Model(&entity.User{}).Where("id = ?", id).Count(&users).Error; err != nil { return err } if users == 0 { return domainUser.ErrUserNotFound } var roles int64 if err := tx.Model(&entity.Role{}).Where("id = ?", to).Count(&roles).Error; err != nil { return err } if roles == 0 { return role.ErrRoleNotFound } return domainUser.ErrRoleChanged } func (u *UserRepository) TransferOwnership(from uint32, to uint32, ownerRoleID uint32, formerRoleID uint32) error { if from == to { return domainUser.ErrInvalidTransfer } return u.db.Transaction(func(tx *gorm.DB) error { demoted := tx.Model(&entity.User{}).Where("id = ? AND role_id = ?", from, ownerRoleID).Updates(map[string]any{"role_id": formerRoleID, "role_assigned_by_id": nil}) if demoted.Error != nil { return demoted.Error } if demoted.RowsAffected == 0 { return domainUser.ErrRoleChanged } promoted := tx.Model(&entity.User{}).Where("id = ? AND is_verified = ?", to, true).Updates(map[string]any{"role_id": ownerRoleID, "role_assigned_by_id": from}) if promoted.Error != nil { return promoted.Error } if promoted.RowsAffected == 0 { return domainUser.ErrInvalidTransfer } return nil }) } var _ domainUser.Repository = &UserRepository{}