refac: reamaining auth surface rewrite, T0
This commit is contained in:
1 parent
6d510d2805
commit
aa865af2e9
50 files changed
+3723
-149
No files matched your search
@@ -8,16 +8,22 @@ import (
|
||||
var ErrUserNotFound = errors.New("user not found")
|
||||
var ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
var ErrRegistrationClosed = errors.New("registration closed")
|
||||
var ErrInvalidVerificationCode = errors.New("invalid verification code")
|
||||
var ErrVerificationCodeExpired = errors.New("verification code expired")
|
||||
var ErrAlreadyVerified = errors.New("user already verified")
|
||||
var ErrInvalidPasswordResetCode = errors.New("invalid password reset code")
|
||||
var ErrPasswordResetCodeExpired = errors.New("password reset code expired")
|
||||
|
||||
type User struct {
|
||||
ID uint32 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
PasswordResetCode string `json:"password_reset_code"`
|
||||
Email string `json:"email"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
VerificationCode string `json:"verification_code"`
|
||||
VerificationExpiration time.Time `json:"verification_expiration"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint32 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
PasswordResetCode string `json:"password_reset_code"`
|
||||
Email string `json:"email"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
VerificationCode string `json:"verification_code"`
|
||||
VerificationExpiration time.Time `json:"verification_expiration"`
|
||||
PasswordResetExpiration time.Time `json:"password_reset_expiration"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package user
|
||||
|
||||
import "time"
|
||||
|
||||
type Repository interface {
|
||||
Get(id uint32) (User, error)
|
||||
GetByUsername(username string) (User, error)
|
||||
@@ -9,5 +11,7 @@ type Repository interface {
|
||||
Delete(id uint32) error
|
||||
UpdateVerificationStatus(userId uint32, isVerified bool) (User, error)
|
||||
UpdatePassword(userId uint32, password string) (User, error)
|
||||
UpdatePasswordResetCode(userId uint32, code string) (User, error)
|
||||
UpdatePasswordResetCode(userId uint32, code string, expiration time.Time) (User, error)
|
||||
ClearVerificationCode(userId uint32) (User, error)
|
||||
ResetPassword(userId uint32, hashedPassword string) (User, error)
|
||||
}
|
||||
@@ -21,29 +21,31 @@ func DomainToNullableString(value string) *string {
|
||||
|
||||
func UserToDomain(m models.User) user.User {
|
||||
return user.User{
|
||||
ID: m.ID,
|
||||
Username: m.Username,
|
||||
Password: m.Password,
|
||||
PasswordResetCode: nullableStringToDomain(m.PasswordResetCode),
|
||||
Email: m.Email,
|
||||
IsVerified: m.IsVerified,
|
||||
VerificationCode: nullableStringToDomain(m.VerificationCode),
|
||||
VerificationExpiration: m.VerificationExpiration,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
ID: m.ID,
|
||||
Username: m.Username,
|
||||
Password: m.Password,
|
||||
PasswordResetCode: nullableStringToDomain(m.PasswordResetCode),
|
||||
Email: m.Email,
|
||||
IsVerified: m.IsVerified,
|
||||
VerificationCode: nullableStringToDomain(m.VerificationCode),
|
||||
VerificationExpiration: m.VerificationExpiration,
|
||||
PasswordResetExpiration: m.PasswordResetExpiration,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func UserToModel(a user.User) models.User {
|
||||
return models.User{
|
||||
Username: a.Username,
|
||||
Password: a.Password,
|
||||
PasswordResetCode: DomainToNullableString(a.PasswordResetCode),
|
||||
Email: a.Email,
|
||||
IsVerified: a.IsVerified,
|
||||
VerificationCode: DomainToNullableString(a.VerificationCode),
|
||||
VerificationExpiration: a.VerificationExpiration,
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: a.UpdatedAt,
|
||||
Username: a.Username,
|
||||
Password: a.Password,
|
||||
PasswordResetCode: DomainToNullableString(a.PasswordResetCode),
|
||||
Email: a.Email,
|
||||
IsVerified: a.IsVerified,
|
||||
VerificationCode: DomainToNullableString(a.VerificationCode),
|
||||
VerificationExpiration: a.VerificationExpiration,
|
||||
PasswordResetExpiration: a.PasswordResetExpiration,
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: a.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,15 @@ import (
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint32 `gorm:"primaryKey;autoIncrement"`
|
||||
Username string `gorm:"unique;not null"`
|
||||
Password string `gorm:"not null"`
|
||||
PasswordResetCode *string `gorm:"unique"`
|
||||
Email string `gorm:"unique;not null"`
|
||||
IsVerified bool `gorm:"default=false;not null"`
|
||||
VerificationCode *string `gorm:"unique"`
|
||||
VerificationExpiration time.Time
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
ID uint32 `gorm:"primaryKey;autoIncrement"`
|
||||
Username string `gorm:"unique;not null"`
|
||||
Password string `gorm:"not null"`
|
||||
PasswordResetCode *string `gorm:"unique"`
|
||||
Email string `gorm:"unique;not null"`
|
||||
IsVerified bool `gorm:"default=false;not null"`
|
||||
VerificationCode *string `gorm:"unique"`
|
||||
VerificationExpiration time.Time
|
||||
PasswordResetExpiration time.Time
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
domainUser "RenewCMS/internal/domain/user"
|
||||
"RenewCMS/internal/infrastructure/persistence/mappers"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -115,7 +116,7 @@ func (u *UserRepository) UpdatePassword(userId uint32, password string) (domainU
|
||||
return mappers.UserToDomain(localUser), nil
|
||||
}
|
||||
|
||||
func (u *UserRepository) UpdatePasswordResetCode(userId uint32, code string) (domainUser.User, error) {
|
||||
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 {
|
||||
@@ -123,6 +124,42 @@ func (u *UserRepository) UpdatePasswordResetCode(userId uint32, code string) (do
|
||||
}
|
||||
|
||||
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.Model(&entity.User{}).First(&localUser, userId).Error
|
||||
if err != nil {
|
||||
return domainUser.User{}, mapNotFoundErr(err, domainUser.ErrUserNotFound)
|
||||
}
|
||||
|
||||
localUser.IsVerified = true
|
||||
localUser.VerificationCode = mappers.DomainToNullableString("")
|
||||
err = u.db.Save(&localUser).Error
|
||||
if err != nil {
|
||||
return domainUser.User{}, err
|
||||
}
|
||||
|
||||
return mappers.UserToDomain(localUser), 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{}
|
||||
err = u.db.Save(&localUser).Error
|
||||
if err != nil {
|
||||
return domainUser.User{}, err
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/user"
|
||||
)
|
||||
|
||||
type CancelRegistrationUseCase struct {
|
||||
userRepository user.Repository
|
||||
}
|
||||
|
||||
func NewCancelRegistrationUseCase(userRepository user.Repository) *CancelRegistrationUseCase {
|
||||
return &CancelRegistrationUseCase{userRepository}
|
||||
}
|
||||
|
||||
func (c *CancelRegistrationUseCase) CancelRegistration(userId uint32) error {
|
||||
dbUser, err := c.userRepository.Get(userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dbUser.IsVerified {
|
||||
return user.ErrAlreadyVerified
|
||||
}
|
||||
|
||||
return c.userRepository.Delete(userId)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/hash"
|
||||
"RenewCMS/internal/domain/mail"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type RequestPasswordResetUseCase struct {
|
||||
userRepository user.Repository
|
||||
hasher hash.Hasher
|
||||
mailRepository mail.Repository
|
||||
publicURL string
|
||||
}
|
||||
|
||||
type MailPasswordResetData struct {
|
||||
Host string
|
||||
VerificationCode string
|
||||
Email string
|
||||
}
|
||||
|
||||
func NewRequestPasswordResetUseCase(userRepository user.Repository, hasher hash.Hasher, mailRepository mail.Repository, publicURL string) *RequestPasswordResetUseCase {
|
||||
return &RequestPasswordResetUseCase{userRepository, hasher, mailRepository, publicURL}
|
||||
}
|
||||
|
||||
func (g *RequestPasswordResetUseCase) RequestPasswordReset(email string) error {
|
||||
foundUser, err := g.userRepository.GetByEmail(email)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rawCode := uuid.NewString()
|
||||
|
||||
hashedCode, err := g.hasher.Hash(rawCode)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := g.userRepository.UpdatePasswordResetCode(foundUser.ID, hashedCode, time.Now().Add(time.Hour)); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = g.mailRepository.Send(foundUser.Email, "passwordReset", MailPasswordResetData{
|
||||
Host: g.publicURL,
|
||||
VerificationCode: rawCode,
|
||||
Email: foundUser.Email,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/hash"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ResetPasswordUseCase struct {
|
||||
userRepository user.Repository
|
||||
hasher hash.Hasher
|
||||
}
|
||||
|
||||
func NewResetPasswordUseCase(userRepository user.Repository, hasher hash.Hasher) *ResetPasswordUseCase {
|
||||
return &ResetPasswordUseCase{userRepository, hasher}
|
||||
}
|
||||
|
||||
func (r *ResetPasswordUseCase) ResetPassword(email string, code string, newPassword string) error {
|
||||
dbUser, err := r.userRepository.GetByEmail(email)
|
||||
if err != nil {
|
||||
return user.ErrInvalidPasswordResetCode
|
||||
}
|
||||
|
||||
if dbUser.PasswordResetCode == "" || r.hasher.Compare(dbUser.PasswordResetCode, code) != nil {
|
||||
return user.ErrInvalidPasswordResetCode
|
||||
}
|
||||
|
||||
if time.Now().After(dbUser.PasswordResetExpiration) {
|
||||
return user.ErrPasswordResetCodeExpired
|
||||
}
|
||||
|
||||
hashedPassword, err := r.hasher.Hash(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = r.userRepository.ResetPassword(dbUser.ID, hashedPassword)
|
||||
return err
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package useCases
|
||||
import (
|
||||
"RenewCMS/internal/domain/hash"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UpdateUserUseCase struct {
|
||||
@@ -18,12 +19,15 @@ func (g *UpdateUserUseCase) UpdateVerificationStatus(userId uint32, isVerified b
|
||||
return g.userRepository.UpdateVerificationStatus(userId, isVerified)
|
||||
}
|
||||
|
||||
func (g *UpdateUserUseCase) UpdatePasswordResetCode(userId uint32, code string) (user.User, error) {
|
||||
func (g *UpdateUserUseCase) UpdatePasswordResetCode(userId uint32, code string, expiration time.Time) (user.User, error) {
|
||||
if code == "" {
|
||||
return g.userRepository.UpdatePasswordResetCode(userId, "", expiration)
|
||||
}
|
||||
hashedCode, err := g.hasher.Hash(code)
|
||||
if err != nil {
|
||||
return user.User{}, err
|
||||
}
|
||||
return g.userRepository.UpdatePasswordResetCode(userId, hashedCode)
|
||||
return g.userRepository.UpdatePasswordResetCode(userId, hashedCode, expiration)
|
||||
}
|
||||
|
||||
func (g *UpdateUserUseCase) UpdatePassword(userId uint32, password string) (user.User, error) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/hash"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"time"
|
||||
)
|
||||
|
||||
type VerifyUserUseCase struct {
|
||||
userRepository user.Repository
|
||||
hasher hash.Hasher
|
||||
}
|
||||
|
||||
func NewVerifyUserUseCase(userRepository user.Repository, hasher hash.Hasher) *VerifyUserUseCase {
|
||||
return &VerifyUserUseCase{userRepository, hasher}
|
||||
}
|
||||
|
||||
func (v *VerifyUserUseCase) Verify(userId uint32, code string) (user.User, error) {
|
||||
dbUser, err := v.userRepository.Get(userId)
|
||||
if err != nil {
|
||||
return user.User{}, err
|
||||
}
|
||||
|
||||
if dbUser.VerificationCode == "" || v.hasher.Compare(dbUser.VerificationCode, code) != nil {
|
||||
return user.User{}, user.ErrInvalidVerificationCode
|
||||
}
|
||||
|
||||
if time.Now().After(dbUser.VerificationExpiration) {
|
||||
return user.User{}, user.ErrVerificationCodeExpired
|
||||
}
|
||||
|
||||
return v.userRepository.ClearVerificationCode(userId)
|
||||
}
|
||||
Reference in new issue
Block a user