package handlers import ( "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/services/authentication" jwtutil "Yimaru-Backend/internal/web_server/jwt" "Yimaru-Backend/internal/web_server/response" "errors" "fmt" "time" "github.com/gofiber/fiber/v2" "go.uber.org/zap" ) // loginUserReq represents the request body for the Loginuser endpoint. type loginUserReq struct { Email string `json:"email"` PhoneNumber string `json:"phone_number"` Password string `json:"password"` OtpCode string `json:"otp_code"` // UserName string `json:"user_name" validate:"required" example:"johndoe"` // Password string `json:"password" validate:"required" example:"password123"` } // loginUserRes represents the response body for the Loginuser endpoint. type loginUserRes struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` Role string `json:"role"` UserID int64 `json:"user_id"` } // Loginuser godoc // @Summary Login user // @Description Login user // @Tags auth // @Accept json // @Produce json // @Param login body authentication.LoginRequest true "Login user" // @Success 200 {object} loginUserRes // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/{tenant_slug}/user-login [post] func (h *Handler) LoginUser(c *fiber.Ctx) error { var req authentication.LoginRequest if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse LoginUser 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 login", 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 login", Error: errMsg, }) } successRes, err := h.authSvc.Login(c.Context(), req) if err != nil { switch { case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials", zap.Int("status_code", fiber.StatusUnauthorized), zap.String("email", req.Email), zap.String("phone_number", req.PhoneNumber), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ Message: "Failed to login", Error: fmt.Sprintf("Invalid credentials: %v", err), }) case errors.Is(err, authentication.ErrUserSuspended): h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked", zap.Int("status_code", fiber.StatusUnauthorized), zap.String("email", req.Email), zap.String("phone_number", req.PhoneNumber), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ Message: "Failed to login", Error: fmt.Sprintf("User login has been locked: %v", err), }) default: h.mongoLoggerSvc.Error("Login failed", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to login", Error: err.Error(), }) } } // if successRes.Role != domain.RoleStudent { // h.mongoLoggerSvc.Info("Login attempt: user login of other role", // zap.Int("status_code", fiber.StatusForbidden), // zap.String("role", string(successRes.Role)), // zap.String("email", req.Email), // zap.String("phone_number", req.PhoneNumber), // zap.Time("timestamp", time.Now()), // ) // return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ // Message: "Failed to login", // Error: "Only users are allowed to login", // }) // } accessToken, err := jwtutil.CreateJwt( successRes.UserId, successRes.Role, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry, ) if err != nil { h.mongoLoggerSvc.Error("Failed to create access token", zap.Int("status_code", fiber.StatusInternalServerError), zap.Int64("user_id", successRes.UserId), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to login", Error: "Failed to generate access token", }) } res := loginUserRes{ AccessToken: accessToken, RefreshToken: successRes.RfToken, Role: string(successRes.Role), UserID: successRes.UserId, } h.mongoLoggerSvc.Info("Login successful", zap.Int("status_code", fiber.StatusOK), zap.Int64("user_id", successRes.UserId), zap.String("role", string(successRes.Role)), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Login successful", Data: res, }) } // loginAdminReq represents the request body for the LoginAdmin endpoint. type loginAdminReq struct { Email string `json:"email" validate:"required" example:"adminuser"` Password string `json:"password" validate:"required" example:"password123"` } // loginAdminRes represents the response body for the LoginAdmin endpoint. type LoginAdminRes struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` Role string `json:"role"` } // LoginAdmin godoc // @Summary Login user // @Description Login user // @Tags auth // @Accept json // @Produce json // @Param login body authentication.LoginRequest true "Login admin" // @Success 200 {object} LoginAdminRes // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/{tenant_slug}/admin-login [post] func (h *Handler) LoginAdmin(c *fiber.Ctx) error { var req authentication.LoginRequest if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse LoginAdmin 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) } successRes, err := h.authSvc.Login(c.Context(), authentication.LoginRequest(req)) if err != nil { switch { case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials", zap.Int("status_code", fiber.StatusBadRequest), zap.String("email", req.Email), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid credentials") case errors.Is(err, authentication.ErrUserSuspended): h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked", zap.Int("status_code", fiber.StatusForbidden), zap.String("email", req.Email), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusForbidden, "User login has been locked") default: h.mongoLoggerSvc.Error("Login failed", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Internal server error") } } if successRes.Role == domain.RoleStudent || successRes.Role == domain.RoleInstructor { h.mongoLoggerSvc.Warn("Login attempt: admin login of user", zap.Int("status_code", fiber.StatusForbidden), zap.String("role", string(successRes.Role)), zap.String("email", req.Email), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusForbidden, "Only admin roles are allowed") } accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) if err != nil { h.mongoLoggerSvc.Error("Failed to create access token", zap.Int("status_code", fiber.StatusInternalServerError), zap.Int64("user_id", successRes.UserId), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") } res := loginUserRes{ AccessToken: accessToken, RefreshToken: successRes.RfToken, Role: string(successRes.Role), UserID: successRes.UserId, } h.mongoLoggerSvc.Info("Login successful", zap.Int("status_code", fiber.StatusOK), zap.Int64("user_id", successRes.UserId), zap.String("role", string(successRes.Role)), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil) } // LoginSuper godoc // @Summary Login super-admin // @Description Login super-admin // @Tags auth // @Accept json // @Produce json // @Param login body authentication.LoginRequest true "Login super-admin" // @Success 200 {object} LoginAdminRes // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/super-login [post] func (h *Handler) LoginSuper(c *fiber.Ctx) error { var req authentication.LoginRequest if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse LoginAdmin 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) } successRes, err := h.authSvc.Login(c.Context(), authentication.LoginRequest(req)) if err != nil { switch { case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials", zap.Int("status_code", fiber.StatusBadRequest), zap.String("email", req.Email), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid credentials") case errors.Is(err, authentication.ErrUserSuspended): h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked", zap.Int("status_code", fiber.StatusForbidden), zap.String("email", req.Email), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusForbidden, "User login has been locked") default: h.mongoLoggerSvc.Error("Login failed", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Internal server error") } } if successRes.Role != domain.RoleSuperAdmin { h.mongoLoggerSvc.Warn("Login attempt: super-admin login of non-super-admin", zap.Int("status_code", fiber.StatusForbidden), zap.String("role", string(successRes.Role)), zap.String("email", req.Email), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusForbidden, "Only admin roles are allowed") } accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) if err != nil { h.mongoLoggerSvc.Error("Failed to create access token", zap.Int("status_code", fiber.StatusInternalServerError), zap.Int64("user_id", successRes.UserId), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") } res := loginUserRes{ AccessToken: accessToken, RefreshToken: successRes.RfToken, Role: string(successRes.Role), } h.mongoLoggerSvc.Info("Login successful", zap.Int("status_code", fiber.StatusOK), zap.Int64("user_id", successRes.UserId), zap.String("role", string(successRes.Role)), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil) } type refreshToken struct { AccessToken string `json:"access_token" validate:"required" example:""` RefreshToken string `json:"refresh_token" validate:"required" example:""` } // RefreshToken godoc // @Summary Refresh token // @Description Refresh token // @Tags auth // @Accept json // @Produce json // @Param refresh body refreshToken true "tokens" // @Success 200 {object} loginUserRes // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/auth/refresh [post] func (h *Handler) RefreshToken(c *fiber.Ctx) error { type loginUserRes struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` Role string `json:"role"` UserID int64 `json:"user_id"` } var req refreshToken if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse RefreshToken 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) } h.mongoLoggerSvc.Info("Failed to validate request", zap.Int("status_code", fiber.StatusBadRequest), zap.Any("validation_errors", valErrs), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, errMsg) } refreshToken, err := h.authSvc.RefreshToken(c.Context(), req.RefreshToken) if err != nil { switch { case errors.Is(err, authentication.ErrExpiredToken): h.mongoLoggerSvc.Info("Refresh token attempt failed: The refresh token has expired", zap.Int("status_code", fiber.StatusUnauthorized), zap.String("refresh_token", req.RefreshToken), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusUnauthorized, "The refresh token has expired") case errors.Is(err, authentication.ErrRefreshTokenNotFound): h.mongoLoggerSvc.Info("Refresh token attempt failed: Refresh token not found", zap.Int("status_code", fiber.StatusUnauthorized), zap.String("refresh_token", req.RefreshToken), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusUnauthorized, "Refresh token not found") default: h.mongoLoggerSvc.Error("Refresh token failed", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Internal server error") } } user, err := h.userSvc.GetUserByID(c.Context(), refreshToken.UserID) if err != nil { h.mongoLoggerSvc.Error("Failed to get user by ID during refresh", zap.Int("status_code", fiber.StatusInternalServerError), zap.Int64("user_id", refreshToken.UserID), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user information:"+err.Error()) } accessToken, err := jwtutil.CreateJwt(user.ID, user.Role, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) if err != nil { h.mongoLoggerSvc.Error("Failed to create new access token", zap.Int("status_code", fiber.StatusInternalServerError), zap.Int64("user_id", user.ID), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token:"+err.Error()) } res := loginUserRes{ AccessToken: accessToken, RefreshToken: req.RefreshToken, Role: string(user.Role), } h.mongoLoggerSvc.Info("Token Refreshed Successfully", zap.Int("status_code", fiber.StatusOK), zap.Int64("user_id", user.ID), zap.String("role", string(user.Role)), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusOK, "Refresh successful", res, nil) } type logoutReq struct { RefreshToken string `json:"refresh_token" validate:"required" example:""` } // LogOutuser godoc // @Summary Logout user // @Description Logout user // @Tags auth // @Accept json // @Produce json // @Param logout body logoutReq true "Logout user" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/auth/logout [post] func (h *Handler) LogOutuser(c *fiber.Ctx) error { var req logoutReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse LogOutuser 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) } h.mongoLoggerSvc.Info("LogOutuser validation failed", zap.String("errMsg", errMsg), zap.Int("status_code", fiber.StatusBadRequest), zap.Any("validation_errors", valErrs), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, errMsg) } err := h.authSvc.Logout(c.Context(), req.RefreshToken) if err != nil { switch { case errors.Is(err, authentication.ErrExpiredToken): h.mongoLoggerSvc.Info("Logout attempt failed:The refresh token has expired", zap.Int("status_code", fiber.StatusUnauthorized), zap.String("refresh_token", req.RefreshToken), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusUnauthorized, "The refresh token has expired") case errors.Is(err, authentication.ErrRefreshTokenNotFound): h.mongoLoggerSvc.Info("Logout attempt failed: Refresh token not found", zap.Int("status_code", fiber.StatusUnauthorized), zap.String("refresh_token", req.RefreshToken), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusUnauthorized, "Refresh token not found") default: h.mongoLoggerSvc.Error("Logout failed", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Internal server error"+err.Error()) } } h.mongoLoggerSvc.Info("Logout successful", zap.Int("status_code", fiber.StatusOK), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusOK, "Logout successful", nil, nil) }