commit
c4c8f0c6ea
@ -122,9 +122,10 @@ func (a *App) Run(p RunParams) {
|
||||
shortlinkRepo = repos.NewShortlinkRepo(sqlDb, tracer)
|
||||
eventRepo = repos.NewEventRepo(kafka)
|
||||
|
||||
userCache = cache.NewCacheInmemSharded[models.UserDTO](cache.ShardingTypeInteger)
|
||||
jwtCache = cache.NewCacheInmemSharded[string](cache.ShardingTypeJWT)
|
||||
linksCache = cache.NewCacheInmem[string, string]()
|
||||
userCache = cache.NewCacheInmemSharded[models.UserDTO](cache.ShardingTypeInteger)
|
||||
loginAttemptsCache = cache.NewCacheInmem[string, int]()
|
||||
jwtCache = cache.NewCacheInmemSharded[string](cache.ShardingTypeJWT)
|
||||
linksCache = cache.NewCacheInmem[string, string]()
|
||||
)
|
||||
|
||||
// Periodically trigger cache cleanup
|
||||
@ -140,20 +141,22 @@ func (a *App) Run(p RunParams) {
|
||||
userCache.CheckExpired()
|
||||
jwtCache.CheckExpired()
|
||||
linksCache.CheckExpired()
|
||||
loginAttemptsCache.CheckExpired()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
userService = services.NewUserService(
|
||||
services.UserServiceDeps{
|
||||
Jwt: jwtUtil,
|
||||
Password: passwordUtil,
|
||||
UserRepo: userRepo,
|
||||
UserCache: userCache,
|
||||
JwtCache: jwtCache,
|
||||
EventRepo: *eventRepo,
|
||||
ActionTokenRepo: actionTokenRepo,
|
||||
Logger: logger,
|
||||
Jwt: jwtUtil,
|
||||
Password: passwordUtil,
|
||||
UserRepo: userRepo,
|
||||
UserCache: userCache,
|
||||
LoginAttemptsCache: loginAttemptsCache,
|
||||
JwtCache: jwtCache,
|
||||
EventRepo: *eventRepo,
|
||||
ActionTokenRepo: actionTokenRepo,
|
||||
Logger: logger,
|
||||
},
|
||||
)
|
||||
shortlinkService = services.NewShortlinkSevice(
|
||||
|
||||
@ -16,9 +16,9 @@ type createUserInput struct {
|
||||
}
|
||||
|
||||
type createUserOutput struct {
|
||||
Id string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Id string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"fullName"`
|
||||
}
|
||||
|
||||
func NewUserCreateHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc {
|
||||
@ -37,9 +37,9 @@ func NewUserCreateHandler(log logger.Logger, userService services.UserService) g
|
||||
}
|
||||
|
||||
return createUserOutput{
|
||||
Id: user.Id,
|
||||
Email: user.Email,
|
||||
Name: user.Name,
|
||||
Id: user.Id,
|
||||
Email: user.Email,
|
||||
FullName: user.FullName,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
|
||||
@ -16,7 +16,7 @@ type inputSendRestorePassword struct {
|
||||
func NewUserSendRestorePasswordHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc {
|
||||
return httpserver.WrapGin(log,
|
||||
func(ctx context.Context, input inputSendRestorePassword) (interface{}, error) {
|
||||
err := userService.SendEmailForgotPassword(ctx, input.Email)
|
||||
err := userService.RequestRestorePassword(ctx, input.Email)
|
||||
return nil, err
|
||||
},
|
||||
)
|
||||
|
||||
@ -16,7 +16,7 @@ type inputSendVerify struct {
|
||||
func NewUserSendVerifyEmailHandler(log logger.Logger, userService services.UserService) gin.HandlerFunc {
|
||||
return httpserver.WrapGin(log,
|
||||
func(ctx context.Context, input inputSendVerify) (interface{}, error) {
|
||||
err := userService.SendEmailVerifyUser(ctx, input.Email)
|
||||
err := userService.RequestVerifyUser(ctx, input.Email)
|
||||
return nil, err
|
||||
},
|
||||
)
|
||||
|
||||
@ -2,13 +2,11 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type ActionTokenTarget int
|
||||
type ActionTokenTarget string
|
||||
|
||||
const (
|
||||
_ ActionTokenTarget = iota
|
||||
ActionTokenTargetForgotPassword
|
||||
ActionTokenTargetLogin2FA
|
||||
ActionTokenVerifyEmail
|
||||
ActionTokenTargetRestorePassword ActionTokenTarget = "restore"
|
||||
ActionTokenTargetVerifyEmail ActionTokenTarget = "verify"
|
||||
)
|
||||
|
||||
type ActionTokenDTO struct {
|
||||
|
||||
@ -5,10 +5,10 @@ type UserDTO struct {
|
||||
Email string
|
||||
EmailVerified bool
|
||||
Secret string
|
||||
Name string
|
||||
FullName string
|
||||
}
|
||||
|
||||
type UserUpdateDTO struct {
|
||||
Secret string
|
||||
Name string
|
||||
Secret string
|
||||
FullName string
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ type actionTokenRepo struct {
|
||||
func (a *actionTokenRepo) CreateActionToken(ctx context.Context, dto models.ActionTokenDTO) (*models.ActionTokenDTO, error) {
|
||||
query := `
|
||||
insert into
|
||||
action_tokens (user_id, value, target, expiration)
|
||||
action_tokens (user_id, value, target, expires_at)
|
||||
values ($1, $2, $3, $4)
|
||||
returning id;`
|
||||
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
|
||||
where
|
||||
value=$1 and target=$2
|
||||
and CURRENT_TIMESTAMP < expiration;`
|
||||
and CURRENT_TIMESTAMP < expires_at;`
|
||||
row := a.db.QueryRowContext(ctx, query, value, target)
|
||||
|
||||
err := row.Scan(&dto.Id, &dto.UserId)
|
||||
|
||||
@ -11,9 +11,9 @@ import (
|
||||
)
|
||||
|
||||
type ShortlinkDTO struct {
|
||||
Id string
|
||||
Url string
|
||||
Expiration time.Time
|
||||
Id string
|
||||
Url string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
type ShortlinkRepo interface {
|
||||
@ -35,8 +35,8 @@ func (u *shortlinkRepo) AddShortlink(ctx context.Context, dto ShortlinkDTO) erro
|
||||
_, span := u.tracer.Start(ctx, "postgres::AddShortlink")
|
||||
defer span.End()
|
||||
|
||||
query := `insert into shortlinks (id, url, expiration) values ($1, $2, $3);`
|
||||
_, err := u.db.ExecContext(ctx, query, dto.Id, dto.Url, dto.Expiration)
|
||||
query := `insert into shortlinks (url, expires_at) values ($1, $2);`
|
||||
_, err := u.db.ExecContext(ctx, query, dto.Url, dto.ExpiresAt)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -44,14 +44,14 @@ func (u *shortlinkRepo) GetShortlink(ctx context.Context, id string) (*Shortlink
|
||||
_, span := u.tracer.Start(ctx, "postgres::GetShortlink")
|
||||
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)
|
||||
if err := row.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dto := &ShortlinkDTO{Id: id}
|
||||
err := row.Scan(&dto.Url, &dto.Expiration)
|
||||
err := row.Scan(&dto.Url, &dto.ExpiresAt)
|
||||
if err == nil {
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
@ -10,16 +10,10 @@ import (
|
||||
"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 {
|
||||
CreateUser(ctx context.Context, dto models.UserDTO) (*models.UserDTO, 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
|
||||
GetUserById(ctx context.Context, id 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")
|
||||
defer span.End()
|
||||
|
||||
query := `insert into users (email, secret, name) values ($1, $2, $3) returning id;`
|
||||
row := u.db.QueryRowContext(ctx, query, dto.Email, dto.Secret, dto.Name)
|
||||
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.FullName)
|
||||
|
||||
id := ""
|
||||
if err := row.Scan(&id); err != nil {
|
||||
@ -47,10 +41,10 @@ func (u *userRepo) CreateUser(ctx context.Context, dto models.UserDTO) (*models.
|
||||
}
|
||||
|
||||
return &models.UserDTO{
|
||||
Id: id,
|
||||
Email: dto.Email,
|
||||
Secret: dto.Secret,
|
||||
Name: dto.Name,
|
||||
Id: id,
|
||||
Email: dto.Email,
|
||||
Secret: dto.Secret,
|
||||
FullName: dto.FullName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -58,8 +52,21 @@ func (u *userRepo) UpdateUser(ctx context.Context, userId string, dto models.Use
|
||||
_, span := u.tracer.Start(ctx, "postgres::UpdateUser")
|
||||
defer span.End()
|
||||
|
||||
query := `update users set secret=$1, name=$2 where id = $3;`
|
||||
_, err := u.db.ExecContext(ctx, query, dto.Secret, dto.Name, userId)
|
||||
query := `update users set secret=$1, full_name=$2 where id = $3;`
|
||||
_, 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 {
|
||||
return err
|
||||
}
|
||||
@ -84,11 +91,13 @@ func (u *userRepo) GetUserById(ctx context.Context, id string) (*models.UserDTO,
|
||||
_, span := u.tracer.Start(ctx, "postgres::GetUserById")
|
||||
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)
|
||||
|
||||
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 {
|
||||
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")
|
||||
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)
|
||||
|
||||
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 {
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
@ -48,9 +48,9 @@ func (s *shortlinkService) CreateShortlink(ctx context.Context, url string) (str
|
||||
expiration := time.Now().Add(7 * 24 * time.Hour)
|
||||
|
||||
dto := repos.ShortlinkDTO{
|
||||
Id: id,
|
||||
Url: url,
|
||||
Expiration: expiration,
|
||||
Id: id,
|
||||
Url: url,
|
||||
ExpiresAt: expiration,
|
||||
}
|
||||
if err := s.repo.AddShortlink(ctx, dto); err != nil {
|
||||
return "", err
|
||||
@ -73,7 +73,7 @@ func (s *shortlinkService) GetShortlink(ctx context.Context, id string) (string,
|
||||
if link == nil {
|
||||
return "", ErrShortlinkNotexist
|
||||
}
|
||||
if time.Now().After(link.Expiration) {
|
||||
if time.Now().After(link.ExpiresAt) {
|
||||
return "", ErrShortlinkExpired
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"backend/pkg/logger"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -31,13 +32,16 @@ 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)
|
||||
|
||||
// TODO: implement user deactivation flow
|
||||
// DeactivateUser(ctx context.Context, userId 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
|
||||
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 {
|
||||
@ -45,14 +49,15 @@ func NewUserService(deps UserServiceDeps) UserService {
|
||||
}
|
||||
|
||||
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
|
||||
Jwt utils.JwtUtil
|
||||
Password utils.PasswordUtil
|
||||
UserRepo repos.UserRepo
|
||||
UserCache cache.Cache[string, models.UserDTO]
|
||||
JwtCache cache.Cache[string, string]
|
||||
LoginAttemptsCache cache.Cache[string, int]
|
||||
EventRepo repos.EventRepo
|
||||
ActionTokenRepo repos.ActionTokenRepo
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
@ -84,9 +89,9 @@ func (u *userService) CreateUser(ctx context.Context, params UserCreateParams) (
|
||||
}
|
||||
|
||||
user := models.UserDTO{
|
||||
Email: params.Email,
|
||||
Secret: string(secret),
|
||||
Name: params.Name,
|
||||
Email: strings.ToLower(params.Email),
|
||||
Secret: string(secret),
|
||||
FullName: params.Name,
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -131,8 +152,18 @@ func (u *userService) AuthenticateUser(ctx context.Context, email, password stri
|
||||
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 {
|
||||
token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenVerifyEmail)
|
||||
token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenTargetVerifyEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -149,64 +180,8 @@ func (u *userService) VerifyEmail(ctx context.Context, actionToken string) error
|
||||
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)
|
||||
token, err := u.deps.ActionTokenRepo.GetActionToken(ctx, actionToken, models.ActionTokenTargetRestorePassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -257,8 +232,8 @@ func (u *userService) updatePassword(ctx context.Context, user models.UserDTO, n
|
||||
}
|
||||
|
||||
if err = u.deps.UserRepo.UpdateUser(ctx, user.Id, models.UserUpdateDTO{
|
||||
Secret: newSecret,
|
||||
Name: user.Name,
|
||||
Secret: newSecret,
|
||||
FullName: user.FullName,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -307,3 +282,57 @@ func (u *userService) ValidateAuthToken(ctx context.Context, tokenStr string) (*
|
||||
|
||||
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 (
|
||||
id int generated always as identity,
|
||||
email text unique not null,
|
||||
secret text not null,
|
||||
name text not null,
|
||||
id integer primary key generated always as identity,
|
||||
email varchar(256) unique not null,
|
||||
secret varchar(256) not null,
|
||||
full_name varchar(256) not null,
|
||||
email_verified boolean not null default false,
|
||||
|
||||
primary key (id)
|
||||
active boolean default true,
|
||||
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 (
|
||||
id text primary key,
|
||||
url text,
|
||||
expiration date
|
||||
id int generated always as identity,
|
||||
url text not null,
|
||||
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 (
|
||||
id int generated always as identity,
|
||||
user_id int,
|
||||
value text,
|
||||
target int,
|
||||
expiration timestamp,
|
||||
user_id int references users(id),
|
||||
value text not null,
|
||||
target text not null,
|
||||
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