commit
c4c8f0c6ea
@ -123,6 +123,7 @@ func (a *App) Run(p RunParams) {
|
|||||||
eventRepo = repos.NewEventRepo(kafka)
|
eventRepo = repos.NewEventRepo(kafka)
|
||||||
|
|
||||||
userCache = cache.NewCacheInmemSharded[models.UserDTO](cache.ShardingTypeInteger)
|
userCache = cache.NewCacheInmemSharded[models.UserDTO](cache.ShardingTypeInteger)
|
||||||
|
loginAttemptsCache = cache.NewCacheInmem[string, int]()
|
||||||
jwtCache = cache.NewCacheInmemSharded[string](cache.ShardingTypeJWT)
|
jwtCache = cache.NewCacheInmemSharded[string](cache.ShardingTypeJWT)
|
||||||
linksCache = cache.NewCacheInmem[string, string]()
|
linksCache = cache.NewCacheInmem[string, string]()
|
||||||
)
|
)
|
||||||
@ -140,6 +141,7 @@ func (a *App) Run(p RunParams) {
|
|||||||
userCache.CheckExpired()
|
userCache.CheckExpired()
|
||||||
jwtCache.CheckExpired()
|
jwtCache.CheckExpired()
|
||||||
linksCache.CheckExpired()
|
linksCache.CheckExpired()
|
||||||
|
loginAttemptsCache.CheckExpired()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -150,6 +152,7 @@ func (a *App) Run(p RunParams) {
|
|||||||
Password: passwordUtil,
|
Password: passwordUtil,
|
||||||
UserRepo: userRepo,
|
UserRepo: userRepo,
|
||||||
UserCache: userCache,
|
UserCache: userCache,
|
||||||
|
LoginAttemptsCache: loginAttemptsCache,
|
||||||
JwtCache: jwtCache,
|
JwtCache: jwtCache,
|
||||||
EventRepo: *eventRepo,
|
EventRepo: *eventRepo,
|
||||||
ActionTokenRepo: actionTokenRepo,
|
ActionTokenRepo: actionTokenRepo,
|
||||||
|
|||||||
@ -18,7 +18,7 @@ type createUserInput struct {
|
|||||||
type createUserOutput struct {
|
type createUserOutput struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Name string `json:"name"`
|
FullName string `json:"fullName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserCreateHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc {
|
func NewUserCreateHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc {
|
||||||
@ -39,7 +39,7 @@ func NewUserCreateHandler(log logger.Logger, userService services.UserService) g
|
|||||||
return createUserOutput{
|
return createUserOutput{
|
||||||
Id: user.Id,
|
Id: user.Id,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Name: user.Name,
|
FullName: user.FullName,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -16,7 +16,7 @@ type inputSendRestorePassword struct {
|
|||||||
func NewUserSendRestorePasswordHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc {
|
func NewUserSendRestorePasswordHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc {
|
||||||
return httpserver.WrapGin(log,
|
return httpserver.WrapGin(log,
|
||||||
func(ctx context.Context, input inputSendRestorePassword) (interface{}, error) {
|
func(ctx context.Context, input inputSendRestorePassword) (interface{}, error) {
|
||||||
err := userService.SendEmailForgotPassword(ctx, input.Email)
|
err := userService.RequestRestorePassword(ctx, input.Email)
|
||||||
return nil, err
|
return nil, err
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -16,7 +16,7 @@ type inputSendVerify struct {
|
|||||||
func NewUserSendVerifyEmailHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc {
|
func NewUserSendVerifyEmailHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc {
|
||||||
return httpserver.WrapGin(log,
|
return httpserver.WrapGin(log,
|
||||||
func(ctx context.Context, input inputSendVerify) (interface{}, error) {
|
func(ctx context.Context, input inputSendVerify) (interface{}, error) {
|
||||||
err := userService.SendEmailVerifyUser(ctx, input.Email)
|
err := userService.RequestVerifyUser(ctx, input.Email)
|
||||||
return nil, err
|
return nil, err
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,13 +2,11 @@ package models
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type ActionTokenTarget int
|
type ActionTokenTarget string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
_ ActionTokenTarget = iota
|
ActionTokenTargetRestorePassword ActionTokenTarget = "restore"
|
||||||
ActionTokenTargetForgotPassword
|
ActionTokenTargetVerifyEmail ActionTokenTarget = "verify"
|
||||||
ActionTokenTargetLogin2FA
|
|
||||||
ActionTokenVerifyEmail
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ActionTokenDTO struct {
|
type ActionTokenDTO struct {
|
||||||
|
|||||||
@ -5,10 +5,10 @@ type UserDTO struct {
|
|||||||
Email string
|
Email string
|
||||||
EmailVerified bool
|
EmailVerified bool
|
||||||
Secret string
|
Secret string
|
||||||
Name string
|
FullName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserUpdateDTO struct {
|
type UserUpdateDTO struct {
|
||||||
Secret string
|
Secret string
|
||||||
Name string
|
FullName string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ type actionTokenRepo struct {
|
|||||||
func (a *actionTokenRepo) CreateActionToken(ctx context.Context, dto models.ActionTokenDTO) (*models.ActionTokenDTO, error) {
|
func (a *actionTokenRepo) CreateActionToken(ctx context.Context, dto models.ActionTokenDTO) (*models.ActionTokenDTO, error) {
|
||||||
query := `
|
query := `
|
||||||
insert into
|
insert into
|
||||||
action_tokens (user_id, value, target, expiration)
|
action_tokens (user_id, value, target, expires_at)
|
||||||
values ($1, $2, $3, $4)
|
values ($1, $2, $3, $4)
|
||||||
returning id;`
|
returning id;`
|
||||||
row := a.db.QueryRowContext(ctx, query, dto.UserId, dto.Value, dto.Target, dto.Expiration)
|
row := a.db.QueryRowContext(ctx, query, dto.UserId, dto.Value, dto.Target, dto.Expiration)
|
||||||
@ -51,7 +51,7 @@ func (a *actionTokenRepo) GetActionToken(ctx context.Context, value string, targ
|
|||||||
select id, user_id from action_tokens
|
select id, user_id from action_tokens
|
||||||
where
|
where
|
||||||
value=$1 and target=$2
|
value=$1 and target=$2
|
||||||
and CURRENT_TIMESTAMP < expiration;`
|
and CURRENT_TIMESTAMP < expires_at;`
|
||||||
row := a.db.QueryRowContext(ctx, query, value, target)
|
row := a.db.QueryRowContext(ctx, query, value, target)
|
||||||
|
|
||||||
err := row.Scan(&dto.Id, &dto.UserId)
|
err := row.Scan(&dto.Id, &dto.UserId)
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import (
|
|||||||
type ShortlinkDTO struct {
|
type ShortlinkDTO struct {
|
||||||
Id string
|
Id string
|
||||||
Url string
|
Url string
|
||||||
Expiration time.Time
|
ExpiresAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShortlinkRepo interface {
|
type ShortlinkRepo interface {
|
||||||
@ -35,8 +35,8 @@ func (u *shortlinkRepo) AddShortlink(ctx context.Context, dto ShortlinkDTO) erro
|
|||||||
_, span := u.tracer.Start(ctx, "postgres::AddShortlink")
|
_, span := u.tracer.Start(ctx, "postgres::AddShortlink")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
query := `insert into shortlinks (id, url, expiration) values ($1, $2, $3);`
|
query := `insert into shortlinks (url, expires_at) values ($1, $2);`
|
||||||
_, err := u.db.ExecContext(ctx, query, dto.Id, dto.Url, dto.Expiration)
|
_, err := u.db.ExecContext(ctx, query, dto.Url, dto.ExpiresAt)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,14 +44,14 @@ func (u *shortlinkRepo) GetShortlink(ctx context.Context, id string) (*Shortlink
|
|||||||
_, span := u.tracer.Start(ctx, "postgres::GetShortlink")
|
_, span := u.tracer.Start(ctx, "postgres::GetShortlink")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
query := `select url, expiration from shortlinks where id = $1;`
|
query := `select url, expires_at from shortlinks where id = $1;`
|
||||||
row := u.db.QueryRowContext(ctx, query, id)
|
row := u.db.QueryRowContext(ctx, query, id)
|
||||||
if err := row.Err(); err != nil {
|
if err := row.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dto := &ShortlinkDTO{Id: id}
|
dto := &ShortlinkDTO{Id: id}
|
||||||
err := row.Scan(&dto.Url, &dto.Expiration)
|
err := row.Scan(&dto.Url, &dto.ExpiresAt)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return dto, nil
|
return dto, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,16 +10,10 @@ import (
|
|||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
// type userDAO struct {
|
|
||||||
// Id string `json:"id"`
|
|
||||||
// Login string `json:"login"`
|
|
||||||
// Secret string `json:"secret"`
|
|
||||||
// Name string `json:"name"`
|
|
||||||
// }
|
|
||||||
|
|
||||||
type UserRepo interface {
|
type UserRepo interface {
|
||||||
CreateUser(ctx context.Context, dto models.UserDTO) (*models.UserDTO, error)
|
CreateUser(ctx context.Context, dto models.UserDTO) (*models.UserDTO, error)
|
||||||
UpdateUser(ctx context.Context, userId string, dto models.UserUpdateDTO) error
|
UpdateUser(ctx context.Context, userId string, dto models.UserUpdateDTO) error
|
||||||
|
DeactivateUser(ctx context.Context, userId string) error
|
||||||
SetUserEmailVerified(ctx context.Context, userId string) error
|
SetUserEmailVerified(ctx context.Context, userId string) error
|
||||||
GetUserById(ctx context.Context, id string) (*models.UserDTO, error)
|
GetUserById(ctx context.Context, id string) (*models.UserDTO, error)
|
||||||
GetUserByEmail(ctx context.Context, login string) (*models.UserDTO, error)
|
GetUserByEmail(ctx context.Context, login string) (*models.UserDTO, error)
|
||||||
@ -38,8 +32,8 @@ func (u *userRepo) CreateUser(ctx context.Context, dto models.UserDTO) (*models.
|
|||||||
_, span := u.tracer.Start(ctx, "postgres::CreateUser")
|
_, span := u.tracer.Start(ctx, "postgres::CreateUser")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
query := `insert into users (email, secret, name) values ($1, $2, $3) returning id;`
|
query := `insert into users (email, secret, full_name) values ($1, $2, $3) returning id;`
|
||||||
row := u.db.QueryRowContext(ctx, query, dto.Email, dto.Secret, dto.Name)
|
row := u.db.QueryRowContext(ctx, query, dto.Email, dto.Secret, dto.FullName)
|
||||||
|
|
||||||
id := ""
|
id := ""
|
||||||
if err := row.Scan(&id); err != nil {
|
if err := row.Scan(&id); err != nil {
|
||||||
@ -50,7 +44,7 @@ func (u *userRepo) CreateUser(ctx context.Context, dto models.UserDTO) (*models.
|
|||||||
Id: id,
|
Id: id,
|
||||||
Email: dto.Email,
|
Email: dto.Email,
|
||||||
Secret: dto.Secret,
|
Secret: dto.Secret,
|
||||||
Name: dto.Name,
|
FullName: dto.FullName,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,8 +52,21 @@ func (u *userRepo) UpdateUser(ctx context.Context, userId string, dto models.Use
|
|||||||
_, span := u.tracer.Start(ctx, "postgres::UpdateUser")
|
_, span := u.tracer.Start(ctx, "postgres::UpdateUser")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
query := `update users set secret=$1, name=$2 where id = $3;`
|
query := `update users set secret=$1, full_name=$2 where id = $3;`
|
||||||
_, err := u.db.ExecContext(ctx, query, dto.Secret, dto.Name, userId)
|
_, err := u.db.ExecContext(ctx, query, dto.Secret, dto.FullName, userId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userRepo) DeactivateUser(ctx context.Context, userId string) error {
|
||||||
|
_, span := u.tracer.Start(ctx, "postgres::DeactivateUser")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
query := `update users set active=false where id = $1;`
|
||||||
|
_, err := u.db.ExecContext(ctx, query, userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -84,11 +91,13 @@ func (u *userRepo) GetUserById(ctx context.Context, id string) (*models.UserDTO,
|
|||||||
_, span := u.tracer.Start(ctx, "postgres::GetUserById")
|
_, span := u.tracer.Start(ctx, "postgres::GetUserById")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
query := `select id, email, secret, name, email_verified from users where id = $1;`
|
query := `
|
||||||
|
select id, email, secret, full_name, email_verified
|
||||||
|
from users where id = $1 and active;`
|
||||||
row := u.db.QueryRowContext(ctx, query, id)
|
row := u.db.QueryRowContext(ctx, query, id)
|
||||||
|
|
||||||
dto := &models.UserDTO{}
|
dto := &models.UserDTO{}
|
||||||
err := row.Scan(&dto.Id, &dto.Email, &dto.Secret, &dto.Name, &dto.EmailVerified)
|
err := row.Scan(&dto.Id, &dto.Email, &dto.Secret, &dto.FullName, &dto.EmailVerified)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return dto, nil
|
return dto, nil
|
||||||
}
|
}
|
||||||
@ -103,11 +112,12 @@ func (u *userRepo) GetUserByEmail(ctx context.Context, login string) (*models.Us
|
|||||||
_, span := u.tracer.Start(ctx, "postgres::GetUserByEmail")
|
_, span := u.tracer.Start(ctx, "postgres::GetUserByEmail")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
query := `select id, email, secret, name, email_verified from users where email = $1;`
|
query := `select id, email, secret, full_name, email_verified
|
||||||
|
from users where email = $1 and active;`
|
||||||
row := u.db.QueryRowContext(ctx, query, login)
|
row := u.db.QueryRowContext(ctx, query, login)
|
||||||
|
|
||||||
dto := &models.UserDTO{}
|
dto := &models.UserDTO{}
|
||||||
err := row.Scan(&dto.Id, &dto.Email, &dto.Secret, &dto.Name, &dto.EmailVerified)
|
err := row.Scan(&dto.Id, &dto.Email, &dto.Secret, &dto.FullName, &dto.EmailVerified)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return dto, nil
|
return dto, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,7 +50,7 @@ func (s *shortlinkService) CreateShortlink(ctx context.Context, url string) (str
|
|||||||
dto := repos.ShortlinkDTO{
|
dto := repos.ShortlinkDTO{
|
||||||
Id: id,
|
Id: id,
|
||||||
Url: url,
|
Url: url,
|
||||||
Expiration: expiration,
|
ExpiresAt: expiration,
|
||||||
}
|
}
|
||||||
if err := s.repo.AddShortlink(ctx, dto); err != nil {
|
if err := s.repo.AddShortlink(ctx, dto); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -73,7 +73,7 @@ func (s *shortlinkService) GetShortlink(ctx context.Context, id string) (string,
|
|||||||
if link == nil {
|
if link == nil {
|
||||||
return "", ErrShortlinkNotexist
|
return "", ErrShortlinkNotexist
|
||||||
}
|
}
|
||||||
if time.Now().After(link.Expiration) {
|
if time.Now().After(link.ExpiresAt) {
|
||||||
return "", ErrShortlinkExpired
|
return "", ErrShortlinkExpired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"backend/pkg/logger"
|
"backend/pkg/logger"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -31,13 +32,16 @@ type UserService interface {
|
|||||||
CreateUser(ctx context.Context, params UserCreateParams) (*models.UserDTO, error)
|
CreateUser(ctx context.Context, params UserCreateParams) (*models.UserDTO, error)
|
||||||
AuthenticateUser(ctx context.Context, login, password string) (string, error)
|
AuthenticateUser(ctx context.Context, login, password string) (string, error)
|
||||||
ValidateAuthToken(ctx context.Context, tokenStr string) (*models.UserDTO, error)
|
ValidateAuthToken(ctx context.Context, tokenStr string) (*models.UserDTO, error)
|
||||||
|
|
||||||
|
// TODO: implement user deactivation flow
|
||||||
|
// DeactivateUser(ctx context.Context, userId string) error
|
||||||
|
|
||||||
VerifyEmail(ctx context.Context, actionToken string) 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
|
ChangePassword(ctx context.Context, userId, oldPassword, newPassword string) error
|
||||||
ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error
|
ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error
|
||||||
|
|
||||||
|
RequestRestorePassword(ctx context.Context, email string) error
|
||||||
|
RequestVerifyUser(ctx context.Context, email string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserService(deps UserServiceDeps) UserService {
|
func NewUserService(deps UserServiceDeps) UserService {
|
||||||
@ -50,6 +54,7 @@ type UserServiceDeps struct {
|
|||||||
UserRepo repos.UserRepo
|
UserRepo repos.UserRepo
|
||||||
UserCache cache.Cache[string, models.UserDTO]
|
UserCache cache.Cache[string, models.UserDTO]
|
||||||
JwtCache cache.Cache[string, string]
|
JwtCache cache.Cache[string, string]
|
||||||
|
LoginAttemptsCache cache.Cache[string, int]
|
||||||
EventRepo repos.EventRepo
|
EventRepo repos.EventRepo
|
||||||
ActionTokenRepo repos.ActionTokenRepo
|
ActionTokenRepo repos.ActionTokenRepo
|
||||||
Logger logger.Logger
|
Logger logger.Logger
|
||||||
@ -84,9 +89,9 @@ func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
user := models.UserDTO{
|
user := models.UserDTO{
|
||||||
Email: params.Email,
|
Email: strings.ToLower(params.Email),
|
||||||
Secret: string(secret),
|
Secret: string(secret),
|
||||||
Name: params.Name,
|
FullName: params.Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.deps.UserRepo.CreateUser(ctx, user)
|
result, err := u.deps.UserRepo.CreateUser(ctx, user)
|
||||||
@ -104,6 +109,22 @@ func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *userService) AuthenticateUser(ctx context.Context, email, password string) (string, error) {
|
func (u *userService) AuthenticateUser(ctx context.Context, email, password string) (string, error) {
|
||||||
|
attempts, ok := u.deps.LoginAttemptsCache.Get(email)
|
||||||
|
if ok && attempts >= 4 {
|
||||||
|
return "", fmt.Errorf("too many bad login attempts")
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := u.authenticateUser(ctx, email, password)
|
||||||
|
if err != nil {
|
||||||
|
u.deps.LoginAttemptsCache.Set(email, attempts+1, cache.Expiration{Ttl: 30 * time.Second})
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.deps.LoginAttemptsCache.Del(email)
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *userService) authenticateUser(ctx context.Context, email, password string) (string, error) {
|
||||||
user, err := u.deps.UserRepo.GetUserByEmail(ctx, email)
|
user, err := u.deps.UserRepo.GetUserByEmail(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -131,8 +152,18 @@ func (u *userService) AuthenticateUser(ctx context.Context, email, password stri
|
|||||||
return jwt, nil
|
return jwt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *userService) DeactivateUser(ctx context.Context, userId string) error {
|
||||||
|
err := u.deps.UserRepo.DeactivateUser(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.deps.UserCache.Del(userId)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *userService) VerifyEmail(ctx context.Context, actionToken string) error {
|
func (u *userService) VerifyEmail(ctx context.Context, actionToken string) error {
|
||||||
token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenVerifyEmail)
|
token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenTargetVerifyEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -149,64 +180,8 @@ func (u *userService) VerifyEmail(ctx context.Context, actionToken string) error
|
|||||||
return nil
|
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 {
|
func (u *userService) ChangePasswordWithToken(ctx context.Context, actionToken, newPassword string) error {
|
||||||
token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenTargetForgotPassword)
|
token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenTargetRestorePassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -258,7 +233,7 @@ func (u *userService) updatePassword(ctx context.Context, user models.UserDTO, n
|
|||||||
|
|
||||||
if err = u.deps.UserRepo.UpdateUser(ctx, user.Id, models.UserUpdateDTO{
|
if err = u.deps.UserRepo.UpdateUser(ctx, user.Id, models.UserUpdateDTO{
|
||||||
Secret: newSecret,
|
Secret: newSecret,
|
||||||
Name: user.Name,
|
FullName: user.FullName,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -307,3 +282,57 @@ func (u *userService) ValidateAuthToken(ctx context.Context, tokenStr string) (*
|
|||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *userService) RequestRestorePassword(ctx context.Context, email string) error {
|
||||||
|
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.ActionTokenTargetRestorePassword,
|
||||||
|
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) RequestVerifyUser(ctx context.Context, email string) error {
|
||||||
|
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) 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.ActionTokenTargetVerifyEmail,
|
||||||
|
Expiration: time.Now().Add(1 * time.Hour),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.deps.EventRepo.SendEmailVerifyUser(ctx, email, actionToken.Value)
|
||||||
|
}
|
||||||
|
|||||||
18
sql/00_common.sql
Normal file
18
sql/00_common.sql
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
create or replace function trg_proc_row_updated()
|
||||||
|
returns trigger as $$
|
||||||
|
begin
|
||||||
|
if new is distinct from old then
|
||||||
|
new.updated_at = now();
|
||||||
|
end if;
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
create or replace function trg_proc_row_created()
|
||||||
|
returns trigger as $$
|
||||||
|
begin
|
||||||
|
new.created_at = now();
|
||||||
|
new.updated_at = now();
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
@ -1,11 +1,25 @@
|
|||||||
create table if not exists users (
|
create table if not exists users (
|
||||||
id int generated always as identity,
|
id integer primary key generated always as identity,
|
||||||
email text unique not null,
|
email varchar(256) unique not null,
|
||||||
secret text not null,
|
secret varchar(256) not null,
|
||||||
name text not null,
|
full_name varchar(256) not null,
|
||||||
email_verified boolean not null default false,
|
email_verified boolean not null default false,
|
||||||
|
active boolean default true,
|
||||||
primary key (id)
|
created_at timestamp,
|
||||||
|
updated_at timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
create index if not exists users_email_idx on users(email);
|
alter table users alter column active set default true;
|
||||||
|
|
||||||
|
create index if not exists idx_users_email on users(email);
|
||||||
|
|
||||||
|
create or replace trigger trg_user_created
|
||||||
|
before insert on users
|
||||||
|
for each row
|
||||||
|
execute function trg_proc_row_created();
|
||||||
|
|
||||||
|
create or replace trigger trg_user_updated
|
||||||
|
before update on users
|
||||||
|
for each row
|
||||||
|
when(new is distinct from old)
|
||||||
|
execute function trg_proc_row_updated();
|
||||||
@ -1,5 +1,18 @@
|
|||||||
create table if not exists shortlinks (
|
create table if not exists shortlinks (
|
||||||
id text primary key,
|
id int generated always as identity,
|
||||||
url text,
|
url text not null,
|
||||||
expiration date
|
expires_at timestamp not null,
|
||||||
|
created_at timestamp,
|
||||||
|
updated_at timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create or replace trigger trg_shortlink_created
|
||||||
|
before insert on shortlinks
|
||||||
|
for each row
|
||||||
|
execute function trg_proc_row_created();
|
||||||
|
|
||||||
|
create or replace trigger trg_shortlink_updated
|
||||||
|
before update on shortlinks
|
||||||
|
for each row
|
||||||
|
when (new is distinct from old)
|
||||||
|
execute function trg_proc_row_updated();
|
||||||
@ -1,9 +1,25 @@
|
|||||||
create table if not exists action_tokens (
|
create table if not exists action_tokens (
|
||||||
id int generated always as identity,
|
id int generated always as identity,
|
||||||
user_id int,
|
user_id int references users(id),
|
||||||
value text,
|
value text not null,
|
||||||
target int,
|
target text not null,
|
||||||
expiration timestamp,
|
expires_at timestamp not null,
|
||||||
|
created_at timestamp,
|
||||||
|
updated_at timestamp,
|
||||||
|
|
||||||
primary key(id)
|
constraint pk_action_tokens_id primary key(id),
|
||||||
|
constraint chk_action_tokens_target check(target in ('verify', 'restore'))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create index if not exists idx_action_tokens_value on action_tokens(value);
|
||||||
|
|
||||||
|
create or replace trigger trg_action_token_created
|
||||||
|
before insert on action_tokens
|
||||||
|
for each row
|
||||||
|
execute function trg_proc_row_created();
|
||||||
|
|
||||||
|
create or replace trigger trg_action_token_updated
|
||||||
|
before update on action_tokens
|
||||||
|
for each row
|
||||||
|
when (new is distinct from old)
|
||||||
|
execute function trg_proc_row_updated();
|
||||||
Loading…
x
Reference in New Issue
Block a user