Yimaru-BackEnd/internal/web_server/handlers/user.go

1574 lines
51 KiB
Go

package handlers
import (
"Yimaru-Backend/internal/domain"
"Yimaru-Backend/internal/services/authentication"
"Yimaru-Backend/internal/web_server/response"
"errors"
"fmt"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
// CheckProfileCompleted godoc
// @Summary Check if user profile is completed
// @Description Returns whether the specified user's profile is completed
// @Tags user
// @Accept json
// @Produce json
// @Param user_id path int true "User ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/user/{user_id}/is-profile-completed [get]
func (h *Handler) CheckProfileCompleted(c *fiber.Ctx) error {
userIDParam := c.Params("user_id")
if userIDParam == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid user id",
Error: "User id cannot be empty",
})
}
userID, err := strconv.ParseInt(userIDParam, 10, 64)
if err != nil || userID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid user id",
Error: "User id must be a valid positive integer",
})
}
isCompleted, err := h.userSvc.IsProfileCompleted(c.Context(), userID)
if err != nil {
if errors.Is(err, authentication.ErrUserNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "User not found",
Error: err.Error(),
})
}
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to check profile completion status",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Profile completion status fetched successfully",
Data: map[string]bool{
"is_profile_completed": isCompleted,
},
})
}
// UpdateUser godoc
// @Summary Update user profile
// @Description Updates user profile information (partial updates supported)
// @Tags user
// @Accept json
// @Produce json
// @Param user_id path int true "User ID"
// @Param body body domain.UpdateUserReq true "Update user payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/{tenant_slug}/user [put]
func (h *Handler) UpdateUser(c *fiber.Ctx) error {
// Extract user ID from context
userIDStr, ok := c.Locals("user_id").(string)
if !ok {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid user context",
Error: "User ID not found in request context",
})
}
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil || userID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid user ID",
Error: "User ID must be a positive integer",
})
}
// Parse request body
var req domain.UpdateUserReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
// Enforce user identity
req.UserID = userID
// Optional: lightweight validation (example)
// if req.Status.IsSet() {
// if !domain.(req.Status.Value) {
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
// Message: "Invalid status value",
// Error: "Unsupported user status",
// })
// }
// }
// Call service
if err := h.userSvc.UpdateUser(c.Context(), req); err != nil {
if errors.Is(err, authentication.ErrUserNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "User not found",
Error: err.Error(),
})
}
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update user",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "User updated successfully",
})
}
// UpdateUserKnowledgeLevel godoc
// @Summary Update user's knowledge level
// @Description Updates the knowledge level of the specified user after initial assessment
// @Tags user
// @Accept json
// @Produce json
// @Param user_id path int true "User ID"
// @Param knowledge_level body domain.UpdateKnowledgeLevelReq true "Knowledge level"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/{tenant_slug}/user/knowledge-level [put]
func (h *Handler) UpdateUserKnowledgeLevel(c *fiber.Ctx) error {
userIDStr := c.Locals("user_id").(string)
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil || userID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid user ID",
Error: "User ID must be a positive integer",
})
}
var req domain.UpdateKnowledgeLevelReq
if err := c.BodyParser(&req); err != nil || req.KnowledgeLevel == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: "Knowledge level is required",
})
}
err = h.userSvc.UpdateUserKnowledgeLevel(c.Context(), userID, req.KnowledgeLevel)
if err != nil {
if errors.Is(err, authentication.ErrUserNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "User not found",
Error: err.Error(),
})
}
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update user knowledge level",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "User knowledge level updated successfully",
})
}
// ResendOtp godoc
// @Summary Resend OTP
// @Description Resend OTP if the previous one is expired
// @Tags otp
// @Accept json
// @Produce json
// @Param resendOtp body domain.ResendOtpReq true "Resend OTP"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/otp/resend [post]
func (h *Handler) ResendOtp(c *fiber.Ctx) error {
var req domain.ResendOtpReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse ResendOtp request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to resend OTP",
Error: "Invalid request body: " + err.Error(),
})
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to resend OTP",
Error: errMsg,
})
}
user, err := h.userSvc.GetUserByEmailPhone(c.Context(), req.Email, req.PhoneNumber)
if err != nil {
h.mongoLoggerSvc.Info("Failed to get user by user name",
zap.String("email", req.Email),
zap.String("phone_number", req.PhoneNumber),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to resend OTP",
Error: err.Error(),
})
}
medium, err := getMedium(user.Email, user.PhoneNumber)
if err != nil {
h.mongoLoggerSvc.Info("Failed to determine OTP medium",
zap.String("email", user.Email),
zap.String("phone_number", user.PhoneNumber),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to resend OTP",
Error: err.Error(),
})
}
sentTo := user.Email
if medium == domain.OtpMediumSms {
sentTo = user.PhoneNumber
}
if err := h.userSvc.ResendOtp(
c.Context(),
req.Email,
req.PhoneNumber,
); err != nil {
h.mongoLoggerSvc.Error("Failed to resend OTP",
zap.String("sent_to", sentTo),
zap.String("medium", string(medium)),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to resend OTP",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "OTP resent successfully",
Success: true,
StatusCode: fiber.StatusOK,
Data: nil,
})
}
// CheckUserNameUnique godoc
// @Summary Check if user_name is unique
// @Description Returns whether the specified user_name is available (unique)
// @Tags user
// @Accept json
// @Produce json
// @Param user_name path string true "User Name"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/user/{user_name}/is-unique [get]
func (h *Handler) CheckUserNameUnique(c *fiber.Ctx) error {
userName := c.Params("user_name")
if userName == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid user name",
Error: "user_name path parameter cannot be empty",
})
}
isUnique, err := h.userSvc.IsUserNameUnique(c.Context(), userName)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to check user name uniqueness",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "User name uniqueness checked successfully",
Data: map[string]bool{
"is_unique": isUnique,
},
})
}
// CheckUserPending godoc
// @Summary Check if user status is pending
// @Description Returns whether the specified user has a status of "pending"
// @Tags user
// @Accept json
// @Produce json
// @Param user_name path string true "User Name"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/{tenant_slug}/user/{user_name}/is-pending [get]
func (h *Handler) CheckUserPending(c *fiber.Ctx) error {
userName := c.Params("user_name")
if userName == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid user name",
Error: "User name cannot be empty",
})
}
isPending, err := h.userSvc.IsUserPending(c.Context(), userName)
if err != nil {
if errors.Is(err, authentication.ErrUserNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "User not found",
Error: err.Error(),
})
}
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to check user status",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "User status fetched successfully",
Data: map[string]bool{
"is_pending": isPending,
},
})
}
// GetAllUsers godoc
// @Summary Get all users
// @Description Get users with optional filters
// @Tags user
// @Accept json
// @Produce json
// @Param role query string false "Role filter"
// @Param query query string false "Search query"
// @Param page query int false "Page number"
// @Param page_size query int false "Page size"
// @Param created_before query string false "Created before (RFC3339)"
// @Param created_after query string false "Created after (RFC3339)"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/users [get]
func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
searchQuery := c.Query("query")
searchString := domain.ValidString{
Value: searchQuery,
Valid: searchQuery != "",
}
createdBeforeQuery := c.Query("created_before")
var createdBefore domain.ValidTime
if createdBeforeQuery != "" {
parsed, err := time.Parse(time.RFC3339, createdBeforeQuery)
if err != nil {
h.logger.Info("invalid created_before format", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid created_before format")
}
createdBefore = domain.ValidTime{Value: parsed, Valid: true}
}
createdAfterQuery := c.Query("created_after")
var createdAfter domain.ValidTime
if createdAfterQuery != "" {
parsed, err := time.Parse(time.RFC3339, createdAfterQuery)
if err != nil {
h.logger.Info("invalid created_after format", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid created_after format")
}
createdAfter = domain.ValidTime{Value: parsed, Valid: true}
}
filter := domain.UserFilter{
Role: c.Query("role"),
Page: domain.ValidInt{
Value: c.QueryInt("page", 1) - 1,
Valid: true,
},
PageSize: domain.ValidInt{
Value: c.QueryInt("page_size", 10),
Valid: true,
},
Query: searchString,
CreatedBefore: createdBefore,
CreatedAfter: createdAfter,
}
if valErrs, ok := h.validator.Validate(c, filter); !ok {
var errMsg string
for f, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", f, msg)
}
h.mongoLoggerSvc.Info("invalid filter values in GetAllUsers request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Any("validation_errors", valErrs),
zap.Time("timestamp", time.Now()))
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
users, total, err := h.userSvc.GetAllUsers(c.Context(), filter)
if err != nil {
h.mongoLoggerSvc.Error("failed to get users",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Any("filter", filter),
zap.Error(err),
zap.Time("timestamp", time.Now()))
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get users: "+err.Error())
}
// Map to profile response to avoid leaking sensitive fields
result := make([]domain.UserProfileResponse, len(users))
for i, u := range users {
result[i] = domain.UserProfileResponse{
ID: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
UserName: u.UserName,
Email: u.Email,
PhoneNumber: u.PhoneNumber,
Role: u.Role,
Age: u.Age,
EducationLevel: u.EducationLevel,
Country: u.Country,
Region: u.Region,
NickName: u.NickName,
Occupation: u.Occupation,
LearningGoal: u.LearningGoal,
LanguageGoal: u.LanguageGoal,
LanguageChallange: u.LanguageChallange,
FavoutiteTopic: u.FavoutiteTopic,
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
LastLogin: u.LastLogin,
ProfileCompleted: u.ProfileCompleted,
ProfilePictureURL: u.ProfilePictureURL,
PreferredLanguage: u.PreferredLanguage,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
return response.WriteJSON(c, fiber.StatusOK, "Users fetched successfully", map[string]interface{}{"users": result, "total": total}, nil)
}
// VerifyOtp godoc
// @Summary Verify OTP
// @Description Verify OTP for registration or other actions
// @Tags user
// @Accept json
// @Produce json
// @Param verifyOtp body domain.VerifyOtpReq true "Verify OTP"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/user/verify-otp [post]
func (h *Handler) VerifyOtp(c *fiber.Ctx) error {
var req domain.VerifyOtpReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse VerifyOtp request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to verify OTP",
Error: "Invalid request body: " + err.Error(),
})
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to verify OTP",
Error: errMsg,
})
}
// Call service to verify OTP
err := h.userSvc.VerifyOtp(c.Context(), req.Email, req.PhoneNumber, req.Otp)
if err != nil {
var errMsg string
switch {
case errors.Is(err, domain.ErrOtpAlreadyUsed):
errMsg = "OTP already used"
case errors.Is(err, domain.ErrOtpExpired):
errMsg = "OTP expired"
case errors.Is(err, domain.ErrInvalidOtp):
errMsg = "Invalid OTP"
case errors.Is(err, domain.ErrOtpNotFound):
errMsg = "OTP not found"
default:
h.mongoLoggerSvc.Error("Failed to verify OTP",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
errMsg = "Failed to verify OTP: " + err.Error()
}
statusCode := fiber.StatusBadRequest
if errMsg == "Failed to verify OTP: "+err.Error() {
statusCode = fiber.StatusInternalServerError
}
return c.Status(statusCode).JSON(domain.ErrorResponse{
Message: "Failed to verify OTP",
Error: errMsg,
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "OTP verified successfully",
Data: nil,
})
}
type GetTenantSlugByToken struct {
Slug string `json:"slug"`
}
// GetTenantSlugByToken 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 /api/v1/tenant [get]
func (h *Handler) GetTenantSlugByToken(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.mongoLoggerSvc.Error("Invalid user ID in context",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification")
}
_, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get user profile",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile:"+err.Error())
}
// if !user.OrganizationID.Valid {
// if user.Role == domain.RoleSuperAdmin {
// return fiber.NewError(fiber.StatusBadRequest, "Role Super-Admin Doesn't have a company-id")
// }
// h.mongoLoggerSvc.Error("Unknown Error: User doesn't have a company-id",
// zap.Int64("userID", userID),
// zap.Int("status_code", fiber.StatusInternalServerError),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusInternalServerError, "Unknown Error: User doesn't have a company-id")
// }
// company, err := h.companySvc.GetCompanyByID(c.Context(), user.CompanyID.Value)
// if err != nil {
// h.mongoLoggerSvc.Error("Failed to get company by id",
// zap.Int64("company", user.CompanyID.Value),
// zap.Int("status_code", fiber.StatusInternalServerError),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve company:"+err.Error())
// }
res := GetTenantSlugByToken{
Slug: strconv.FormatInt(userID, 10),
}
return response.WriteJSON(c, fiber.StatusOK, "Tenant Slug retrieved successfully", res, nil)
}
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 /api/v1/{tenant_slug}/user/checkPhoneEmailExist [post]
func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error {
// companyID := c.Locals("company_id").(domain.ValidInt64)
// if !companyID.Valid {
// h.BadRequestLogger().Error("invalid company id")
// return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
// }
var req CheckPhoneEmailExistReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse CheckPhoneEmailExist request",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error())
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
emailExist, phoneExist, err := h.userSvc.CheckPhoneEmailExist(c.Context(), req.PhoneNumber, req.Email)
if err != nil {
h.mongoLoggerSvc.Error("Failed to check phone/email existence",
zap.Any("request", req),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check phone/email existence:"+err.Error())
}
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 /api/v1/{tenant_slug}/user/sendRegisterCode [post]
func (h *Handler) SendRegisterCode(c *fiber.Ctx) error {
// companyID := c.Locals("company_id").(domain.ValidInt64)
// if !companyID.Valid {
// h.BadRequestLogger().Error("invalid company id")
// return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
// }
var req RegisterCodeReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse SendRegisterCode request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error())
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
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, domain.AfroMessage); err != nil {
h.mongoLoggerSvc.Error("Failed to send register code",
zap.String("Medium", string(medium)),
zap.String("Send To", string(sentTo)),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to send register code:"+err.Error())
}
return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil)
}
// RegisterUser godoc
// @Summary Register user
// @Description Register user
// @Tags user
// @Accept json
// @Produce json
// @Param registerUser body domain.RegisterUserReq true "Register user"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/user/register [post]
func (h *Handler) RegisterUser(c *fiber.Ctx) error {
var req domain.RegisterUserReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse RegisterUser request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to register user",
Error: "Invalid request body: " + err.Error(),
})
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to register user",
Error: errMsg,
})
}
user := domain.RegisterUserReq{
FirstName: req.FirstName,
LastName: req.LastName,
UserName: req.UserName,
Email: req.Email,
PhoneNumber: req.PhoneNumber,
Password: req.Password,
OtpMedium: domain.OtpMediumEmail,
// Role: string(domain.RoleStudent),
// Age: req.Age,
// EducationLevel: req.EducationLevel,
// Country: req.Country,
// Region: req.Region,
// PreferredLanguage: req.PreferredLanguage,
// NickName: req.NickName,
// Occupation: req.Occupation,
// LearningGoal: req.LearningGoal,
// LanguageGoal: req.LanguageGoal,
// LanguageChallange: req.LanguageChallange,
// FavoutiteTopic: req.FavoutiteTopic,
}
medium, err := getMedium(req.Email, req.PhoneNumber)
if err != nil {
h.mongoLoggerSvc.Info("Failed to get medium",
zap.String("email", req.Email),
zap.String("phone number", req.PhoneNumber),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to register user",
Error: err.Error(),
})
}
user.OtpMedium = medium
_, err = h.userSvc.RegisterUser(c.Context(), user)
if err != nil {
h.mongoLoggerSvc.Error("Failed to register user",
zap.String("email", req.Email),
zap.String("phone number", req.PhoneNumber),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to register user",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Registration successful",
Data: nil,
})
}
func MapRegisterReqToUser(req domain.RegisterUserReq) domain.User {
return domain.User{
FirstName: req.FirstName,
LastName: req.LastName,
UserName: req.UserName,
Email: req.Email,
PhoneNumber: req.PhoneNumber,
Password: []byte(req.Password), // or hashed password
Role: domain.Role(req.Role),
// Age: req.Age,
// EducationLevel: req.EducationLevel,
// Country: req.Country,
// Region: req.Region,
// PreferredLanguage: req.PreferredLanguage,
// NickName: req.NickName,
// Occupation: req.Occupation,
// LearningGoal: req.LearningGoal,
// LanguageGoal: req.LanguageGoal,
// LanguageChallange: req.LanguageChallange,
// FavoutiteTopic: req.FavoutiteTopic,
}
}
type ResetCodeReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
// Provider domain.OtpProvider `json:"provider" validate:"required" example:"twilio"`
}
// 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 /api/v1/user/sendResetCode [post]
func (h *Handler) SendResetCode(c *fiber.Ctx) error {
var req ResetCodeReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse SendResetCode request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error())
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
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 {
h.mongoLoggerSvc.Info("Email or PhoneNumber must be provided",
zap.String("Email", req.Email),
zap.String("Phone Number", req.PhoneNumber),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
}
if err := h.userSvc.SendResetCode(c.Context(), "", medium, sentTo, domain.AfroMessage); err != nil {
h.mongoLoggerSvc.Error("Failed to send reset code",
zap.String("medium", string(medium)),
zap.String("sentTo", string(sentTo)),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code:"+err.Error())
}
return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil)
}
// SendTenantResetCode 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 /api/v1/{tenant_slug}/user/sendResetCode [post]
func (h *Handler) SendTenantResetCode(c *fiber.Ctx) error {
// companyID := c.Locals("company_id").(domain.ValidInt64)
// if !companyID.Valid {
// h.BadRequestLogger().Error("invalid company id")
// return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
// }
var req ResetCodeReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse SendResetCode request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error())
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
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 {
h.mongoLoggerSvc.Info("Email or PhoneNumber must be provided",
zap.String("Email", req.Email),
zap.String("Phone Number", req.PhoneNumber),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
}
if err := h.userSvc.SendResetCode(c.Context(), "", medium, sentTo, domain.AfroMessage); err != nil {
h.mongoLoggerSvc.Error("Failed to send reset code",
zap.String("medium", string(medium)),
zap.String("sentTo", string(sentTo)),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code:"+err.Error())
}
return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil)
}
type ResetPasswordReq struct {
UserName string `json:"user_name" validate:"required" example:"johndoe"`
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 /api/v1/user/resetPassword [post]
func (h *Handler) ResetPassword(c *fiber.Ctx) error {
var req ResetPasswordReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse ResetPassword request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error())
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
// user, err := h.userSvc.GetUserByUserName(c.Context(), req.UserName)
// if err != nil {
// h.mongoLoggerSvc.Info("Failed to get user by user name",
// zap.String("user_name", req.UserName),
// zap.Int("status_code", fiber.StatusBadRequest),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// }
// medium, err := getMedium(user.Email, user.PhoneNumber)
// if err != nil {
// h.mongoLoggerSvc.Info("Failed to determine medium for ResetPassword",
// zap.String("Email", user.Email),
// zap.String("Phone Number", user.PhoneNumber),
// zap.Int("status_code", fiber.StatusBadRequest),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusBadRequest, err.Error())
// }
resetReq := domain.ResetPasswordReq{
UserName: req.UserName,
Password: req.Password,
OtpCode: req.Otp,
}
if err := h.userSvc.ResetPassword(c.Context(), resetReq); err != nil {
h.mongoLoggerSvc.Error("Failed to reset password",
zap.Any("userID", resetReq),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset password:"+err.Error())
}
return response.WriteJSON(c, fiber.StatusOK, "Password reset successful", nil, nil)
}
// ResetTenantPassword godoc
// @Summary Reset tenant password
// @Description Reset tenant 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 /api/v1/{tenant_slug}/user/resetPassword [post]
func (h *Handler) ResetTenantPassword(c *fiber.Ctx) error {
// companyID := c.Locals("company_id").(domain.ValidInt64)
// if !companyID.Valid {
// h.BadRequestLogger().Error("invalid company id")
// return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
// }
var req ResetPasswordReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse ResetPassword request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error())
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
// medium, err := getMedium(req.Email, req.PhoneNumber)
// if err != nil {
// h.mongoLoggerSvc.Info("Failed to determine medium for ResetPassword",
// zap.String("Email", req.Email),
// zap.String("Phone Number", req.PhoneNumber),
// zap.Int("status_code", fiber.StatusBadRequest),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusBadRequest, err.Error())
// }
resetReq := domain.ResetPasswordReq{
UserName: req.UserName,
Password: req.Password,
OtpCode: req.Otp,
}
if err := h.userSvc.ResetPassword(c.Context(), resetReq); err != nil {
h.mongoLoggerSvc.Error("Failed to reset password",
zap.Any("userID", resetReq),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset password:"+err.Error())
}
return response.WriteJSON(c, fiber.StatusOK, "Password reset successful", nil, nil)
}
// CustomerProfile godoc
// @Summary Get user profile
// @Description Get user profile
// @Tags user
// @Accept json
// @Produce json
// @Success 200 {object} domain.UserProfileResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security Bearer
// @Router /api/v1/{tenant_slug}/user/user-profile [get]
func (h *Handler) GetUserProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.mongoLoggerSvc.Error("Invalid user ID in context",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification")
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get user profile",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile:"+err.Error())
}
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil {
if err != authentication.ErrRefreshTokenNotFound {
h.mongoLoggerSvc.Error("Failed to get user last login",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login:"+err.Error())
}
lastLogin = &user.CreatedAt
}
res := domain.UserProfileResponse{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
UserName: user.UserName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
Age: user.Age,
EducationLevel: user.EducationLevel,
Country: user.Country,
Region: user.Region,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
Status: user.Status,
LastLogin: lastLogin,
ProfileCompleted: user.ProfileCompleted,
ProfilePictureURL: user.ProfilePictureURL,
PreferredLanguage: user.PreferredLanguage,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil)
}
type AdminProfileRes 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"`
}
// AdminProfile godoc
// @Summary Get user profile
// @Description Get user profile
// @Tags user
// @Accept json
// @Produce json
// @Success 200 {object} AdminProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security Bearer
// @Router /api/v1/{tenant_slug}/user/admin-profile [get]
func (h *Handler) AdminProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.mongoLoggerSvc.Error("Invalid user ID in context",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification")
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get user profile",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile:"+err.Error())
}
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil {
if err != authentication.ErrRefreshTokenNotFound {
h.mongoLoggerSvc.Error("Failed to get user last login",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login:"+err.Error())
}
lastLogin = &user.CreatedAt
}
res := domain.UserProfileResponse{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
UserName: user.UserName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
Age: user.Age,
EducationLevel: user.EducationLevel,
Country: user.Country,
Region: user.Region,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
Status: user.Status,
LastLogin: lastLogin,
ProfileCompleted: user.ProfileCompleted,
ProfilePictureURL: user.ProfilePictureURL,
PreferredLanguage: user.PreferredLanguage,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
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 `json:"query"`
Role *domain.Role `json:"role,omitempty"`
}
// 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} domain.UserProfileResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/user/search [post]
func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
var req SearchUserByNameOrPhoneReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Error("SearchUserByNameOrPhone failed - invalid request body",
zap.Any("request", req),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body: "+err.Error())
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
// Optional role filter
var rolePtr *int64
if req.Role != nil && *req.Role != "" {
roleStr := string(*req.Role)
roleVal, err := strconv.ParseInt(roleStr, 10, 64)
if err != nil {
h.mongoLoggerSvc.Info("Invalid role value",
zap.String("role", roleStr),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "invalid role value")
}
rolePtr = &roleVal
}
users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, rolePtr)
if err != nil {
h.mongoLoggerSvc.Error("SearchUserByNameOrPhone - failed to fetch users",
zap.Any("request", req),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get users: "+err.Error())
}
res := make([]domain.UserProfileResponse, 0, len(users))
for _, user := range users {
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil && err != authentication.ErrRefreshTokenNotFound {
h.mongoLoggerSvc.Error("Failed to get user last login",
zap.Int64("user_id", user.ID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login: "+err.Error())
}
if err == authentication.ErrRefreshTokenNotFound {
lastLogin = &user.CreatedAt
}
// var orgID *int64
// if user.OrganizationID.Valid {
// orgID = &user.OrganizationID.Value
// }
res = append(res, domain.UserProfileResponse{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
UserName: user.UserName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
Age: user.Age,
EducationLevel: user.EducationLevel,
Country: user.Country,
Region: user.Region,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
Status: user.Status,
LastLogin: lastLogin,
ProfileCompleted: user.ProfileCompleted,
ProfilePictureURL: user.ProfilePictureURL,
PreferredLanguage: user.PreferredLanguage,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
})
}
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} domain.UserProfileResponse
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/user/single/{id} [get]
func (h *Handler) GetUserByID(c *fiber.Ctx) error {
userIDstr := c.Params("id")
userID, err := strconv.ParseInt(userIDstr, 10, 64)
if err != nil {
h.mongoLoggerSvc.Info("failed to parse user id",
zap.String("userID", userIDstr),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "invalid user id")
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.mongoLoggerSvc.Error("Get User By ID failed",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get user: "+err.Error())
}
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil && err != authentication.ErrRefreshTokenNotFound {
h.mongoLoggerSvc.Error("Failed to get user last login",
zap.Int64("userID", user.ID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login: "+err.Error())
}
if err == authentication.ErrRefreshTokenNotFound {
lastLogin = &user.CreatedAt
}
// var orgID *int64
// if user.OrganizationID.Valid {
// orgID = &user.OrganizationID.Value
// }
res := domain.UserProfileResponse{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
UserName: user.UserName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
Age: user.Age,
EducationLevel: user.EducationLevel,
Country: user.Country,
Region: user.Region,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
Status: user.Status,
LastLogin: lastLogin,
ProfileCompleted: user.ProfileCompleted,
ProfilePictureURL: user.ProfilePictureURL,
PreferredLanguage: user.PreferredLanguage,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
return response.WriteJSON(c, fiber.StatusOK, "User 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 /api/v1/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.mongoLoggerSvc.Info("Failed to parse user id",
zap.String("userID", userIDstr),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid user ID")
}
err = h.userSvc.DeleteUser(c.Context(), userID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to delete user",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete user:"+err.Error())
}
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" example:"true"`
}
type UpdateUserSuspendRes struct {
UserID int64 `json:"user_id"`
Suspended bool `json:"suspended"`
}