384 lines
12 KiB
Go
384 lines
12 KiB
Go
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{}
|