UPDATE: Auth module, User module
Some checks failed
Build and Release / release (push) Failing after 1m25s
Some checks failed
Build and Release / release (push) Failing after 1m25s
This commit is contained in:
@@ -20,14 +20,15 @@ func NewAuthController(svc services.AuthService) *AuthController {
|
||||
}
|
||||
|
||||
// Signin godoc
|
||||
// @Summary Sign in an existing user
|
||||
// @Description Authenticate user and return token data
|
||||
// @Summary Sign in a user
|
||||
// @Description Authenticate user credentials and return access/refresh tokens
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body request.SignInDto true "Sign In request"
|
||||
// @Param request body request.SignInDto true "Sign In credentials"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 401 {object} response.CommonResponse "Invalid credentials"
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/signin [post]
|
||||
func (h *AuthController) Signin(c fiber.Ctx) error {
|
||||
@@ -57,12 +58,12 @@ func (h *AuthController) Signin(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// Signup godoc
|
||||
// @Summary Sign up a new user
|
||||
// @Description Create a new user account
|
||||
// @Summary Register a new user
|
||||
// @Description Create a new user account in the system
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body request.SignUpDto true "Sign Up request"
|
||||
// @Param request body request.SignUpDto true "Sign Up details"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
@@ -94,13 +95,14 @@ func (h *AuthController) Signup(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// RefreshToken godoc
|
||||
// @Summary Refresh access token
|
||||
// @Description Get a new access token using the user's current session/refresh token
|
||||
// @Summary Refresh session tokens
|
||||
// @Description Generate a new access token using a valid refresh token from context
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 401 {object} response.CommonResponse "Unauthorized or expired refresh token"
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/refresh [post]
|
||||
func (h *AuthController) RefreshToken(c fiber.Ctx) error {
|
||||
@@ -120,3 +122,116 @@ func (h *AuthController) RefreshToken(c fiber.Ctx) error {
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyToken godoc
|
||||
// @Summary Verify a security token
|
||||
// @Description Validate an OTP or email verification token
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body request.VerifyTokenDto true "Token verification data"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/token/verify [post]
|
||||
func (h *AuthController) VerifyToken(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
dto := &request.VerifyTokenDto{}
|
||||
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
res, err := h.service.VerifyToken(ctx, dto)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateToken godoc
|
||||
// @Summary Generate a new verification token
|
||||
// @Description Request a new token for specific actions like email confirmation
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body request.CreateTokenDto true "Token creation request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/token/create [post]
|
||||
func (h *AuthController) CreateToken(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
dto := &request.CreateTokenDto{}
|
||||
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
err := h.service.CreateToken(ctx, dto)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: nil,
|
||||
Message: "Token created successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ForgotPassword godoc
|
||||
// @Summary Handle forgotten password
|
||||
// @Description Initiate password recovery process for a user
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body request.ForgotPasswordDto true "Forgot Password request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/forgot-password [post]
|
||||
func (h *AuthController) ForgotPassword(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
dto := &request.ForgotPasswordDto{}
|
||||
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
err := h.service.ForgotPassword(ctx, dto)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: nil,
|
||||
Message: "Password reset successfully",
|
||||
})
|
||||
}
|
||||
276
internal/controllers/userController.go
Normal file
276
internal/controllers/userController.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"history-api/internal/dtos/request"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/services"
|
||||
"history-api/pkg/validator"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type UserController struct {
|
||||
service services.UserService
|
||||
}
|
||||
|
||||
func NewUserController(svc services.UserService) *UserController {
|
||||
return &UserController{service: svc}
|
||||
}
|
||||
|
||||
// GetUserCurrent godoc
|
||||
// @Summary Get current user profile
|
||||
// @Description Retrieve the profile information of the currently authenticated user
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/current [get]
|
||||
func (h *UserController) GetUserCurrent(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := h.service.GetUserCurrent(ctx, c.Locals("uid").(string))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfile godoc
|
||||
// @Summary Update user profile
|
||||
// @Description Update the profile details of the currently authenticated user
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param request body request.UpdateProfileDto true "Update Profile request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id} [put]
|
||||
func (h *UserController) UpdateProfile(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dto := &request.UpdateProfileDto{}
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
res, err := h.service.UpdateProfile(ctx, c.Locals("uid").(string), dto)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// ChangePassword godoc
|
||||
// @Summary Change user password
|
||||
// @Description Update the password for the currently authenticated user
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param request body request.ChangePasswordDto true "Change Password request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id}/password [patch]
|
||||
func (h *UserController) ChangePassword(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
dto := &request.ChangePasswordDto{}
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
err := h.service.ChangePassword(ctx, c.Locals("uid").(string), dto)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Message: "Password changed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// RestoreUser godoc
|
||||
// @Summary Restore a deleted user
|
||||
// @Description Restore a soft-deleted user account (Admin/Mod only)
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id}/restore [patch]
|
||||
func (h *UserController) RestoreUser(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
userId := c.Params("id")
|
||||
res, err := h.service.RestoreUser(ctx, userId)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteUser godoc
|
||||
// @Summary Delete a user
|
||||
// @Description Soft delete a user account (Admin/Mod only)
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id} [delete]
|
||||
func (h *UserController) DeleteUser(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
userId := c.Params("id")
|
||||
err := h.service.DeleteUser(ctx, userId)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Message: "User deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ChangeRoleUser godoc
|
||||
// @Summary Change user role
|
||||
// @Description Update the role of a user (Admin only)
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param request body request.ChangeRoleDto true "Change Role request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id}/role [patch]
|
||||
func (h *UserController) ChangeRoleUser(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dto := &request.ChangeRoleDto{}
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
user, err := h.service.ChangeRoleUser(ctx, dto)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: user,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserById godoc
|
||||
// @Summary Get user by ID
|
||||
// @Description Retrieve details of a specific user (Admin/Mod only)
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id} [get]
|
||||
func (h *UserController) GetUserById(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
userId := c.Params("id")
|
||||
res, err := h.service.GetUserByID(ctx, userId)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// Search godoc
|
||||
// @Summary Search users
|
||||
// @Description Search and filter users with pagination (Admin/Mod only)
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param query query request.SearchUserDto false "Search Query"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users [get]
|
||||
func (h *UserController) Search(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dto := &request.SearchUserDto{}
|
||||
if err := validator.ValidateQueryDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
res, err := h.service.Search(ctx, dto)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(res)
|
||||
}
|
||||
@@ -1,11 +1,36 @@
|
||||
package request
|
||||
|
||||
import "history-api/pkg/constants"
|
||||
|
||||
type SignUpDto struct {
|
||||
Email string `json:"email" validate:"required,min=5,max=255,email"`
|
||||
Password string `json:"password" validate:"required,min=8,max=64"`
|
||||
DisplayName string `json:"display_name" validate:"required,min=2,max=50"`
|
||||
TokenID string `json:"token_id" validate:"required,uuid"`
|
||||
}
|
||||
type SignInDto struct {
|
||||
Email string `json:"email" validate:"required,min=5,max=255,email"`
|
||||
Password string `json:"password" validate:"required,min=8,max=64"`
|
||||
}
|
||||
|
||||
type CreateTokenDto struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
TokenType constants.TokenType `json:"token_type" validate:"required,oneof=1 2 3 4"`
|
||||
}
|
||||
|
||||
type VerifyTokenDto struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
TokenType constants.TokenType `json:"token_type" validate:"required,oneof=1 2 3 4"`
|
||||
Token string `json:"token" validate:"required,len=6,numeric"`
|
||||
}
|
||||
|
||||
type ForgotPasswordDto struct {
|
||||
TokenID string `json:"token_id" validate:"required,uuid"`
|
||||
Email string `json:"email" validate:"required,min=5,max=255,email"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=8,max=64"`
|
||||
}
|
||||
|
||||
type SigninWith3rdDto struct {
|
||||
Provider string `json:"provider" validate:"required,oneof=google github facebook"`
|
||||
AccessToken string `json:"access_token" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
package request
|
||||
|
||||
import "history-api/pkg/constant"
|
||||
|
||||
type CreateUserDto struct {
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
DiscordUserId string `json:"discord_user_id" validate:"required"`
|
||||
Role []constant.Role `json:"role" validate:"required"`
|
||||
type UpdateProfileDto struct {
|
||||
DisplayName string `json:"display_name" validate:"omitempty,min=2,max=50"`
|
||||
FullName string `json:"full_name" validate:"omitempty,min=2,max=100"`
|
||||
AvatarUrl string `json:"avatar_url" validate:"omitempty,url"`
|
||||
Bio string `json:"bio" validate:"omitempty,max=255"`
|
||||
Location string `json:"location" validate:"omitempty,max=100"`
|
||||
Website string `json:"website" validate:"omitempty,url"`
|
||||
CountryCode string `json:"country_code" validate:"omitempty,len=2"`
|
||||
Phone string `json:"phone" validate:"omitempty,min=8,max=20"`
|
||||
}
|
||||
|
||||
type UpdateUserDto struct {
|
||||
Password *string `json:"password" validate:"omitempty"`
|
||||
DiscordUserId *string `json:"discord_user_id" validate:"omitempty"`
|
||||
Role *[]constant.Role `json:"role" validate:"omitempty"`
|
||||
type ChangePasswordDto struct {
|
||||
OldPassword string `json:"old_password" validate:"required,min=8,max=64"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=8,max=64,nefield=OldPassword"`
|
||||
}
|
||||
|
||||
type ChangeRoleDto struct {
|
||||
UserID string `json:"user_id" validate:"required,uuid"`
|
||||
Roles []string `json:"role_ids" validate:"required,min=1,dive,required,uuid"`
|
||||
}
|
||||
|
||||
type GetAllUserDto struct {
|
||||
CursorPaginationDto
|
||||
IsDeleted *bool `json:"is_deleted" query:"is_deleted" validate:"omitempty"`
|
||||
RoleIDs []string `json:"role_ids" query:"role_ids" validate:"omitempty,dive,uuid"`
|
||||
}
|
||||
|
||||
type CursorPaginationDto struct {
|
||||
Cursor string `json:"cursor" query:"cursor" validate:"omitempty,uuid"`
|
||||
Limit int `json:"limit" query:"limit" validate:"required,min=1,max=100"`
|
||||
Sort string `json:"sort" query:"sort" validate:"omitempty,oneof=created_at updated_at email display_name"`
|
||||
Order string `json:"order" query:"order" validate:"omitempty,oneof=asc desc"`
|
||||
}
|
||||
|
||||
type SearchUserDto struct {
|
||||
Username *string `query:"username" validate:"omitempty"`
|
||||
DiscordUserId *string `query:"discord_user_id" validate:"omitempty"`
|
||||
Role *[]constant.Role `query:"role" validate:"omitempty"`
|
||||
SortBy string `query:"sort_by" default:"created_at" validate:"oneof=created_at updated_at"`
|
||||
Order string `query:"order" default:"desc" validate:"oneof=asc desc"`
|
||||
Page int `query:"page" default:"1" validate:"min=1"`
|
||||
Limit int `query:"limit" default:"10" validate:"min=1,max=100"`
|
||||
CursorPaginationDto
|
||||
Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"`
|
||||
IsDeleted *bool `json:"is_deleted" query:"is_deleted" validate:"omitempty"`
|
||||
RoleIDs []string `json:"role_ids" query:"role_ids" validate:"omitempty,dive,uuid"`
|
||||
}
|
||||
|
||||
@@ -4,3 +4,7 @@ type AuthResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type VerifyTokenResponse struct {
|
||||
TokenID string `json:"token_id"`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/constants"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
@@ -13,7 +13,18 @@ type CommonResponse struct {
|
||||
}
|
||||
|
||||
type JWTClaims struct {
|
||||
UId string `json:"uid"`
|
||||
Roles []constant.Role `json:"roles"`
|
||||
UId string `json:"uid"`
|
||||
Roles []constants.Role `json:"roles"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
}
|
||||
|
||||
type PaginatedResponse struct {
|
||||
Data any `json:"data"`
|
||||
Status bool `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Pagination struct {
|
||||
NextCursor string `json:"next_cursor"`
|
||||
HasMore bool `json:"has_more"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package response
|
||||
|
||||
import "time"
|
||||
|
||||
type TokenResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
TokenType int16 `json:"token_type"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
}
|
||||
@@ -22,7 +22,6 @@ type User struct {
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
GoogleID pgtype.Text `json:"google_id"`
|
||||
AuthProvider string `json:"auth_provider"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
@@ -49,16 +48,6 @@ type UserRole struct {
|
||||
RoleID pgtype.UUID `json:"role_id"`
|
||||
}
|
||||
|
||||
type UserToken struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Token string `json:"token"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
TokenType int16 `json:"token_type"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type UserVerification struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
|
||||
@@ -13,19 +13,17 @@ import (
|
||||
|
||||
const addUserRole = `-- name: AddUserRole :exec
|
||||
INSERT INTO user_roles (user_id, role_id)
|
||||
SELECT $1, r.id
|
||||
FROM roles r
|
||||
WHERE r.name = $2
|
||||
SELECT $1, unnest($2::uuid[])
|
||||
ON CONFLICT DO NOTHING
|
||||
`
|
||||
|
||||
type AddUserRoleParams struct {
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Column2 []pgtype.UUID `json:"column_2"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddUserRole(ctx context.Context, arg AddUserRoleParams) error {
|
||||
_, err := q.db.Exec(ctx, addUserRole, arg.UserID, arg.Name)
|
||||
_, err := q.db.Exec(ctx, addUserRole, arg.UserID, arg.Column2)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -129,6 +127,38 @@ func (q *Queries) GetRoles(ctx context.Context) ([]Role, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getRolesByIDs = `-- name: GetRolesByIDs :many
|
||||
SELECT id, name, is_deleted, created_at, updated_at
|
||||
FROM roles
|
||||
WHERE id = ANY($1::uuid[]) AND is_deleted = false
|
||||
`
|
||||
|
||||
func (q *Queries) GetRolesByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Role, error) {
|
||||
rows, err := q.db.Query(ctx, getRolesByIDs, dollar_1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Role{}
|
||||
for rows.Next() {
|
||||
var i Role
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const removeAllRolesFromUser = `-- name: RemoveAllRolesFromUser :exec
|
||||
DELETE FROM user_roles
|
||||
WHERE user_id = $1
|
||||
|
||||
@@ -77,7 +77,6 @@ SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
@@ -116,7 +115,6 @@ type GetUserByEmailRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
@@ -132,7 +130,6 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEm
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
@@ -148,7 +145,6 @@ SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
@@ -190,7 +186,6 @@ type GetUserByIDRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
@@ -207,7 +202,79 @@ func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (GetUserByIDR
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Profile,
|
||||
&i.Roles,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByIDWithoutDeleted = `-- name: GetUserByIDWithoutDeleted :one
|
||||
SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
|
||||
-- profile JSON
|
||||
(
|
||||
SELECT json_build_object(
|
||||
'display_name', p.display_name,
|
||||
'full_name', p.full_name,
|
||||
'avatar_url', p.avatar_url,
|
||||
'bio', p.bio,
|
||||
'location', p.location,
|
||||
'website', p.website,
|
||||
'country_code', p.country_code,
|
||||
'phone', p.phone
|
||||
)
|
||||
FROM user_profiles p
|
||||
WHERE p.user_id = u.id
|
||||
) AS profile,
|
||||
|
||||
-- roles JSON
|
||||
(
|
||||
SELECT COALESCE(
|
||||
json_agg(json_build_object('id', r.id, 'name', r.name)),
|
||||
'[]'
|
||||
)::json
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON ur.role_id = r.id
|
||||
WHERE ur.user_id = u.id
|
||||
) AS roles
|
||||
|
||||
FROM users u
|
||||
WHERE u.id = $1
|
||||
`
|
||||
|
||||
type GetUserByIDWithoutDeletedRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
Profile []byte `json:"profile"`
|
||||
Roles []byte `json:"roles"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserByIDWithoutDeleted(ctx context.Context, id pgtype.UUID) (GetUserByIDWithoutDeletedRow, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByIDWithoutDeleted, id)
|
||||
var i GetUserByIDWithoutDeletedRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
@@ -224,7 +291,127 @@ SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
|
||||
-- profile JSON
|
||||
(
|
||||
SELECT json_build_object(
|
||||
'display_name', p.display_name,
|
||||
'full_name', p.full_name,
|
||||
'avatar_url', p.avatar_url,
|
||||
'bio', p.bio,
|
||||
'location', p.location,
|
||||
'website', p.website,
|
||||
'country_code', p.country_code,
|
||||
'phone', p.phone
|
||||
)
|
||||
FROM user_profiles p
|
||||
WHERE p.user_id = u.id
|
||||
) AS profile,
|
||||
|
||||
-- roles JSON
|
||||
(
|
||||
SELECT COALESCE(
|
||||
json_agg(json_build_object('id', r.id, 'name', r.name)),
|
||||
'[]'
|
||||
)::json
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON ur.role_id = r.id
|
||||
WHERE ur.user_id = u.id
|
||||
) AS roles
|
||||
|
||||
FROM users u
|
||||
WHERE
|
||||
($1::uuid IS NULL OR u.id > $1::uuid)
|
||||
AND ($2::boolean IS NULL OR u.is_deleted = $2::boolean)
|
||||
AND (
|
||||
$3::uuid[] IS NULL OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM user_roles ur2
|
||||
WHERE ur2.user_id = u.id AND ur2.role_id = ANY($3::uuid[])
|
||||
)
|
||||
)
|
||||
ORDER BY u.id ASC
|
||||
LIMIT $4
|
||||
`
|
||||
|
||||
type GetUsersParams struct {
|
||||
Cursor pgtype.UUID `json:"cursor"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
RoleIds []pgtype.UUID `json:"role_ids"`
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetUsersRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
Profile []byte `json:"profile"`
|
||||
Roles []byte `json:"roles"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) {
|
||||
rows, err := q.db.Query(ctx, getUsers,
|
||||
arg.Cursor,
|
||||
arg.IsDeleted,
|
||||
arg.RoleIds,
|
||||
arg.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetUsersRow{}
|
||||
for rows.Next() {
|
||||
var i GetUsersRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Profile,
|
||||
&i.Roles,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const restoreUser = `-- name: RestoreUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
is_deleted = false
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) RestoreUser(ctx context.Context, id pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, restoreUser, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const searchUsers = `-- name: SearchUsers :many
|
||||
SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
@@ -257,14 +444,44 @@ SELECT
|
||||
) AS roles
|
||||
|
||||
FROM users u
|
||||
WHERE u.is_deleted = false
|
||||
WHERE
|
||||
($1::uuid IS NULL OR u.id > $1::uuid)
|
||||
|
||||
AND ($2::boolean IS NULL OR u.is_deleted = $2::boolean)
|
||||
AND (
|
||||
$3::uuid[] IS NULL OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM user_roles ur2
|
||||
WHERE ur2.user_id = u.id AND ur2.role_id = ANY($3::uuid[])
|
||||
)
|
||||
)
|
||||
|
||||
AND ($4::uuid IS NULL OR u.id = $4::uuid)
|
||||
AND (
|
||||
$5::text IS NULL OR
|
||||
u.email ILIKE '%' || $5::text || '%' OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM user_profiles p
|
||||
WHERE p.user_id = u.id AND p.display_name ILIKE '%' || $5::text || '%'
|
||||
)
|
||||
)
|
||||
ORDER BY u.id ASC
|
||||
LIMIT $6
|
||||
`
|
||||
|
||||
type GetUsersRow struct {
|
||||
type SearchUsersParams struct {
|
||||
Cursor pgtype.UUID `json:"cursor"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
RoleIds []pgtype.UUID `json:"role_ids"`
|
||||
SearchID pgtype.UUID `json:"search_id"`
|
||||
SearchText pgtype.Text `json:"search_text"`
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
type SearchUsersRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
@@ -274,20 +491,26 @@ type GetUsersRow struct {
|
||||
Roles []byte `json:"roles"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUsers(ctx context.Context) ([]GetUsersRow, error) {
|
||||
rows, err := q.db.Query(ctx, getUsers)
|
||||
func (q *Queries) SearchUsers(ctx context.Context, arg SearchUsersParams) ([]SearchUsersRow, error) {
|
||||
rows, err := q.db.Query(ctx, searchUsers,
|
||||
arg.Cursor,
|
||||
arg.IsDeleted,
|
||||
arg.RoleIds,
|
||||
arg.SearchID,
|
||||
arg.SearchText,
|
||||
arg.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetUsersRow{}
|
||||
items := []SearchUsersRow{}
|
||||
for rows.Next() {
|
||||
var i GetUsersRow
|
||||
var i SearchUsersRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
@@ -306,18 +529,6 @@ func (q *Queries) GetUsers(ctx context.Context) ([]GetUsersRow, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const restoreUser = `-- name: RestoreUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
is_deleted = false
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) RestoreUser(ctx context.Context, id pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, restoreUser, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateTokenVersion = `-- name: UpdateTokenVersion :exec
|
||||
UPDATE users
|
||||
SET token_version = $2
|
||||
@@ -432,18 +643,15 @@ INSERT INTO users (
|
||||
email,
|
||||
password_hash,
|
||||
google_id,
|
||||
auth_provider,
|
||||
is_verified
|
||||
auth_provider
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5
|
||||
$1, $2, $3, $4
|
||||
)
|
||||
ON CONFLICT (email)
|
||||
DO UPDATE SET
|
||||
google_id = EXCLUDED.google_id,
|
||||
auth_provider = EXCLUDED.auth_provider,
|
||||
is_verified = users.is_verified OR EXCLUDED.is_verified,
|
||||
updated_at = now()
|
||||
RETURNING id, email, password_hash, google_id, auth_provider, is_verified, is_deleted, token_version, refresh_token, created_at, updated_at
|
||||
auth_provider = EXCLUDED.auth_provider
|
||||
RETURNING id, email, password_hash, google_id, auth_provider, is_deleted, token_version, refresh_token, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpsertUserParams struct {
|
||||
@@ -451,7 +659,6 @@ type UpsertUserParams struct {
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
GoogleID pgtype.Text `json:"google_id"`
|
||||
AuthProvider string `json:"auth_provider"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, error) {
|
||||
@@ -460,7 +667,6 @@ func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, e
|
||||
arg.PasswordHash,
|
||||
arg.GoogleID,
|
||||
arg.AuthProvider,
|
||||
arg.IsVerified,
|
||||
)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
@@ -469,7 +675,6 @@ func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, e
|
||||
&i.PasswordHash,
|
||||
&i.GoogleID,
|
||||
&i.AuthProvider,
|
||||
&i.IsVerified,
|
||||
&i.IsDeleted,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
@@ -478,16 +683,3 @@ func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, e
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const verifyUser = `-- name: VerifyUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
is_verified = true
|
||||
WHERE id = $1
|
||||
AND is_deleted = false
|
||||
`
|
||||
|
||||
func (q *Queries) VerifyUser(ctx context.Context, id pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, verifyUser, id)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,16 +2,18 @@ package middlewares
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/config"
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/constants"
|
||||
"slices"
|
||||
|
||||
jwtware "github.com/gofiber/contrib/v3/jwt"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/extractors"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func JwtAccess() fiber.Handler {
|
||||
func JwtAccess(userRepo repositories.UserRepository) fiber.Handler {
|
||||
jwtSecret, err := config.GetConfig("JWT_SECRET")
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -20,13 +22,13 @@ func JwtAccess() fiber.Handler {
|
||||
return jwtware.New(jwtware.Config{
|
||||
SigningKey: jwtware.SigningKey{Key: []byte(jwtSecret)},
|
||||
ErrorHandler: jwtError,
|
||||
SuccessHandler: jwtSuccess,
|
||||
SuccessHandler: jwtSuccess(userRepo),
|
||||
Extractor: extractors.FromAuthHeader("Bearer"),
|
||||
Claims: &response.JWTClaims{},
|
||||
})
|
||||
}
|
||||
|
||||
func JwtRefresh() fiber.Handler {
|
||||
func JwtRefresh(userRepo repositories.UserRepository) fiber.Handler {
|
||||
jwtRefreshSecret, err := config.GetConfig("JWT_REFRESH_SECRET")
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -35,41 +37,61 @@ func JwtRefresh() fiber.Handler {
|
||||
return jwtware.New(jwtware.Config{
|
||||
SigningKey: jwtware.SigningKey{Key: []byte(jwtRefreshSecret)},
|
||||
ErrorHandler: jwtError,
|
||||
SuccessHandler: jwtSuccess,
|
||||
SuccessHandler: jwtSuccess(userRepo),
|
||||
Extractor: extractors.FromAuthHeader("Bearer"),
|
||||
Claims: &response.JWTClaims{},
|
||||
})
|
||||
}
|
||||
|
||||
func jwtSuccess(c fiber.Ctx) error {
|
||||
user := jwtware.FromContext(c)
|
||||
unauthorized := func() error {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: "Invalid or missing token",
|
||||
})
|
||||
func jwtSuccess(userRepo repositories.UserRepository) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
user := jwtware.FromContext(c)
|
||||
|
||||
unauthorized := func() error {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: "Invalid or missing token",
|
||||
})
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return unauthorized()
|
||||
}
|
||||
|
||||
claims, ok := user.Claims.(*response.JWTClaims)
|
||||
if !ok {
|
||||
return unauthorized()
|
||||
}
|
||||
|
||||
if slices.Contains(claims.Roles, constants.BANNED) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: "User account is banned",
|
||||
})
|
||||
}
|
||||
|
||||
var pgID pgtype.UUID
|
||||
err := pgID.Scan(claims.UId)
|
||||
if err != nil {
|
||||
return unauthorized()
|
||||
}
|
||||
tokenVersion, err := userRepo.GetTokenVersion(c.Context(), pgID)
|
||||
if err != nil {
|
||||
return unauthorized()
|
||||
}
|
||||
|
||||
if tokenVersion != claims.TokenVersion {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: "Token has been invalidated",
|
||||
})
|
||||
}
|
||||
|
||||
c.Locals("uid", claims.UId)
|
||||
c.Locals("user_claims", claims)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return unauthorized()
|
||||
}
|
||||
|
||||
claims, ok := user.Claims.(*response.JWTClaims)
|
||||
if !ok {
|
||||
return unauthorized()
|
||||
}
|
||||
|
||||
if slices.Contains(claims.Roles, constant.BANNED) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: "User account is banned",
|
||||
})
|
||||
}
|
||||
|
||||
c.Locals("uid", claims.UId)
|
||||
c.Locals("user_claims", claims)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
func jwtError(c fiber.Ctx, err error) error {
|
||||
if err.Error() == "Missing or malformed JWT" {
|
||||
|
||||
@@ -2,13 +2,13 @@ package middlewares
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/constants"
|
||||
"slices"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func getRoles(c fiber.Ctx) ([]constant.Role, error) {
|
||||
func getRoles(c fiber.Ctx) ([]constants.Role, error) {
|
||||
claimsVal := c.Locals("user_claims")
|
||||
if claimsVal == nil {
|
||||
return nil, fiber.ErrUnauthorized
|
||||
@@ -22,7 +22,7 @@ func getRoles(c fiber.Ctx) ([]constant.Role, error) {
|
||||
return claims.Roles, nil
|
||||
}
|
||||
|
||||
func RequireAnyRole(required ...constant.Role) fiber.Handler {
|
||||
func RequireAnyRole(required ...constants.Role) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
userRoles, err := getRoles(c)
|
||||
if err != nil {
|
||||
@@ -43,7 +43,7 @@ func RequireAnyRole(required ...constant.Role) fiber.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func RequireAllRoles(required ...constant.Role) fiber.Handler {
|
||||
func RequireAllRoles(required ...constants.Role) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
userRoles, err := getRoles(c)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/constants"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -44,6 +44,13 @@ func (r *RoleEntity) ToResponse() *response.RoleResponse {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RoleEntity) ToRoleSimple() *RoleSimple {
|
||||
return &RoleSimple{
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func RolesEntityToResponse(rs []*RoleEntity) []*response.RoleResponse {
|
||||
out := make([]*response.RoleResponse, len(rs))
|
||||
for i := range rs {
|
||||
@@ -52,10 +59,10 @@ func RolesEntityToResponse(rs []*RoleEntity) []*response.RoleResponse {
|
||||
return out
|
||||
}
|
||||
|
||||
func RolesEntityToRoleConstant(rs []*RoleSimple) []constant.Role {
|
||||
out := make([]constant.Role, len(rs))
|
||||
func RolesEntityToRoleConstant(rs []*RoleSimple) []constants.Role {
|
||||
out := make([]constants.Role, len(rs))
|
||||
for i := range rs {
|
||||
data, ok := constant.ParseRole(rs[i].Name)
|
||||
data, ok := constants.ParseRole(rs[i].Name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,35 +1,9 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/convert"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
import "history-api/pkg/constants"
|
||||
|
||||
type TokenEntity struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Token string `json:"token"`
|
||||
TokenType int16 `json:"token_type"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (t *TokenEntity) ToResponse() *response.TokenResponse {
|
||||
return &response.TokenResponse{
|
||||
ID: convert.UUIDToString(t.ID),
|
||||
UserID: convert.UUIDToString(t.UserID),
|
||||
TokenType: t.TokenType,
|
||||
ExpiresAt: convert.TimeToPtr(t.ExpiresAt),
|
||||
CreatedAt: convert.TimeToPtr(t.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func TokensEntityToResponse(ts []*TokenEntity) []*response.TokenResponse {
|
||||
out := make([]*response.TokenResponse, len(ts))
|
||||
for i := range ts {
|
||||
out[i] = ts[i].ToResponse()
|
||||
}
|
||||
return out
|
||||
Email string `json:"email"`
|
||||
Token string `json:"token"`
|
||||
TokenType constants.TokenType `json:"token_type"`
|
||||
}
|
||||
|
||||
@@ -2,19 +2,22 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/convert"
|
||||
)
|
||||
|
||||
type RoleRepository interface {
|
||||
GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error)
|
||||
GetByIDs(ctx context.Context, ids []string) ([]*models.RoleEntity, error)
|
||||
GetByname(ctx context.Context, name string) (*models.RoleEntity, error)
|
||||
All(ctx context.Context) ([]*models.RoleEntity, error)
|
||||
Create(ctx context.Context, name string) (*models.RoleEntity, error)
|
||||
@@ -39,6 +42,56 @@ func NewRoleRepository(db sqlc.DBTX, c cache.Cache) RoleRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *roleRepository) generateQueryKey(prefix string, params any) string {
|
||||
b, _ := json.Marshal(params)
|
||||
hash := fmt.Sprintf("%x", md5.Sum(b))
|
||||
return fmt.Sprintf("%s:%s", prefix, hash)
|
||||
}
|
||||
|
||||
func (r *roleRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.RoleEntity, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*models.RoleEntity{}, nil
|
||||
}
|
||||
keys := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
keys[i] = fmt.Sprintf("role:id:%s", id)
|
||||
}
|
||||
raws := r.c.MGet(ctx, keys...)
|
||||
|
||||
var roles []*models.RoleEntity
|
||||
missingRolesToCache := make(map[string]any)
|
||||
|
||||
for i, b := range raws {
|
||||
if len(b) > 0 {
|
||||
var u models.RoleEntity
|
||||
if err := json.Unmarshal(b, &u); err == nil {
|
||||
roles = append(roles, &u)
|
||||
}
|
||||
} else {
|
||||
pgId := pgtype.UUID{}
|
||||
err := pgId.Scan(ids[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dbRole, err := r.GetByID(ctx, pgId)
|
||||
if err == nil && dbRole != nil {
|
||||
roles = append(roles, dbRole)
|
||||
missingRolesToCache[keys[i]] = dbRole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingRolesToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, missingRolesToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.RoleEntity, error) {
|
||||
return r.getByIDsWithFallback(ctx, ids)
|
||||
}
|
||||
|
||||
func (r *roleRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error) {
|
||||
cacheId := fmt.Sprintf("role:id:%s", convert.UUIDToString(id))
|
||||
var role models.RoleEntity
|
||||
@@ -59,7 +112,7 @@ func (r *roleRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.R
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
_ = r.c.Set(ctx, cacheId, role, 5*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration)
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
@@ -83,7 +136,7 @@ func (r *roleRepository) GetByname(ctx context.Context, name string) (*models.Ro
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, role, 5*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration)
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
@@ -104,7 +157,7 @@ func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleE
|
||||
fmt.Sprintf("role:name:%s", name): role,
|
||||
fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)): role,
|
||||
}
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
@@ -125,7 +178,7 @@ func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParam
|
||||
fmt.Sprintf("role:name:%s", row.Name): role,
|
||||
fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)): role,
|
||||
}
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -50,7 +51,7 @@ func (r *tileRepository) GetMetadata(ctx context.Context) (map[string]string, er
|
||||
metadata[name] = value
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, metadata, 10*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, metadata, constants.NormalCacheDuration)
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
79
internal/repositories/tokenRepository.go
Normal file
79
internal/repositories/tokenRepository.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
)
|
||||
|
||||
type TokenRepository interface {
|
||||
CheckCooldown(ctx context.Context, email string, tokenType constants.TokenType) (bool, error)
|
||||
Get(ctx context.Context, email string, tokenType constants.TokenType) (*models.TokenEntity, error)
|
||||
Create(ctx context.Context, token *models.TokenEntity) error
|
||||
Delete(ctx context.Context, email string, tokenType constants.TokenType) error
|
||||
CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error)
|
||||
CreateVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error
|
||||
DeleteVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error
|
||||
}
|
||||
|
||||
type tokenRepository struct {
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewTokenRepository(c cache.Cache) TokenRepository {
|
||||
return &tokenRepository{
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tokenRepository) CreateVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error {
|
||||
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
||||
return t.c.Set(ctx, cacheKey, true, constants.TokenVerifiedDuration)
|
||||
}
|
||||
|
||||
func (t *tokenRepository) DeleteVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error {
|
||||
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
||||
return t.c.Del(ctx, cacheKey)
|
||||
}
|
||||
|
||||
|
||||
func (t *tokenRepository) CheckCooldown(ctx context.Context, email string, tokenType constants.TokenType) (bool, error) {
|
||||
cacheKey := fmt.Sprintf("token:cooldown:%d:%s", tokenType.Value(), email)
|
||||
exists, err := t.c.Exists(ctx, cacheKey)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func (t *tokenRepository) CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error) {
|
||||
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
||||
exists, err := t.c.Exists(ctx, cacheKey)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func (t *tokenRepository) Create(ctx context.Context, token *models.TokenEntity) error {
|
||||
cacheKey := fmt.Sprintf("token:%d:%s", token.TokenType.Value(), token.Email)
|
||||
err := t.c.Set(ctx, cacheKey, token, constants.TokenExpirationDuration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cooldownKey := fmt.Sprintf("token:cooldown:%d:%s", token.TokenType.Value(), token.Email)
|
||||
return t.c.Set(ctx, cooldownKey, true, constants.TokenCooldownDuration)
|
||||
}
|
||||
|
||||
func (t *tokenRepository) Delete(ctx context.Context, email string, tokenType constants.TokenType) error {
|
||||
cacheKey := fmt.Sprintf("token:%d:%s", tokenType.Value(), email)
|
||||
cooldownKey := fmt.Sprintf("token:cooldown:%d:%s", tokenType.Value(), email)
|
||||
_ = t.c.Del(ctx, cooldownKey)
|
||||
return t.c.Del(ctx, cacheKey)
|
||||
}
|
||||
|
||||
func (t *tokenRepository) Get(ctx context.Context, email string, tokenType constants.TokenType) (*models.TokenEntity, error) {
|
||||
cacheKey := fmt.Sprintf("token:%d:%s", tokenType.Value(), email)
|
||||
var token models.TokenEntity
|
||||
err := t.c.Get(ctx, cacheKey, &token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
@@ -2,21 +2,25 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/convert"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error)
|
||||
GetByIDWithoutDeleted(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error)
|
||||
GetByEmail(ctx context.Context, email string) (*models.UserEntity, error)
|
||||
All(ctx context.Context) ([]*models.UserEntity, error)
|
||||
All(ctx context.Context, params sqlc.GetUsersParams) ([]*models.UserEntity, error)
|
||||
Search(ctx context.Context, params sqlc.SearchUsersParams) ([]*models.UserEntity, error)
|
||||
UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error)
|
||||
CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error)
|
||||
UpdateProfile(ctx context.Context, params sqlc.UpdateUserProfileParams) (*models.UserEntity, error)
|
||||
@@ -24,7 +28,6 @@ type UserRepository interface {
|
||||
UpdateRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error
|
||||
GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, error)
|
||||
UpdateTokenVersion(ctx context.Context, params sqlc.UpdateTokenVersionParams) error
|
||||
Verify(ctx context.Context, id pgtype.UUID) error
|
||||
Delete(ctx context.Context, id pgtype.UUID) error
|
||||
Restore(ctx context.Context, id pgtype.UUID) error
|
||||
}
|
||||
@@ -41,6 +44,52 @@ func NewUserRepository(db sqlc.DBTX, c cache.Cache) UserRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *userRepository) generateQueryKey(prefix string, params any) string {
|
||||
b, _ := json.Marshal(params)
|
||||
hash := fmt.Sprintf("%x", md5.Sum(b))
|
||||
return fmt.Sprintf("%s:%s", prefix, hash)
|
||||
}
|
||||
|
||||
func (r *userRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.UserEntity, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*models.UserEntity{}, nil
|
||||
}
|
||||
keys := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
keys[i] = fmt.Sprintf("user:id:%s", id)
|
||||
}
|
||||
raws := r.c.MGet(ctx, keys...)
|
||||
|
||||
var users []*models.UserEntity
|
||||
missingUsersToCache := make(map[string]any)
|
||||
|
||||
for i, b := range raws {
|
||||
if len(b) > 0 {
|
||||
var u models.UserEntity
|
||||
if err := json.Unmarshal(b, &u); err == nil {
|
||||
users = append(users, &u)
|
||||
}
|
||||
} else {
|
||||
pgId := pgtype.UUID{}
|
||||
err := pgId.Scan(ids[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dbUser, err := r.GetByID(ctx, pgId)
|
||||
if err == nil && dbUser != nil {
|
||||
users = append(users, dbUser)
|
||||
missingUsersToCache[keys[i]] = dbUser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingUsersToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, missingUsersToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error) {
|
||||
cacheId := fmt.Sprintf("user:id:%s", convert.UUIDToString(id))
|
||||
var user models.UserEntity
|
||||
@@ -58,7 +107,6 @@ func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.U
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
@@ -73,7 +121,43 @@ func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.U
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, user, constants.NormalCacheDuration)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByIDWithoutDeleted(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error) {
|
||||
cacheId := fmt.Sprintf("user:deleted:id:%s", convert.UUIDToString(id))
|
||||
var user models.UserEntity
|
||||
err := r.c.Get(ctx, cacheId, &user)
|
||||
if err == nil {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
row, err := r.q.GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user = models.UserEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
|
||||
if err := user.ParseRoles(row.Roles); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := user.ParseProfile(row.Profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, user, constants.NormalCacheDuration)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
@@ -96,7 +180,6 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
@@ -111,7 +194,7 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, user, constants.NormalCacheDuration)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
@@ -121,12 +204,17 @@ func (r *userRepository) UpsertUser(ctx context.Context, params sqlc.UpsertUserP
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
bgCtx := context.Background()
|
||||
|
||||
_ = r.c.DelByPattern(bgCtx, "user:all*")
|
||||
_ = r.c.DelByPattern(bgCtx, "user:search*")
|
||||
}()
|
||||
|
||||
return &models.UserEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
@@ -161,7 +249,7 @@ func (r *userRepository) UpdateProfile(ctx context.Context, params sqlc.UpdateUs
|
||||
fmt.Sprintf("user:email:%s", user.Email): user,
|
||||
fmt.Sprintf("user:id:%s", user.ID): user,
|
||||
}
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -183,19 +271,27 @@ func (r *userRepository) CreateProfile(ctx context.Context, params sqlc.CreateUs
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error) {
|
||||
rows, err := r.q.GetUsers(ctx)
|
||||
func (r *userRepository) All(ctx context.Context, params sqlc.GetUsersParams) ([]*models.UserEntity, error) {
|
||||
queryKey := r.generateQueryKey("user:all", params)
|
||||
|
||||
var cachedIDs []string
|
||||
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
|
||||
return r.getByIDsWithFallback(ctx, cachedIDs)
|
||||
}
|
||||
rows, err := r.q.GetUsers(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var users []*models.UserEntity
|
||||
var ids []string
|
||||
usersToCache := make(map[string]any)
|
||||
|
||||
for _, row := range rows {
|
||||
user := &models.UserEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
@@ -205,44 +301,75 @@ func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error)
|
||||
if err := user.ParseRoles(row.Roles); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := user.ParseProfile(row.Profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users = append(users, user)
|
||||
ids = append(ids, user.ID)
|
||||
|
||||
usersToCache[fmt.Sprintf("user:id:%s", user.ID)] = user
|
||||
}
|
||||
|
||||
if len(usersToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, usersToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Verify(ctx context.Context, id pgtype.UUID) error {
|
||||
user, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
func (r *userRepository) Search(ctx context.Context, params sqlc.SearchUsersParams) ([]*models.UserEntity, error) {
|
||||
queryKey := r.generateQueryKey("user:search", params)
|
||||
|
||||
var cachedIDs []string
|
||||
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
|
||||
return r.getByIDsWithFallback(ctx, cachedIDs)
|
||||
}
|
||||
|
||||
err = r.q.VerifyUser(ctx, id)
|
||||
rows, err := r.q.SearchUsers(ctx, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = r.q.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
|
||||
ID: id,
|
||||
TokenVersion: user.TokenVersion + 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user.IsVerified = true
|
||||
user.TokenVersion += 1
|
||||
var users []*models.UserEntity
|
||||
var ids []string
|
||||
usersToCache := make(map[string]any)
|
||||
|
||||
mapCache := map[string]any{
|
||||
fmt.Sprintf("user:email:%s", user.Email): user,
|
||||
fmt.Sprintf("user:id:%s", user.ID): user,
|
||||
for _, row := range rows {
|
||||
user := &models.UserEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
|
||||
if err := user.ParseRoles(row.Roles); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := user.ParseProfile(row.Profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users = append(users, user)
|
||||
ids = append(ids, user.ID)
|
||||
usersToCache[fmt.Sprintf("user:id:%s", user.ID)] = user
|
||||
}
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
return nil
|
||||
|
||||
if len(usersToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, usersToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Delete(ctx context.Context, id pgtype.UUID) error {
|
||||
@@ -288,7 +415,7 @@ func (r *userRepository) GetTokenVersion(ctx context.Context, id pgtype.UUID) (i
|
||||
return 0, err
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, raw, 5*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, raw, constants.NormalCacheDuration)
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
@@ -299,7 +426,7 @@ func (r *userRepository) UpdateTokenVersion(ctx context.Context, params sqlc.Upd
|
||||
}
|
||||
|
||||
cacheId := fmt.Sprintf("user:token:%s", convert.UUIDToString(params.ID))
|
||||
_ = r.c.Set(ctx, cacheId, params.TokenVersion, 5*time.Minute)
|
||||
_ = r.c.Set(ctx, cacheId, params.TokenVersion, constants.NormalCacheDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -328,7 +455,7 @@ func (r *userRepository) UpdatePassword(ctx context.Context, params sqlc.UpdateU
|
||||
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
|
||||
}
|
||||
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -349,6 +476,6 @@ func (r *userRepository) UpdateRefreshToken(ctx context.Context, params sqlc.Upd
|
||||
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
|
||||
}
|
||||
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,13 +3,17 @@ package routes
|
||||
import (
|
||||
"history-api/internal/controllers"
|
||||
"history-api/internal/middlewares"
|
||||
"history-api/internal/repositories"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func AuthRoutes(app *fiber.App, controller *controllers.AuthController) {
|
||||
func AuthRoutes(app *fiber.App, controller *controllers.AuthController, userRepo repositories.UserRepository) {
|
||||
route := app.Group("/auth")
|
||||
route.Post("/signin", controller.Signin)
|
||||
route.Post("/signup", controller.Signup)
|
||||
route.Post("/refresh", middlewares.JwtRefresh(), controller.RefreshToken)
|
||||
route.Post("/refresh", middlewares.JwtRefresh(userRepo), controller.RefreshToken)
|
||||
route.Post("/token/create", controller.CreateToken)
|
||||
route.Post("/token/verify", controller.VerifyToken)
|
||||
route.Post("/forgot-password", controller.ForgotPassword)
|
||||
}
|
||||
|
||||
65
internal/routes/userRoute.go
Normal file
65
internal/routes/userRoute.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"history-api/internal/controllers"
|
||||
"history-api/internal/middlewares"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/constants"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo repositories.UserRepository) {
|
||||
route := app.Group("/users")
|
||||
|
||||
route.Get(
|
||||
"/",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.Search,
|
||||
)
|
||||
route.Get(
|
||||
"/:id",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.Search,
|
||||
)
|
||||
|
||||
route.Delete(
|
||||
"/:id",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.DeleteUser,
|
||||
)
|
||||
route.Patch(
|
||||
"/:id/restore",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.RestoreUser,
|
||||
)
|
||||
|
||||
route.Patch(
|
||||
"/:id/role",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN),
|
||||
controller.ChangeRoleUser,
|
||||
)
|
||||
|
||||
route.Patch(
|
||||
"/:id/password",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.ChangePassword,
|
||||
)
|
||||
|
||||
route.Get(
|
||||
"/current",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.GetUserCurrent,
|
||||
)
|
||||
|
||||
route.Put(
|
||||
"/:id",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.UpdateProfile,
|
||||
)
|
||||
}
|
||||
@@ -2,19 +2,25 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"history-api/internal/dtos/request"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/models"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/config"
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/convert"
|
||||
"math/big"
|
||||
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -22,29 +28,35 @@ import (
|
||||
type AuthService interface {
|
||||
Signin(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error)
|
||||
Signup(ctx context.Context, dto *request.SignUpDto) (*response.AuthResponse, error)
|
||||
ForgotPassword(ctx context.Context) error
|
||||
VerifyToken(ctx context.Context) error
|
||||
CreateToken(ctx context.Context) error
|
||||
SigninWith3rd(ctx context.Context) error
|
||||
ForgotPassword(ctx context.Context, dto *request.ForgotPasswordDto) error
|
||||
VerifyToken(ctx context.Context, dto *request.VerifyTokenDto) (*response.VerifyTokenResponse, error)
|
||||
CreateToken(ctx context.Context, dto *request.CreateTokenDto) error
|
||||
SigninWith3rd(ctx context.Context, dto *request.SigninWith3rdDto) error
|
||||
RefreshToken(ctx context.Context, id string) (*response.AuthResponse, error)
|
||||
}
|
||||
|
||||
type authService struct {
|
||||
userRepo repositories.UserRepository
|
||||
roleRepo repositories.RoleRepository
|
||||
userRepo repositories.UserRepository
|
||||
roleRepo repositories.RoleRepository
|
||||
tokenRepo repositories.TokenRepository
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewAuthService(
|
||||
userRepo repositories.UserRepository,
|
||||
roleRepo repositories.RoleRepository,
|
||||
tokenRepo repositories.TokenRepository,
|
||||
c cache.Cache,
|
||||
) AuthService {
|
||||
return &authService{
|
||||
userRepo: userRepo,
|
||||
roleRepo: roleRepo,
|
||||
userRepo: userRepo,
|
||||
roleRepo: roleRepo,
|
||||
tokenRepo: tokenRepo,
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *authService) genToken(Uid string, role []constant.Role) (*response.AuthResponse, error) {
|
||||
func (a *authService) genToken(user *models.UserEntity) (*response.AuthResponse, error) {
|
||||
jwtSecret, err := config.GetConfig("JWT_SECRET")
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "missing JWT_SECRET in environment")
|
||||
@@ -59,18 +71,20 @@ func (a *authService) genToken(Uid string, role []constant.Role) (*response.Auth
|
||||
}
|
||||
|
||||
claimsAccess := &response.JWTClaims{
|
||||
UId: Uid,
|
||||
Roles: role,
|
||||
UId: user.ID,
|
||||
Roles: models.RolesEntityToRoleConstant(user.Roles),
|
||||
TokenVersion: user.TokenVersion,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(constants.AccessTokenDuration)),
|
||||
},
|
||||
}
|
||||
|
||||
claimsRefresh := &response.JWTClaims{
|
||||
UId: Uid,
|
||||
Roles: role,
|
||||
UId: user.ID,
|
||||
Roles: models.RolesEntityToRoleConstant(user.Roles),
|
||||
TokenVersion: user.TokenVersion,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * 24 * time.Hour)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(constants.RefreshTokenDuration)),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -102,11 +116,11 @@ func (a *authService) saveNewRefreshToken(ctx context.Context, params sqlc.Updat
|
||||
}
|
||||
|
||||
func (a *authService) Signin(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error) {
|
||||
if !constant.EMAIL_REGEX.MatchString(dto.Email) {
|
||||
if !constants.EMAIL_REGEX.MatchString(dto.Email) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid email")
|
||||
}
|
||||
|
||||
err := constant.ValidatePassword(dto.Password)
|
||||
err := constants.ValidatePassword(dto.Password)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
@@ -120,13 +134,12 @@ func (a *authService) Signin(ctx context.Context, dto *request.SignInDto) (*resp
|
||||
return nil, fiber.NewError(fiber.StatusUnauthorized, "Invalid identity or password!")
|
||||
}
|
||||
|
||||
data, err := a.genToken(user.ID, models.RolesEntityToRoleConstant(user.Roles))
|
||||
data, err := a.genToken(user)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
|
||||
}
|
||||
var pgID pgtype.UUID
|
||||
err = pgID.Scan(user.ID)
|
||||
pgID, err := convert.StringToUUID(user.ID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
@@ -160,11 +173,11 @@ func (a *authService) RefreshToken(ctx context.Context, id string) (*response.Au
|
||||
}
|
||||
roles := models.RolesEntityToRoleConstant(user.Roles)
|
||||
|
||||
if slices.Contains(roles, constant.BANNED) {
|
||||
if slices.Contains(roles, constants.BANNED) {
|
||||
return nil, fiber.NewError(fiber.StatusUnauthorized, "User is banned!")
|
||||
}
|
||||
|
||||
data, err := a.genToken(id, roles)
|
||||
data, err := a.genToken(user)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
@@ -187,14 +200,23 @@ func (a *authService) RefreshToken(ctx context.Context, id string) (*response.Au
|
||||
}
|
||||
|
||||
func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*response.AuthResponse, error) {
|
||||
if !constant.EMAIL_REGEX.MatchString(dto.Email) {
|
||||
if !constants.EMAIL_REGEX.MatchString(dto.Email) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid email")
|
||||
}
|
||||
err := constant.ValidatePassword(dto.Password)
|
||||
err := constants.ValidatePassword(dto.Password)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ok, err := a.tokenRepo.CheckVerified(ctx, dto.Email, constants.TokenEmailVerify, dto.TokenID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid or expired token")
|
||||
}
|
||||
|
||||
user, err := a.userRepo.GetByEmail(ctx, dto.Email)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
@@ -202,6 +224,7 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
|
||||
if user != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "User already exists")
|
||||
}
|
||||
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(dto.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
@@ -215,14 +238,13 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
|
||||
String: string(hashed),
|
||||
Valid: len(hashed) != 0,
|
||||
},
|
||||
IsVerified: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
var userId pgtype.UUID
|
||||
err = userId.Scan(user.ID)
|
||||
|
||||
userId, err := convert.StringToUUID(user.ID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
@@ -239,19 +261,28 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
role, err := a.roleRepo.GetByname(ctx, constants.USER.String())
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
roleId, err := convert.StringToUUID(role.ID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
err = a.roleRepo.AddUserRole(
|
||||
ctx,
|
||||
sqlc.AddUserRoleParams{
|
||||
UserID: userId,
|
||||
Name: constant.USER.String(),
|
||||
UserID: userId,
|
||||
Column2: []pgtype.UUID{roleId},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
data, err := a.genToken(user.ID, constant.USER.ToSlice())
|
||||
data, err := a.genToken(user)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
@@ -273,22 +304,101 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ForgotPassword implements [AuthService].
|
||||
func (a *authService) ForgotPassword(ctx context.Context) error {
|
||||
panic("unimplemented")
|
||||
func (a *authService) ForgotPassword(ctx context.Context, dto *request.ForgotPasswordDto) error {
|
||||
ok, err := a.tokenRepo.CheckVerified(ctx, dto.Email, constants.TokenPasswordReset, dto.TokenID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if !ok {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid or expired token")
|
||||
}
|
||||
user, err := a.userRepo.GetByEmail(ctx, dto.Email)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if user == nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "User not found")
|
||||
}
|
||||
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(dto.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
userId, err := convert.StringToUUID(user.ID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
err = a.userRepo.UpdatePassword(ctx, sqlc.UpdateUserPasswordParams{
|
||||
ID: userId,
|
||||
PasswordHash: pgtype.Text{
|
||||
String: string(hashed),
|
||||
Valid: len(hashed) != 0,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SigninWith3rd implements [AuthService].
|
||||
func (a *authService) SigninWith3rd(ctx context.Context) error {
|
||||
func (a *authService) SigninWith3rd(ctx context.Context, dto *request.SigninWith3rdDto) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
func (a *authService) GenerateOTP() (string, error) {
|
||||
max := big.NewInt(900000)
|
||||
n, err := rand.Int(rand.Reader, max)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
otp := n.Int64() + 100000
|
||||
return fmt.Sprintf("%06d", otp), nil
|
||||
}
|
||||
|
||||
// CreateToken implements [AuthService].
|
||||
func (a *authService) CreateToken(ctx context.Context) error {
|
||||
panic("unimplemented")
|
||||
func (a *authService) CreateToken(ctx context.Context, dto *request.CreateTokenDto) error {
|
||||
ok, err := a.tokenRepo.CheckCooldown(ctx, dto.Email, dto.TokenType)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if ok {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Please wait before requesting another token")
|
||||
}
|
||||
|
||||
otp, err := a.GenerateOTP()
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
token := &models.TokenEntity{
|
||||
Email: dto.Email,
|
||||
Token: otp,
|
||||
TokenType: dto.TokenType,
|
||||
}
|
||||
|
||||
err = a.tokenRepo.Create(ctx, token)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
a.c.PublishTask(ctx, constants.StreamEmailName, constants.TaskTypeSendEmailOTP, token)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify implements [AuthService].
|
||||
func (a *authService) VerifyToken(ctx context.Context) error {
|
||||
panic("unimplemented")
|
||||
func (a *authService) VerifyToken(ctx context.Context, dto *request.VerifyTokenDto) (*response.VerifyTokenResponse, error) {
|
||||
token, err := a.tokenRepo.Get(ctx, dto.Email, dto.TokenType)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if token == nil || token.Token != dto.Token {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid token")
|
||||
}
|
||||
tokenId := uuid.New().String()
|
||||
err = a.tokenRepo.CreateVerified(ctx, dto.Email, dto.TokenType, tokenId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return &response.VerifyTokenResponse{
|
||||
TokenID: tokenId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -4,22 +4,28 @@ import (
|
||||
"context"
|
||||
"history-api/internal/dtos/request"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/models"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/convert"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type UserService interface {
|
||||
//user
|
||||
GetUserCurrent(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error)
|
||||
UpdateProfile(ctx context.Context, id string) (*response.UserResponse, error)
|
||||
ChangePassword(ctx context.Context, id string) (*response.UserResponse, error)
|
||||
GetUserCurrent(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||
UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error)
|
||||
ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error
|
||||
|
||||
//admin
|
||||
DeleteUser(ctx context.Context, id string) (*response.UserResponse, error)
|
||||
ChangeRoleUser(ctx context.Context, id string) (*response.UserResponse, error)
|
||||
RestoreUser(ctx context.Context, id string) (*response.UserResponse, error)
|
||||
GetUserByID(ctx context.Context, id string) (*response.UserResponse, error)
|
||||
Search(ctx context.Context, id string) ([]*response.UserResponse, error)
|
||||
GetAllUser(ctx context.Context, id string) ([]*response.UserResponse, error)
|
||||
DeleteUser(ctx context.Context, userId string) error
|
||||
ChangeRoleUser(ctx context.Context, dto *request.ChangeRoleDto) (*response.UserResponse, error)
|
||||
RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||
GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||
Search(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error)
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
@@ -37,47 +43,241 @@ func NewUserService(
|
||||
}
|
||||
}
|
||||
|
||||
// ChangePassword implements [UserService].
|
||||
func (u *userService) ChangePassword(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error {
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user, err := u.userRepo.GetByID(ctx, pgID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
if user == nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.OldPassword)); err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid identity or password!")
|
||||
}
|
||||
|
||||
hashPassword, err := bcrypt.GenerateFromPassword([]byte(dto.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
err = u.userRepo.UpdatePassword(ctx, sqlc.UpdateUserPasswordParams{
|
||||
ID: pgID,
|
||||
PasswordHash: pgtype.Text{String: string(hashPassword), Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangeRoleUser implements [UserService].
|
||||
func (u *userService) ChangeRoleUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) ChangeRoleUser(ctx context.Context, dto *request.ChangeRoleDto) (*response.UserResponse, error) {
|
||||
userId, err := convert.StringToUUID(dto.UserID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
user, err := u.userRepo.GetByID(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
if user == nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
|
||||
roleIdstr, err := u.roleRepo.GetByIDs(ctx, dto.Roles)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user.Roles = make([]*models.RoleSimple, 0)
|
||||
roleIdList := make([]pgtype.UUID, 0)
|
||||
for _, role := range roleIdstr {
|
||||
roleID, err := convert.StringToUUID(role.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
roleIdList = append(roleIdList, roleID)
|
||||
user.Roles = append(user.Roles, role.ToRoleSimple())
|
||||
}
|
||||
|
||||
err = u.roleRepo.RemoveAllRolesFromUser(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
err = u.roleRepo.AddUserRole(ctx, sqlc.AddUserRoleParams{
|
||||
UserID: userId,
|
||||
Column2: roleIdList,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return user.ToResponse(), nil
|
||||
|
||||
}
|
||||
|
||||
// DeleteUser implements [UserService].
|
||||
func (u *userService) DeleteUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) DeleteUser(ctx context.Context, userId string) error {
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user, err := u.userRepo.GetByID(ctx, pgID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
if user == nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
err = u.userRepo.Delete(ctx, pgID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllUser implements [UserService].
|
||||
func (u *userService) GetAllUser(ctx context.Context, id string) ([]*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error) {
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user, err := u.userRepo.GetByID(ctx, pgID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
if user == nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
|
||||
newUser, err := u.userRepo.UpdateProfile(
|
||||
ctx,
|
||||
sqlc.UpdateUserProfileParams{
|
||||
DisplayName: pgtype.Text{String: dto.DisplayName, Valid: len(dto.DisplayName) > 0},
|
||||
FullName: pgtype.Text{String: dto.FullName, Valid: len(dto.FullName) > 0},
|
||||
AvatarUrl: pgtype.Text{String: dto.AvatarUrl, Valid: len(dto.AvatarUrl) > 0},
|
||||
Bio: pgtype.Text{String: dto.Bio, Valid: len(dto.Bio) > 0},
|
||||
Location: pgtype.Text{String: dto.Location, Valid: len(dto.Location) > 0},
|
||||
Website: pgtype.Text{String: dto.Website, Valid: len(dto.Website) > 0},
|
||||
CountryCode: pgtype.Text{String: dto.CountryCode, Valid: len(dto.CountryCode) > 0},
|
||||
Phone: pgtype.Text{String: dto.Phone, Valid: len(dto.Phone) > 0},
|
||||
UserID: pgID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return newUser.ToResponse(), nil
|
||||
}
|
||||
|
||||
// GetUserByID implements [UserService].
|
||||
func (u *userService) GetUserByID(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) GetUserCurrent(ctx context.Context, userId string) (*response.UserResponse, error) {
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user, err := u.userRepo.GetByID(ctx, pgID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
return user.ToResponse(), nil
|
||||
}
|
||||
|
||||
// GetUserCurrent implements [UserService].
|
||||
func (u *userService) GetUserCurrent(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error) {
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
user, err := u.userRepo.GetByIDWithoutDeleted(ctx, pgID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
if user == nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
|
||||
err = u.userRepo.Restore(ctx, pgID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user.IsDeleted = false
|
||||
return user.ToResponse(), nil
|
||||
}
|
||||
|
||||
// RestoreUser implements [UserService].
|
||||
func (u *userService) RestoreUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (u *userService) Search(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) {
|
||||
arg := sqlc.SearchUsersParams{
|
||||
Limit: int32(dto.Limit + 1),
|
||||
}
|
||||
|
||||
if dto.Cursor != "" {
|
||||
pgID, err := convert.StringToUUID(dto.Cursor)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid cursor format")
|
||||
}
|
||||
arg.Cursor = pgID
|
||||
}
|
||||
|
||||
if dto.Search != "" {
|
||||
pgID, err := convert.StringToUUID(dto.Search)
|
||||
if err == nil {
|
||||
arg.SearchID = pgID
|
||||
} else {
|
||||
arg.SearchText = pgtype.Text{String: dto.Search, Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
if dto.IsDeleted != nil {
|
||||
arg.IsDeleted = pgtype.Bool{Bool: *dto.IsDeleted, Valid: true}
|
||||
}
|
||||
if len(dto.RoleIDs) > 0 {
|
||||
var pgRoleIDs []pgtype.UUID
|
||||
for _, idStr := range dto.RoleIDs {
|
||||
pgID, err := convert.StringToUUID(idStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
pgRoleIDs = append(pgRoleIDs, pgID)
|
||||
}
|
||||
arg.RoleIds = pgRoleIDs
|
||||
}
|
||||
|
||||
rows, err := u.userRepo.Search(ctx, arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasMore := false
|
||||
var nextCursor string
|
||||
|
||||
if len(rows) > dto.Limit {
|
||||
hasMore = true
|
||||
nextCursor = rows[dto.Limit-1].ID
|
||||
rows = rows[:dto.Limit]
|
||||
}
|
||||
|
||||
res := &response.PaginatedResponse{
|
||||
Data: rows,
|
||||
Status: true,
|
||||
Message: "",
|
||||
}
|
||||
res.Pagination.HasMore = hasMore
|
||||
res.Pagination.NextCursor = nextCursor
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Search implements [UserService].
|
||||
func (u *userService) Search(ctx context.Context, id string) ([]*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// UpdateProfile implements [UserService].
|
||||
func (u *userService) UpdateProfile(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
func (u *userService) GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error) {
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user, err := u.userRepo.GetByID(ctx, pgID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
return user.ToResponse(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user