move handlers to separate package

This commit is contained in:
lafetz 2025-03-30 22:18:20 +03:00
parent ef006abd10
commit d1a33b18dc
17 changed files with 499 additions and 213 deletions

View File

@ -10,6 +10,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server" 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" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
) )
@ -43,7 +44,7 @@ func main() {
store := repository.NewStore(db) store := repository.NewStore(db)
v := customvalidator.NewCustomValidator(validator.New()) v := customvalidator.NewCustomValidator(validator.New())
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) 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, JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry, JwtAccessExpiry: cfg.AccessExpiry,
}) })

View File

@ -2,14 +2,20 @@ CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
first_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE ,
phone_number VARCHAR(20) UNIQUE NOT NULL, phone_number VARCHAR(20) UNIQUE,
password BYTEA NOT NULL, password BYTEA NOT NULL,
role VARCHAR(50) NOT NULL, role VARCHAR(50) NOT NULL,
verified BOOLEAN DEFAULT FALSE, email_verified BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ , phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
updated_at TIMESTAMPTZ , created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_email_phone UNIQUE (email, phone_number) 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 ( CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,

10
internal/domain/common.go Normal file
View 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
View 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
View 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"
)

View File

@ -9,8 +9,38 @@ type User struct {
Email string Email string
PhoneNumber string PhoneNumber string
Password []byte Password []byte
Role string Role Role
Verified bool //
CreatedAt time.Time EmailVerified bool
UpdatedAt time.Time 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
} }

View File

@ -29,7 +29,7 @@ func (s *Store) GetUserByEmailPhone(ctx context.Context, email, phone string) (d
Email: user.Email, Email: user.Email,
PhoneNumber: user.PhoneNumber, PhoneNumber: user.PhoneNumber,
Password: user.Password, Password: user.Password,
Role: user.Role, Role: domain.Role(user.Role),
}, nil }, nil
} }

View File

@ -26,7 +26,7 @@ func (s *Store) CreateUser(ctx context.Context, firstName, lastName, email, phon
Email: user.Email, Email: user.Email,
PhoneNumber: user.PhoneNumber, PhoneNumber: user.PhoneNumber,
Password: user.Password, Password: user.Password,
Role: user.Role, // Role: user.Role,
}, nil }, nil
} }
@ -42,7 +42,7 @@ func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error)
Email: user.Email, Email: user.Email,
PhoneNumber: user.PhoneNumber, PhoneNumber: user.PhoneNumber,
Password: user.Password, Password: user.Password,
Role: user.Role, // Role: user.Role,
}, nil }, nil
} }
func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) { 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, Email: user.Email,
PhoneNumber: user.PhoneNumber, PhoneNumber: user.PhoneNumber,
Password: user.Password, Password: user.Password,
Role: user.Role, // Role: user.Role,
}) })
} }
return result, nil return result, nil

View File

@ -20,7 +20,7 @@ var (
type LoginSuccess struct { type LoginSuccess struct {
UserId int64 UserId int64
Role string Role domain.Role
RfToken string RfToken string
} }

View File

@ -7,11 +7,25 @@ import (
) )
type UserStore interface { 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) GetUserByID(ctx context.Context, id int64) (domain.User, error)
GetAllUsers(ctx context.Context) ([]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 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
} }

View File

@ -2,36 +2,199 @@ package user
import ( import (
"context" "context"
"errors"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"golang.org/x/crypto/bcrypt" "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 { type Service struct {
userStore UserStore 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{ return &Service{
userStore: userStore, userStore: userStore,
otpStore: otpStore,
} }
} }
func (s *Service) CreateUser(ctx context.Context, firstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error) { func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
return s.userStore.CreateUser(ctx, firstName, lastName, email, phoneNumber, password, role, verified) 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) { func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
return s.userStore.GetUserByID(ctx, id) 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) { func hashPassword(plaintextPassword string) ([]byte, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12) hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
if err != nil { if err != nil {

View File

@ -5,29 +5,26 @@ import (
"log/slog" "log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "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" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type JwtConfig struct {
JwtAccessKey string
JwtAccessExpiry int
}
type App struct { type App struct {
fiber *fiber.App fiber *fiber.App
logger *slog.Logger logger *slog.Logger
port int port int
authSvc *authentication.Service authSvc *authentication.Service
validator *customvalidator.CustomValidator validator *customvalidator.CustomValidator
JwtConfig JwtConfig JwtConfig jwtutil.JwtConfig
} }
func NewApp( func NewApp(
port int, validator *customvalidator.CustomValidator, port int, validator *customvalidator.CustomValidator,
authSvc *authentication.Service, authSvc *authentication.Service,
logger *slog.Logger, logger *slog.Logger,
JwtConfig JwtConfig, JwtConfig jwtutil.JwtConfig,
) *App { ) *App {
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
CaseSensitive: true, CaseSensitive: true,

View 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)
}
}

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
@ -20,10 +21,14 @@ var (
type UserClaim struct { type UserClaim struct {
jwt.RegisteredClaims jwt.RegisteredClaims
UserId string 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", token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{RegisteredClaims: jwt.RegisteredClaims{Issuer: "github.com/lafetz/snippitstash",
IssuedAt: jwt.NewNumericDate(time.Now()), IssuedAt: jwt.NewNumericDate(time.Now()),
Audience: jwt.ClaimStrings{"fortune.com"}, Audience: jwt.ClaimStrings{"fortune.com"},

View File

@ -2,14 +2,15 @@ package httpserver
import ( import (
_ "github.com/SamuelTariku/FortuneBet-Backend/docs" _ "github.com/SamuelTariku/FortuneBet-Backend/docs"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/handlers"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
fiberSwagger "github.com/swaggo/fiber-swagger" fiberSwagger "github.com/swaggo/fiber-swagger"
) )
func (a *App) initAppRoutes() { func (a *App) initAppRoutes() {
a.fiber.Post("/auth/login", a.LoginCustomer) a.fiber.Post("/auth/login", handlers.LoginCustomer(a.logger, a.authSvc, a.validator, a.JwtConfig))
a.fiber.Post("/auth/refresh", a.authMiddleware, a.RefreshToken) a.fiber.Post("/auth/refresh", a.authMiddleware, handlers.RefreshToken(a.logger, a.authSvc, a.validator, a.JwtConfig))
a.fiber.Post("/auth/logout", a.authMiddleware, a.LogOutCustomer) 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 { a.fiber.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error {
userId := c.Locals("user_id") userId := c.Locals("user_id")
role := c.Locals("role") role := c.Locals("role")

View File

@ -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)
}

View File

@ -30,3 +30,9 @@ migrations/up:
.PHONY: swagger .PHONY: swagger
swagger: swagger:
swag init -g cmd/main.go swag init -g cmd/main.go
.PHONY: db-up
db-up:
docker compose -f compose.db.yaml up
.PHONY: db-down
db-down:
docker compose -f compose.db.yaml down