From d1a33b18dcb7b06af943b51ee1deaebc4601865e Mon Sep 17 00:00:00 2001 From: lafetz Date: Sun, 30 Mar 2025 22:18:20 +0300 Subject: [PATCH] move handlers to separate package --- cmd/main.go | 3 +- db/migrations/000001_fortune.up.sql | 18 +- internal/domain/common.go | 10 + internal/domain/otp.go | 29 +++ internal/domain/role.go | 11 ++ internal/domain/user.go | 38 +++- internal/repository/auth.go | 2 +- internal/repository/user.go | 6 +- internal/services/authentication/impl.go | 2 +- internal/services/user/port.go | 20 +- internal/services/user/service.go | 189 +++++++++++++++++-- internal/web_server/app.go | 9 +- internal/web_server/handlers/auth_handler.go | 183 ++++++++++++++++++ internal/web_server/jwt/jwt.go | 9 +- internal/web_server/routes.go | 7 +- internal/web_server/user_handler.go | 170 ----------------- makefile | 6 + 17 files changed, 499 insertions(+), 213 deletions(-) create mode 100644 internal/domain/common.go create mode 100644 internal/domain/otp.go create mode 100644 internal/domain/role.go create mode 100644 internal/web_server/handlers/auth_handler.go delete mode 100644 internal/web_server/user_handler.go diff --git a/cmd/main.go b/cmd/main.go index 451f9d4..5bc6f41 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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, }) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 69b5959..7647214 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -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, diff --git a/internal/domain/common.go b/internal/domain/common.go new file mode 100644 index 0000000..94666fd --- /dev/null +++ b/internal/domain/common.go @@ -0,0 +1,10 @@ +package domain + +type ValidString struct { + Value string + Valid bool +} +type ValidBool struct { + Value bool + Valid bool +} diff --git a/internal/domain/otp.go b/internal/domain/otp.go new file mode 100644 index 0000000..cc3630f --- /dev/null +++ b/internal/domain/otp.go @@ -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 +} diff --git a/internal/domain/role.go b/internal/domain/role.go new file mode 100644 index 0000000..59a17a5 --- /dev/null +++ b/internal/domain/role.go @@ -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" +) diff --git a/internal/domain/user.go b/internal/domain/user.go index 21d1a77..ed38ae8 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -9,8 +9,38 @@ type User struct { Email string PhoneNumber string Password []byte - Role string - Verified bool - CreatedAt time.Time - UpdatedAt time.Time + 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 } diff --git a/internal/repository/auth.go b/internal/repository/auth.go index 663e5b3..9695fc9 100644 --- a/internal/repository/auth.go +++ b/internal/repository/auth.go @@ -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 } diff --git a/internal/repository/user.go b/internal/repository/user.go index d6f7a36..dbbc6ca 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -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 diff --git a/internal/services/authentication/impl.go b/internal/services/authentication/impl.go index 7447312..ea8de4d 100644 --- a/internal/services/authentication/impl.go +++ b/internal/services/authentication/impl.go @@ -20,7 +20,7 @@ var ( type LoginSuccess struct { UserId int64 - Role string + Role domain.Role RfToken string } diff --git a/internal/services/user/port.go b/internal/services/user/port.go index b6f20cf..8773525 100644 --- a/internal/services/user/port.go +++ b/internal/services/user/port.go @@ -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 } diff --git a/internal/services/user/service.go b/internal/services/user/service.go index 8232bbb..2172211 100644 --- a/internal/services/user/service.go +++ b/internal/services/user/service.go @@ -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 + 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 { diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 3672a2c..811b973 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -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, diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go new file mode 100644 index 0000000..551b259 --- /dev/null +++ b/internal/web_server/handlers/auth_handler.go @@ -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) + } +} diff --git a/internal/web_server/jwt/jwt.go b/internal/web_server/jwt/jwt.go index 7dc3659..a59e81f 100644 --- a/internal/web_server/jwt/jwt.go +++ b/internal/web_server/jwt/jwt.go @@ -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"}, diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index e54fc9c..4af9781 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -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") diff --git a/internal/web_server/user_handler.go b/internal/web_server/user_handler.go deleted file mode 100644 index 199eadb..0000000 --- a/internal/web_server/user_handler.go +++ /dev/null @@ -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) -} diff --git a/makefile b/makefile index 6fad4c7..1cacc84 100644 --- a/makefile +++ b/makefile @@ -30,3 +30,9 @@ migrations/up: .PHONY: swagger swagger: 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 \ No newline at end of file