Yimaru-BackEnd/internal/web_server/handlers/user.go
2025-05-16 22:38:10 +03:00

588 lines
20 KiB
Go

package handlers
import (
"errors"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
)
type CheckPhoneEmailExistReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
}
type CheckPhoneEmailExistRes struct {
EmailExist bool `json:"email_exist"`
PhoneNumberExist bool `json:"phone_number_exist"`
}
// CheckPhoneEmailExist godoc
// @Summary Check if phone number or email exist
// @Description Check if phone number or email exist
// @Tags user
// @Accept json
// @Produce json
// @Param checkPhoneEmailExist body CheckPhoneEmailExistReq true "Check phone number or email exist"
// @Success 200 {object} CheckPhoneEmailExistRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/checkPhoneEmailExist [post]
func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error {
var req CheckPhoneEmailExistReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse CheckPhoneEmailExist request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
emailExist, phoneExist, err := h.userSvc.CheckPhoneEmailExist(c.Context(), req.PhoneNumber, req.Email)
if err != nil {
h.logger.Error("Failed to check phone/email existence", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check phone/email existence")
}
res := CheckPhoneEmailExistRes{
EmailExist: emailExist,
PhoneNumberExist: phoneExist,
}
return response.WriteJSON(c, fiber.StatusOK, "Check successful", res, nil)
}
type RegisterCodeReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
}
// SendRegisterCode godoc
// @Summary Send register code
// @Description Send register code
// @Tags user
// @Accept json
// @Produce json
// @Param registerCode body RegisterCodeReq true "Send register code"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/sendRegisterCode [post]
func (h *Handler) SendRegisterCode(c *fiber.Ctx) error {
var req RegisterCodeReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse SendRegisterCode request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
var sentTo string
var medium domain.OtpMedium
if req.Email != "" {
sentTo = req.Email
medium = domain.OtpMediumEmail
} else if req.PhoneNumber != "" {
sentTo = req.PhoneNumber
medium = domain.OtpMediumSms
} else {
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
}
if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo); err != nil {
h.logger.Error("Failed to send register code", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to send register code")
}
return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil)
}
type RegisterUserReq struct {
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"`
Otp string `json:"otp" example:"123456"`
ReferalCode string `json:"referal_code" example:"ABC123"`
}
// RegisterUser godoc
// @Summary Register user
// @Description Register user
// @Tags user
// @Accept json
// @Produce json
// @Param registerUser body RegisterUserReq true "Register user"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/register [post]
func (h *Handler) RegisterUser(c *fiber.Ctx) error {
var req RegisterUserReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse RegisterUser request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
user := domain.RegisterUserReq{
FirstName: req.FirstName,
LastName: req.LastName,
Email: req.Email,
PhoneNumber: req.PhoneNumber,
Password: req.Password,
Otp: req.Otp,
ReferralCode: req.ReferalCode,
OtpMedium: domain.OtpMediumEmail,
}
medium, err := getMedium(req.Email, req.PhoneNumber)
if err != nil {
h.logger.Error("RegisterUser failed", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}
user.OtpMedium = medium
newUser, err := h.userSvc.RegisterUser(c.Context(), user)
if err != nil {
if errors.Is(err, domain.ErrOtpAlreadyUsed) {
return response.WriteJSON(c, fiber.StatusBadRequest, "Otp already used", nil, nil)
}
if errors.Is(err, domain.ErrOtpExpired) {
return response.WriteJSON(c, fiber.StatusBadRequest, "Otp expired", nil, nil)
}
if errors.Is(err, domain.ErrInvalidOtp) {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid otp", nil, nil)
}
if errors.Is(err, domain.ErrOtpNotFound) {
return response.WriteJSON(c, fiber.StatusBadRequest, "User already exist", nil, nil)
}
h.logger.Error("RegisterUser failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
_, err = h.walletSvc.CreateWallet(c.Context(), domain.CreateWallet{
UserID: newUser.ID,
IsWithdraw: true,
IsBettable: true,
})
if err != nil {
h.logger.Error("Failed to create wallet for user", "userID", newUser.ID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create user wallet")
}
if req.ReferalCode != "" {
err = h.referralSvc.ProcessReferral(c.Context(), req.PhoneNumber, req.ReferalCode)
if err != nil {
h.logger.Warn("Failed to process referral during registration", "phone", req.PhoneNumber, "code", req.ReferalCode, "error", err)
}
}
return response.WriteJSON(c, fiber.StatusOK, "Registration successful", nil, nil)
}
type ResetCodeReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
}
// SendResetCode godoc
// @Summary Send reset code
// @Description Send reset code
// @Tags user
// @Accept json
// @Produce json
// @Param resetCode body ResetCodeReq true "Send reset code"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/sendResetCode [post]
func (h *Handler) SendResetCode(c *fiber.Ctx) error {
var req ResetCodeReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse SendResetCode request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
var sentTo string
var medium domain.OtpMedium
if req.Email != "" {
sentTo = req.Email
medium = domain.OtpMediumEmail
} else if req.PhoneNumber != "" {
sentTo = req.PhoneNumber
medium = domain.OtpMediumSms
} else {
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
}
if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo); err != nil {
h.logger.Error("Failed to send reset code", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code")
}
return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil)
}
type ResetPasswordReq struct {
Email string `json:"email" validate:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
Password string `json:"password" validate:"required,min=8" example:"newpassword123"`
Otp string `json:"otp" validate:"required" example:"123456"`
}
// ResetPassword godoc
// @Summary Reset password
// @Description Reset password
// @Tags user
// @Accept json
// @Produce json
// @Param resetPassword body ResetPasswordReq true "Reset password"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/resetPassword [post]
func (h *Handler) ResetPassword(c *fiber.Ctx) error {
var req ResetPasswordReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse ResetPassword request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
medium, err := getMedium(req.Email, req.PhoneNumber)
if err != nil {
h.logger.Error("Failed to determine medium for ResetPassword", "error", err)
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
resetReq := domain.ResetPasswordReq{
Email: req.Email,
PhoneNumber: req.PhoneNumber,
Password: req.Password,
Otp: req.Otp,
OtpMedium: medium,
}
if err := h.userSvc.ResetPassword(c.Context(), resetReq); err != nil {
h.logger.Error("Failed to reset password", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset password")
}
return response.WriteJSON(c, fiber.StatusOK, "Password reset successful", nil, nil)
}
type UserProfileRes struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
Role domain.Role `json:"role"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastLogin time.Time `json:"last_login"`
SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"`
}
// UserProfile godoc
// @Summary Get user profile
// @Description Get user profile
// @Tags user
// @Accept json
// @Produce json
// @Success 200 {object} UserProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security Bearer
// @Router /user/profile [get]
func (h *Handler) UserProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.logger.Error("Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.logger.Error("Failed to get user profile", "userID", userID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile")
}
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil {
if err != authentication.ErrRefreshTokenNotFound {
h.logger.Error("Failed to get user last login", "userID", userID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login")
}
lastLogin = &user.CreatedAt
}
res := UserProfileRes{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
SuspendedAt: user.SuspendedAt,
Suspended: user.Suspended,
LastLogin: *lastLogin,
}
return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil)
}
// Helper function (unchanged)
func getMedium(email, phoneNumber string) (domain.OtpMedium, error) {
if email != "" {
return domain.OtpMediumEmail, nil
}
if phoneNumber != "" {
return domain.OtpMediumSms, nil
}
return "", errors.New("both email and phone number are empty")
}
type SearchUserByNameOrPhoneReq struct {
SearchString string
}
// SearchUserByNameOrPhone godoc
// @Summary Search for user using name or phone
// @Description Search for user using name or phone
// @Tags user
// @Accept json
// @Produce json
// @Param searchUserByNameOrPhone body SearchUserByNameOrPhoneReq true "Search for using his name or phone"
// @Success 200 {object} UserProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/search [post]
func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
// TODO: Add filtering by role based on which user is calling this
var req SearchUserByNameOrPhoneReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("SearchUserByNameOrPhone failed", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request",
})
}
valErrs, ok := h.validator.Validate(c, req)
if !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
}
users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString)
if err != nil {
h.logger.Error("SearchUserByNameOrPhone failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
var res []UserProfileRes = make([]UserProfileRes, 0, len(users))
for _, user := range users {
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil {
if err != authentication.ErrRefreshTokenNotFound {
h.logger.Error("Failed to get user last login", "userID", user.ID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login")
}
lastLogin = &user.CreatedAt
}
res = append(res, UserProfileRes{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
SuspendedAt: user.SuspendedAt,
Suspended: user.Suspended,
LastLogin: *lastLogin,
})
}
return response.WriteJSON(c, fiber.StatusOK, "Search Successful", res, nil)
}
// GetUserByID godoc
// @Summary Get user by id
// @Description Get a single user by id
// @Tags user
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} UserProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/single/{id} [get]
func (h *Handler) GetUserByID(c *fiber.Ctx) error {
// branchId := int64(12) //c.Locals("branch_id").(int64)
// filter := user.Filter{
// Role: string(domain.RoleUser),
// BranchId: user.ValidBranchId{
// Value: branchId,
// Valid: true,
// },
// Page: c.QueryInt("page", 1),
// PageSize: c.QueryInt("page_size", 10),
// }
// valErrs, ok := validator.Validate(c, filter)
// if !ok {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
// }
userIDstr := c.Params("id")
userID, err := strconv.ParseInt(userIDstr, 10, 64)
if err != nil {
h.logger.Error("UpdateCashier failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil)
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.logger.Error("GetAllCashiers failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil)
}
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil {
if err != authentication.ErrRefreshTokenNotFound {
h.logger.Error("Failed to get user last login", "userID", user.ID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login")
}
lastLogin = &user.CreatedAt
}
res := UserProfileRes{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
SuspendedAt: user.SuspendedAt,
Suspended: user.Suspended,
LastLogin: *lastLogin,
}
return response.WriteJSON(c, fiber.StatusOK, "Cashiers retrieved successfully", res, nil)
}
// DeleteUser godoc
// @Summary Delete user by ID
// @Description Delete a user by their ID
// @Tags user
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/delete/{id} [delete]
func (h *Handler) DeleteUser(c *fiber.Ctx) error {
userIDstr := c.Params("id")
userID, err := strconv.ParseInt(userIDstr, 10, 64)
if err != nil {
h.logger.Error("DeleteUser failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid user ID", nil, nil)
}
err = h.userSvc.DeleteUser(c.Context(), userID)
if err != nil {
h.logger.Error("Failed to delete user", "userID", userID, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to delete user", nil, nil)
}
return response.WriteJSON(c, fiber.StatusOK, "User deleted successfully", nil, nil)
}
type UpdateUserSuspendReq struct {
UserID int64 `json:"user_id" validate:"required" example:"123"`
Suspended bool `json:"suspended" validate:"required" example:"true"`
}
type UpdateUserSuspendRes struct {
UserID int64 `json:"user_id"`
Suspended bool `json:"suspended"`
}
// UpdateUserSuspend godoc
// @Summary Suspend or unsuspend a user
// @Description Suspend or unsuspend a user
// @Tags user
// @Accept json
// @Produce json
// @Param updateUserSuspend body UpdateUserSuspendReq true "Suspend or unsuspend a user"
// @Success 200 {object} UpdateUserSuspendRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/suspend [post]
func (h *Handler) UpdateUserSuspend(c *fiber.Ctx) error {
var req UpdateUserSuspendReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse UpdateUserSuspend request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
err := h.userSvc.UpdateUserSuspend(c.Context(), req.UserID, req.Suspended)
if err != nil {
h.logger.Error("Failed to update user suspend status", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update user suspend status")
}
res := UpdateUserSuspendRes{
UserID: req.UserID,
Suspended: req.Suspended,
}
return response.WriteJSON(c, fiber.StatusOK, "User suspend status updated successfully", res, nil)
}