311 lines
7.8 KiB
Go
311 lines
7.8 KiB
Go
package services
|
|
|
|
import (
|
|
"backend/internal/core/models"
|
|
"backend/internal/core/repos"
|
|
"backend/internal/core/utils"
|
|
"backend/pkg/cache"
|
|
"backend/pkg/logger"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
var (
|
|
ErrUserNotExists = fmt.Errorf("no such user")
|
|
ErrUserExists = fmt.Errorf("user with this login already exists")
|
|
ErrUserWrongPassword = fmt.Errorf("wrong password")
|
|
ErrUserWrongToken = fmt.Errorf("bad user token")
|
|
ErrUserBadPassword = fmt.Errorf("password must contain at least 8 characters")
|
|
ErrUserEmailUnverified = fmt.Errorf("user has not verified email yet")
|
|
// ErrUserInternal = fmt.Errorf("unexpected error. contact tech support")
|
|
)
|
|
|
|
const (
|
|
userCacheTtl = time.Hour
|
|
)
|
|
|
|
type UserService interface {
|
|
CreateUser(ctx context.Context, params UserCreateParams) (*models.UserDTO, error)
|
|
AuthenticateUser(ctx context.Context, login, password string) (string, error)
|
|
ValidateAuthToken(ctx context.Context, tokenStr string) (*models.UserDTO, error)
|
|
VerifyEmail(ctx context.Context, actionToken string) error
|
|
|
|
SendEmailForgotPassword(ctx context.Context, userId string) error
|
|
SendEmailVerifyUser(ctx context.Context, email string) error
|
|
|
|
ChangePassword(ctx context.Context, userId, oldPassword, newPassword string) error
|
|
ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error
|
|
}
|
|
|
|
func NewUserService(deps UserServiceDeps) UserService {
|
|
return &userService{deps}
|
|
}
|
|
|
|
type UserServiceDeps struct {
|
|
Jwt utils.JwtUtil
|
|
Password utils.PasswordUtil
|
|
UserRepo repos.UserRepo
|
|
UserCache cache.Cache[string, models.UserDTO]
|
|
JwtCache cache.Cache[string, string]
|
|
EventRepo repos.EventRepo
|
|
ActionTokenRepo repos.ActionTokenRepo
|
|
Logger logger.Logger
|
|
}
|
|
|
|
type userService struct {
|
|
deps UserServiceDeps
|
|
}
|
|
|
|
type UserCreateParams struct {
|
|
Email string
|
|
Password string
|
|
Name string
|
|
}
|
|
|
|
func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) (*models.UserDTO, error) {
|
|
exisitngUser, err := u.deps.UserRepo.GetUserByEmail(ctx, params.Email)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exisitngUser != nil {
|
|
return nil, ErrUserExists
|
|
}
|
|
|
|
if err := u.deps.Password.Validate(params.Password); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
secret, err := u.deps.Password.Hash(params.Password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user := models.UserDTO{
|
|
Email: strings.ToLower(params.Email),
|
|
Secret: string(secret),
|
|
FullName: params.Name,
|
|
}
|
|
|
|
result, err := u.deps.UserRepo.CreateUser(ctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := u.sendEmailVerifyUser(ctx, result.Id, user.Email); err != nil {
|
|
u.deps.Logger.Error().Err(err).Msg("error occured on sending email")
|
|
}
|
|
|
|
u.deps.UserCache.Set(result.Id, *result, cache.Expiration{Ttl: userCacheTtl})
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (u *userService) AuthenticateUser(ctx context.Context, email, password string) (string, error) {
|
|
user, err := u.deps.UserRepo.GetUserByEmail(ctx, email)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if user == nil {
|
|
return "", ErrUserNotExists
|
|
}
|
|
|
|
if !u.deps.Password.Compare(password, user.Secret) {
|
|
return "", ErrUserWrongPassword
|
|
}
|
|
|
|
if !user.EmailVerified {
|
|
return "", ErrUserEmailUnverified
|
|
}
|
|
|
|
payload := utils.JwtPayload{UserId: user.Id}
|
|
jwt, err := u.deps.Jwt.Create(payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
u.deps.UserCache.Set(user.Id, *user, cache.Expiration{Ttl: userCacheTtl})
|
|
|
|
return jwt, nil
|
|
}
|
|
|
|
func (u *userService) VerifyEmail(ctx context.Context, actionToken string) error {
|
|
token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenVerifyEmail)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if token == nil {
|
|
return fmt.Errorf("wrong action token")
|
|
}
|
|
|
|
if err := u.deps.UserRepo.SetUserEmailVerified(ctx, token.UserId); err != nil {
|
|
return err
|
|
}
|
|
|
|
//TODO: log warnings somehow
|
|
u.deps.ActionTokenRepo.DeleteActionToken(ctx, token.Id)
|
|
return nil
|
|
}
|
|
|
|
func (u *userService) SendEmailForgotPassword(ctx context.Context, email string) error {
|
|
// user, err := u.getUserById(ctx, userId)
|
|
user, err := u.deps.UserRepo.GetUserByEmail(ctx, email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
actionToken, err := u.deps.ActionTokenRepo.CreateActionToken(
|
|
ctx,
|
|
models.ActionTokenDTO{
|
|
UserId: user.Id,
|
|
Value: uuid.New().String(),
|
|
Target: models.ActionTokenTargetForgotPassword,
|
|
Expiration: time.Now().Add(15 * time.Minute),
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return u.deps.EventRepo.SendEmailForgotPassword(ctx, user.Email, actionToken.Value)
|
|
}
|
|
|
|
func (u *userService) sendEmailVerifyUser(ctx context.Context, userId, email string) error {
|
|
actionToken, err := u.deps.ActionTokenRepo.CreateActionToken(
|
|
ctx,
|
|
models.ActionTokenDTO{
|
|
UserId: userId,
|
|
Value: uuid.New().String(),
|
|
Target: models.ActionTokenVerifyEmail,
|
|
Expiration: time.Now().Add(1 * time.Hour),
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return u.deps.EventRepo.SendEmailVerifyUser(ctx, email, actionToken.Value)
|
|
}
|
|
|
|
func (u *userService) SendEmailVerifyUser(ctx context.Context, email string) error {
|
|
//user, err := u.getUserById(ctx, userId)
|
|
user, err := u.deps.UserRepo.GetUserByEmail(ctx, email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if user == nil {
|
|
return fmt.Errorf("no such user")
|
|
}
|
|
if user.EmailVerified {
|
|
return fmt.Errorf("user already verified")
|
|
}
|
|
|
|
return u.sendEmailVerifyUser(ctx, user.Id, user.Email)
|
|
}
|
|
|
|
func (u *userService) ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error {
|
|
token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenTargetForgotPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if token == nil {
|
|
return fmt.Errorf("wrong action token")
|
|
}
|
|
|
|
user, err := u.getUserById(ctx, token.UserId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if user == nil {
|
|
return fmt.Errorf("no such user")
|
|
}
|
|
|
|
if err := u.updatePassword(ctx, *user, newPassword); err != nil {
|
|
return err
|
|
}
|
|
|
|
//TODO: log warnings somehow
|
|
u.deps.ActionTokenRepo.DeleteActionToken(ctx, token.Id)
|
|
return nil
|
|
}
|
|
|
|
func (u *userService) ChangePassword(ctx context.Context, userId, oldPassword, newPassword string) error {
|
|
user, err := u.getUserById(ctx, userId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !u.deps.Password.Compare(oldPassword, user.Secret) {
|
|
return ErrUserWrongPassword
|
|
}
|
|
|
|
return u.updatePassword(ctx, *user, newPassword)
|
|
}
|
|
|
|
func (u *userService) updatePassword(ctx context.Context, user models.UserDTO, newPassword string) error {
|
|
if err := u.deps.Password.Validate(newPassword); err != nil {
|
|
return ErrUserBadPassword
|
|
}
|
|
|
|
u.deps.UserCache.Del(user.Id)
|
|
|
|
newSecret, err := u.deps.Password.Hash(newPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = u.deps.UserRepo.UpdateUser(ctx, user.Id, models.UserUpdateDTO{
|
|
Secret: newSecret,
|
|
FullName: user.FullName,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := u.deps.EventRepo.SendEmailPasswordChanged(ctx, user.Email); err != nil {
|
|
u.deps.Logger.Error().Err(err).Msg("error occured on sending email")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *userService) getUserById(ctx context.Context, userId string) (*models.UserDTO, error) {
|
|
if user, ok := u.deps.UserCache.GetEx(userId, cache.Expiration{Ttl: userCacheTtl}); ok {
|
|
return &user, nil
|
|
}
|
|
|
|
user, err := u.deps.UserRepo.GetUserById(ctx, userId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if user == nil {
|
|
return nil, ErrUserNotExists
|
|
}
|
|
|
|
u.deps.UserCache.Set(user.Id, *user, cache.Expiration{Ttl: userCacheTtl})
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (u *userService) ValidateAuthToken(ctx context.Context, tokenStr string) (*models.UserDTO, error) {
|
|
if userId, ok := u.deps.JwtCache.Get(tokenStr); ok {
|
|
return u.getUserById(ctx, userId)
|
|
}
|
|
|
|
payload, err := u.deps.Jwt.Parse(tokenStr)
|
|
if err != nil {
|
|
return nil, ErrUserWrongToken
|
|
}
|
|
|
|
user, err := u.getUserById(ctx, payload.UserId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
u.deps.JwtCache.Set(tokenStr, payload.UserId, cache.Expiration{ExpiresAt: payload.ExpiresAt.Time})
|
|
|
|
return user, nil
|
|
}
|