move handlers to separate package
This commit is contained in:
parent
ef006abd10
commit
d1a33b18dc
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
|
@ -43,7 +44,7 @@ func main() {
|
|||
store := repository.NewStore(db)
|
||||
v := customvalidator.NewCustomValidator(validator.New())
|
||||
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
|
||||
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, httpserver.JwtConfig{
|
||||
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
|
||||
JwtAccessKey: cfg.JwtKey,
|
||||
JwtAccessExpiry: cfg.AccessExpiry,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,14 +2,20 @@ CREATE TABLE users (
|
|||
id BIGSERIAL PRIMARY KEY,
|
||||
first_name VARCHAR(255) NOT NULL,
|
||||
last_name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
phone_number VARCHAR(20) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE ,
|
||||
phone_number VARCHAR(20) UNIQUE,
|
||||
password BYTEA NOT NULL,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ ,
|
||||
updated_at TIMESTAMPTZ ,
|
||||
CONSTRAINT unique_email_phone UNIQUE (email, phone_number)
|
||||
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
suspended_at TIMESTAMPTZ NULL, -- this can be NULL if the user is not suspended
|
||||
suspended BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
CHECK (
|
||||
(email IS NOT NULL AND phone_number IS NULL) OR
|
||||
(email IS NULL AND phone_number IS NOT NULL)
|
||||
)
|
||||
);
|
||||
CREATE TABLE refresh_tokens (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
|
|
|||
10
internal/domain/common.go
Normal file
10
internal/domain/common.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package domain
|
||||
|
||||
type ValidString struct {
|
||||
Value string
|
||||
Valid bool
|
||||
}
|
||||
type ValidBool struct {
|
||||
Value bool
|
||||
Valid bool
|
||||
}
|
||||
29
internal/domain/otp.go
Normal file
29
internal/domain/otp.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type OtpFor string
|
||||
|
||||
const (
|
||||
OtpReset OtpFor = "reset"
|
||||
OtpRegister OtpFor = "register"
|
||||
)
|
||||
|
||||
type OtpMedium string
|
||||
|
||||
const (
|
||||
OtpMediumEmail OtpMedium = "email"
|
||||
OtpMediumSms OtpMedium = "sms"
|
||||
)
|
||||
|
||||
type Otp struct {
|
||||
ID int64
|
||||
SentTo string
|
||||
Medium OtpMedium
|
||||
For OtpFor
|
||||
Otp string
|
||||
Used bool
|
||||
UsedAt time.Time
|
||||
CreatedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
11
internal/domain/role.go
Normal file
11
internal/domain/role.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package domain
|
||||
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleAdmin Role = "admin"
|
||||
RoleCustomer Role = "customer"
|
||||
RoleSuperAdmin Role = "super_admin"
|
||||
RoleBranchManager Role = "branch_manager"
|
||||
RoleCashier Role = "cashier"
|
||||
)
|
||||
|
|
@ -9,8 +9,38 @@ type User struct {
|
|||
Email string
|
||||
PhoneNumber string
|
||||
Password []byte
|
||||
Role string
|
||||
Verified bool
|
||||
Role Role
|
||||
//
|
||||
EmailVerified bool
|
||||
PhoneVerified bool
|
||||
//
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
//
|
||||
SuspendedAt time.Time
|
||||
Suspended bool
|
||||
}
|
||||
type RegisterUserReq struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
PhoneNumber string
|
||||
Password string
|
||||
//Role string
|
||||
Otp string
|
||||
ReferalCode string
|
||||
//
|
||||
OtpMedium OtpMedium
|
||||
}
|
||||
type ResetPasswordReq struct {
|
||||
Email string
|
||||
PhoneNumber string
|
||||
Password string
|
||||
Otp string
|
||||
OtpMedium OtpMedium
|
||||
}
|
||||
type UpdateUserReq struct {
|
||||
FirstName ValidString
|
||||
LastName ValidString
|
||||
Suspended ValidBool
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ func (s *Store) GetUserByEmailPhone(ctx context.Context, email, phone string) (d
|
|||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Password: user.Password,
|
||||
Role: user.Role,
|
||||
Role: domain.Role(user.Role),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func (s *Store) CreateUser(ctx context.Context, firstName, lastName, email, phon
|
|||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Password: user.Password,
|
||||
Role: user.Role,
|
||||
// Role: user.Role,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error)
|
|||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Password: user.Password,
|
||||
Role: user.Role,
|
||||
// Role: user.Role,
|
||||
}, nil
|
||||
}
|
||||
func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||
|
|
@ -59,7 +59,7 @@ func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
|||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Password: user.Password,
|
||||
Role: user.Role,
|
||||
// Role: user.Role,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ var (
|
|||
|
||||
type LoginSuccess struct {
|
||||
UserId int64
|
||||
Role string
|
||||
Role domain.Role
|
||||
RfToken string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,25 @@ import (
|
|||
)
|
||||
|
||||
type UserStore interface {
|
||||
CreateUser(ctx context.Context, CfirstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error)
|
||||
CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error)
|
||||
GetUserByID(ctx context.Context, id int64) (domain.User, error)
|
||||
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||
UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error
|
||||
UpdateUser(ctx context.Context, user domain.UpdateUserReq) error
|
||||
DeleteUser(ctx context.Context, id int64) error
|
||||
CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (domain.User, error)
|
||||
GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error)
|
||||
//
|
||||
//GetUserByEmailPhone(ctx context.Context, emailPhone EmailPhone) (domain.User, error)
|
||||
UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone
|
||||
}
|
||||
type SmsGateway interface {
|
||||
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
|
||||
}
|
||||
type EmailGateway interface {
|
||||
SendEmailOTP(ctx context.Context, email string, otp string) error
|
||||
}
|
||||
type OtpStore interface {
|
||||
CreateOtp(ctx context.Context, otp domain.Otp) error
|
||||
GetOtp(ctx context.Context, sentTo string, sentfor domain.OtpFor, medium domain.OtpMedium) (domain.Otp, error)
|
||||
MarkUsed(ctx context.Context, id int64) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,36 +2,199 @@ package user
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
OtpExpiry = 5 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
ErrOtpAlreadyUsed = errors.New("otp already used")
|
||||
ErrInvalidOtp = errors.New("invalid otp")
|
||||
ErrOtpExpired = errors.New("otp expired")
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
userStore UserStore
|
||||
otpStore OtpStore
|
||||
smsGateway SmsGateway
|
||||
emailGateway EmailGateway
|
||||
}
|
||||
|
||||
func NewService(userStore UserStore, RefreshExpiry int) *Service {
|
||||
func NewService(
|
||||
userStore UserStore, RefreshExpiry int,
|
||||
otpStore OtpStore, smsGateway SmsGateway,
|
||||
emailGateway EmailGateway,
|
||||
) *Service {
|
||||
return &Service{
|
||||
userStore: userStore,
|
||||
otpStore: otpStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) CreateUser(ctx context.Context, firstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error) {
|
||||
return s.userStore.CreateUser(ctx, firstName, lastName, email, phoneNumber, password, role, verified)
|
||||
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
|
||||
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
|
||||
}
|
||||
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error {
|
||||
var err error
|
||||
// check if user exists
|
||||
switch medium {
|
||||
case domain.OtpMediumEmail:
|
||||
_, err = s.userStore.GetUserByEmail(ctx, sentTo)
|
||||
case domain.OtpMediumSms:
|
||||
_, err = s.userStore.GetUserByPhone(ctx, sentTo)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// send otp based on the medium
|
||||
return s.SendOtp(ctx, sentTo, domain.OtpReset, medium)
|
||||
}
|
||||
func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) { // normal
|
||||
// get otp
|
||||
var sentTo string
|
||||
if registerReq.OtpMedium == domain.OtpMediumEmail {
|
||||
sentTo = registerReq.Email
|
||||
} else {
|
||||
sentTo = registerReq.PhoneNumber
|
||||
}
|
||||
//
|
||||
otp, err := s.otpStore.GetOtp(
|
||||
ctx, sentTo,
|
||||
domain.OtpRegister, registerReq.OtpMedium)
|
||||
if err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
// verify otp
|
||||
if otp.Used {
|
||||
return domain.User{}, ErrOtpAlreadyUsed
|
||||
}
|
||||
if time.Now().After(otp.ExpiresAt) {
|
||||
return domain.User{}, ErrOtpExpired
|
||||
}
|
||||
if otp.Otp != registerReq.Otp {
|
||||
return domain.User{}, ErrInvalidOtp
|
||||
}
|
||||
|
||||
hashedPassword, err := hashPassword(registerReq.Password)
|
||||
if err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
userR := domain.User{
|
||||
FirstName: registerReq.FirstName,
|
||||
LastName: registerReq.LastName,
|
||||
Email: registerReq.Email,
|
||||
PhoneNumber: registerReq.PhoneNumber,
|
||||
Password: hashedPassword,
|
||||
Role: "user",
|
||||
EmailVerified: registerReq.OtpMedium == domain.OtpMediumEmail,
|
||||
PhoneVerified: registerReq.OtpMedium == domain.OtpMediumSms,
|
||||
}
|
||||
// create the user and mark otp as used
|
||||
user, err := s.userStore.CreateUser(ctx, userR, otp.ID)
|
||||
if err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error {
|
||||
|
||||
var err error
|
||||
// check if user exists
|
||||
switch medium {
|
||||
case domain.OtpMediumEmail:
|
||||
_, err = s.userStore.GetUserByEmail(ctx, sentTo)
|
||||
case domain.OtpMediumSms:
|
||||
_, err = s.userStore.GetUserByPhone(ctx, sentTo)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.SendOtp(ctx, sentTo, domain.OtpReset, medium)
|
||||
|
||||
}
|
||||
|
||||
func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswordReq) error {
|
||||
var sentTo string
|
||||
if resetReq.OtpMedium == domain.OtpMediumEmail {
|
||||
sentTo = resetReq.Email
|
||||
} else {
|
||||
sentTo = resetReq.PhoneNumber
|
||||
}
|
||||
otp, err := s.otpStore.GetOtp(
|
||||
ctx, sentTo,
|
||||
domain.OtpRegister, resetReq.OtpMedium)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//
|
||||
if otp.Used {
|
||||
return ErrOtpAlreadyUsed
|
||||
}
|
||||
if time.Now().After(otp.ExpiresAt) {
|
||||
return ErrOtpExpired
|
||||
}
|
||||
if otp.Otp != resetReq.Otp {
|
||||
return ErrInvalidOtp
|
||||
}
|
||||
// hash password
|
||||
hashedPassword, err := hashPassword(resetReq.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// reset pass and mark otp as used
|
||||
err = s.userStore.UpdatePassword(ctx, sentTo, hashedPassword, otp.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium) error {
|
||||
otpCode := "123456" // Generate OTP code
|
||||
|
||||
otp := domain.Otp{
|
||||
SentTo: sentTo,
|
||||
Medium: medium,
|
||||
For: otpFor,
|
||||
Otp: otpCode,
|
||||
Used: false,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(OtpExpiry),
|
||||
}
|
||||
|
||||
err := s.otpStore.CreateOtp(ctx, otp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch medium {
|
||||
case domain.OtpMediumSms:
|
||||
return s.smsGateway.SendSMSOTP(ctx, sentTo, otpCode)
|
||||
case domain.OtpMediumEmail:
|
||||
return s.emailGateway.SendEmailOTP(ctx, sentTo, otpCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error {
|
||||
// update user
|
||||
return s.userStore.UpdateUser(ctx, user)
|
||||
|
||||
}
|
||||
func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
|
||||
return s.userStore.GetUserByID(ctx, id)
|
||||
}
|
||||
func (s *Service) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||
return s.userStore.GetAllUsers(ctx)
|
||||
}
|
||||
func (s *Service) UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error {
|
||||
return s.userStore.UpdateUser(ctx, id, firstName, lastName, email, phoneNumber, password, role, verified)
|
||||
}
|
||||
func (s *Service) DeleteUser(ctx context.Context, id int64) error {
|
||||
return s.userStore.DeleteUser(ctx, id)
|
||||
}
|
||||
|
||||
func hashPassword(plaintextPassword string) ([]byte, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -5,29 +5,26 @@ import (
|
|||
"log/slog"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type JwtConfig struct {
|
||||
JwtAccessKey string
|
||||
JwtAccessExpiry int
|
||||
}
|
||||
type App struct {
|
||||
fiber *fiber.App
|
||||
logger *slog.Logger
|
||||
port int
|
||||
authSvc *authentication.Service
|
||||
validator *customvalidator.CustomValidator
|
||||
JwtConfig JwtConfig
|
||||
JwtConfig jwtutil.JwtConfig
|
||||
}
|
||||
|
||||
func NewApp(
|
||||
port int, validator *customvalidator.CustomValidator,
|
||||
authSvc *authentication.Service,
|
||||
logger *slog.Logger,
|
||||
JwtConfig JwtConfig,
|
||||
JwtConfig jwtutil.JwtConfig,
|
||||
) *App {
|
||||
app := fiber.New(fiber.Config{
|
||||
CaseSensitive: true,
|
||||
|
|
|
|||
183
internal/web_server/handlers/auth_handler.go
Normal file
183
internal/web_server/handlers/auth_handler.go
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type loginCustomerReq struct {
|
||||
Email string `json:"email" example:"john.doe@example.com"`
|
||||
PhoneNumber string `json:"phone_number" example:"1234567890"`
|
||||
Password string `json:"password" example:"password123"`
|
||||
}
|
||||
|
||||
type loginCustomerRes struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// LoginCustomer godoc
|
||||
// @Summary Login customer
|
||||
// @Description Login customer
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param login body loginCustomerReq true "Login customer"
|
||||
// @Success 200 {object} loginCustomerRes
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/login [post]
|
||||
func LoginCustomer(
|
||||
logger *slog.Logger, authSvc *authentication.Service,
|
||||
validator *customvalidator.CustomValidator, JwtConfig jwtutil.JwtConfig) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
var req loginCustomerReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
logger.Error("Login failed", "error", err)
|
||||
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
|
||||
}
|
||||
valErrs, ok := validator.Validate(c, req)
|
||||
if !ok {
|
||||
|
||||
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||
return nil
|
||||
}
|
||||
successRes, err := authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password)
|
||||
if err != nil {
|
||||
logger.Info("Login failed", "error", err)
|
||||
if errors.Is(err, authentication.ErrInvalidPassword) {
|
||||
response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid password or not registered", nil, nil)
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, authentication.ErrUserNotFound) {
|
||||
response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid password or not registered", nil, nil)
|
||||
return nil
|
||||
}
|
||||
logger.Error("Login failed", "error", err)
|
||||
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
|
||||
return nil
|
||||
|
||||
}
|
||||
accessToken, err := jwtutil.CreateJwt(strconv.Itoa(int(successRes.UserId)), successRes.Role, JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry)
|
||||
res := loginCustomerRes{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: successRes.RfToken,
|
||||
}
|
||||
return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil)
|
||||
}
|
||||
}
|
||||
|
||||
type refreshToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// RefreshToken godoc
|
||||
// @Summary Refresh token
|
||||
// @Description Refresh token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param refresh body refreshToken true "tokens"
|
||||
// @Success 200 {object} loginCustomerRes
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/refresh [post]
|
||||
func RefreshToken(logger *slog.Logger, authSvc *authentication.Service,
|
||||
validator *customvalidator.CustomValidator, JwtConfig jwtutil.JwtConfig) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
var req refreshToken
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
|
||||
}
|
||||
valErrs, ok := validator.Validate(c, req)
|
||||
if !ok {
|
||||
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||
return nil
|
||||
}
|
||||
rf, err := authSvc.RefreshToken(c.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
logger.Info("Refresh token failed", "error", err)
|
||||
if errors.Is(err, authentication.ErrExpiredToken) {
|
||||
response.WriteJSON(c, fiber.StatusUnauthorized, "The refresh token has expired", nil, nil)
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, authentication.ErrRefreshTokenNotFound) {
|
||||
response.WriteJSON(c, fiber.StatusUnauthorized, "Refresh token not found", nil, nil)
|
||||
return nil
|
||||
}
|
||||
logger.Error("Refresh token failed", "error", err)
|
||||
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
|
||||
return nil
|
||||
}
|
||||
accessToken, err := jwtutil.CreateJwt("", "", JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry)
|
||||
if err != nil {
|
||||
logger.Error("Create jwt failed", "error", err)
|
||||
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
res := loginCustomerRes{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: rf,
|
||||
}
|
||||
return response.WriteJSON(c, fiber.StatusOK, "refresh successful", res, nil)
|
||||
}
|
||||
}
|
||||
|
||||
type logoutReq struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// LogOutCustomer godoc
|
||||
// @Summary Logout customer
|
||||
// @Description Logout customer
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param logout body logoutReq true "Logout customer"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/logout [post]
|
||||
func LogOutCustomer(
|
||||
logger *slog.Logger, authSvc *authentication.Service,
|
||||
validator *customvalidator.CustomValidator) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
var req logoutReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
|
||||
}
|
||||
valErrs, ok := validator.Validate(c, req)
|
||||
if !ok {
|
||||
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||
return nil
|
||||
}
|
||||
err := authSvc.Logout(c.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
logger.Info("Logout failed", "error", err)
|
||||
if errors.Is(err, authentication.ErrExpiredToken) {
|
||||
response.WriteJSON(c, fiber.StatusUnauthorized, "The refresh token has expired", nil, nil)
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, authentication.ErrRefreshTokenNotFound) {
|
||||
response.WriteJSON(c, fiber.StatusUnauthorized, "Refresh token not found", nil, nil)
|
||||
return nil
|
||||
}
|
||||
logger.Error("Logout failed", "error", err)
|
||||
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
|
||||
return nil
|
||||
}
|
||||
return response.WriteJSON(c, fiber.StatusOK, "Logout successful", nil, nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
|
|
@ -20,10 +21,14 @@ var (
|
|||
type UserClaim struct {
|
||||
jwt.RegisteredClaims
|
||||
UserId string
|
||||
Role string
|
||||
Role domain.Role
|
||||
}
|
||||
type JwtConfig struct {
|
||||
JwtAccessKey string
|
||||
JwtAccessExpiry int
|
||||
}
|
||||
|
||||
func CreateJwt(userId string, Role string, key string, expiry int) (string, error) {
|
||||
func CreateJwt(userId string, Role domain.Role, key string, expiry int) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{RegisteredClaims: jwt.RegisteredClaims{Issuer: "github.com/lafetz/snippitstash",
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Audience: jwt.ClaimStrings{"fortune.com"},
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ package httpserver
|
|||
|
||||
import (
|
||||
_ "github.com/SamuelTariku/FortuneBet-Backend/docs"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/handlers"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
fiberSwagger "github.com/swaggo/fiber-swagger"
|
||||
)
|
||||
|
||||
func (a *App) initAppRoutes() {
|
||||
a.fiber.Post("/auth/login", a.LoginCustomer)
|
||||
a.fiber.Post("/auth/refresh", a.authMiddleware, a.RefreshToken)
|
||||
a.fiber.Post("/auth/logout", a.authMiddleware, a.LogOutCustomer)
|
||||
a.fiber.Post("/auth/login", handlers.LoginCustomer(a.logger, a.authSvc, a.validator, a.JwtConfig))
|
||||
a.fiber.Post("/auth/refresh", a.authMiddleware, handlers.RefreshToken(a.logger, a.authSvc, a.validator, a.JwtConfig))
|
||||
a.fiber.Post("/auth/logout", a.authMiddleware, handlers.LogOutCustomer(a.logger, a.authSvc, a.validator))
|
||||
a.fiber.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error {
|
||||
userId := c.Locals("user_id")
|
||||
role := c.Locals("role")
|
||||
|
|
|
|||
|
|
@ -1,170 +0,0 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type loginCustomerReq struct {
|
||||
Email string `json:"email" example:"john.doe@example.com"`
|
||||
PhoneNumber string `json:"phone_number" example:"1234567890"`
|
||||
Password string `json:"password" example:"password123"`
|
||||
}
|
||||
|
||||
type loginCustomerRes struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// LoginCustomer godoc
|
||||
// @Summary Login customer
|
||||
// @Description Login customer
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param login body loginCustomerReq true "Login customer"
|
||||
// @Success 200 {object} loginCustomerRes
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/login [post]
|
||||
func (a *App) LoginCustomer(c *fiber.Ctx) error {
|
||||
var req loginCustomerReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
a.logger.Error("Login failed", "error", err)
|
||||
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
|
||||
}
|
||||
valErrs, ok := a.validator.Validate(c, req)
|
||||
if !ok {
|
||||
|
||||
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||
return nil
|
||||
}
|
||||
successRes, err := a.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password)
|
||||
if err != nil {
|
||||
a.logger.Info("Login failed", "error", err)
|
||||
if errors.Is(err, authentication.ErrInvalidPassword) {
|
||||
response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid password or not registered", nil, nil)
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, authentication.ErrUserNotFound) {
|
||||
response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid password or not registered", nil, nil)
|
||||
return nil
|
||||
}
|
||||
a.logger.Error("Login failed", "error", err)
|
||||
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
|
||||
return nil
|
||||
|
||||
}
|
||||
accessToken, err := jwtutil.CreateJwt(strconv.Itoa(int(successRes.UserId)), successRes.Role, a.JwtConfig.JwtAccessKey, a.JwtConfig.JwtAccessExpiry)
|
||||
res := loginCustomerRes{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: successRes.RfToken,
|
||||
}
|
||||
return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil)
|
||||
}
|
||||
|
||||
type refreshToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// RefreshToken godoc
|
||||
// @Summary Refresh token
|
||||
// @Description Refresh token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param refresh body refreshToken true "tokens"
|
||||
// @Success 200 {object} loginCustomerRes
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/refresh [post]
|
||||
func (a *App) RefreshToken(c *fiber.Ctx) error {
|
||||
var req refreshToken
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
|
||||
}
|
||||
valErrs, ok := a.validator.Validate(c, req)
|
||||
if !ok {
|
||||
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||
return nil
|
||||
}
|
||||
rf, err := a.authSvc.RefreshToken(c.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
a.logger.Info("Refresh token failed", "error", err)
|
||||
if errors.Is(err, authentication.ErrExpiredToken) {
|
||||
response.WriteJSON(c, fiber.StatusUnauthorized, "The refresh token has expired", nil, nil)
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, authentication.ErrRefreshTokenNotFound) {
|
||||
response.WriteJSON(c, fiber.StatusUnauthorized, "Refresh token not found", nil, nil)
|
||||
return nil
|
||||
}
|
||||
a.logger.Error("Refresh token failed", "error", err)
|
||||
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
|
||||
return nil
|
||||
}
|
||||
accessToken, err := jwtutil.CreateJwt("", "", a.JwtConfig.JwtAccessKey, a.JwtConfig.JwtAccessExpiry)
|
||||
if err != nil {
|
||||
a.logger.Error("Create jwt failed", "error", err)
|
||||
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
res := loginCustomerRes{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: rf,
|
||||
}
|
||||
return response.WriteJSON(c, fiber.StatusOK, "refresh successful", res, nil)
|
||||
}
|
||||
|
||||
type logoutReq struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// LogOutCustomer godoc
|
||||
// @Summary Logout customer
|
||||
// @Description Logout customer
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param logout body logoutReq true "Logout customer"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/logout [post]
|
||||
func (a *App) LogOutCustomer(c *fiber.Ctx) error {
|
||||
var req logoutReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
|
||||
}
|
||||
valErrs, ok := a.validator.Validate(c, req)
|
||||
if !ok {
|
||||
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||
return nil
|
||||
}
|
||||
err := a.authSvc.Logout(c.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
a.logger.Info("Logout failed", "error", err)
|
||||
if errors.Is(err, authentication.ErrExpiredToken) {
|
||||
response.WriteJSON(c, fiber.StatusUnauthorized, "The refresh token has expired", nil, nil)
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, authentication.ErrRefreshTokenNotFound) {
|
||||
response.WriteJSON(c, fiber.StatusUnauthorized, "Refresh token not found", nil, nil)
|
||||
return nil
|
||||
}
|
||||
a.logger.Error("Logout failed", "error", err)
|
||||
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
|
||||
return nil
|
||||
}
|
||||
return response.WriteJSON(c, fiber.StatusOK, "Logout successful", nil, nil)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user