This commit is contained in:
@@ -1 +1,122 @@
|
||||
package controllers
|
||||
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 AuthController struct {
|
||||
service services.AuthService
|
||||
}
|
||||
|
||||
func NewAuthController(svc services.AuthService) *AuthController {
|
||||
return &AuthController{service: svc}
|
||||
}
|
||||
|
||||
// Signin godoc
|
||||
// @Summary Sign in an existing user
|
||||
// @Description Authenticate user and return token data
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body request.SignInDto true "Sign In request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/signin [post]
|
||||
func (h *AuthController) Signin(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
dto := &request.SignInDto{}
|
||||
|
||||
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.Signin(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,
|
||||
})
|
||||
}
|
||||
|
||||
// Signup godoc
|
||||
// @Summary Sign up a new user
|
||||
// @Description Create a new user account
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body request.SignUpDto true "Sign Up request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/signup [post]
|
||||
func (h *AuthController) Signup(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
dto := &request.SignUpDto{}
|
||||
|
||||
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.Signup(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,
|
||||
})
|
||||
}
|
||||
|
||||
// RefreshToken godoc
|
||||
// @Summary Refresh access token
|
||||
// @Description Get a new access token using the user's current session/refresh token
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /auth/refresh [post]
|
||||
func (h *AuthController) RefreshToken(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := h.service.RefreshToken(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,
|
||||
})
|
||||
}
|
||||
|
||||
113
internal/controllers/tileController.go
Normal file
113
internal/controllers/tileController.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/services"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type TileController struct {
|
||||
service services.TileService
|
||||
}
|
||||
|
||||
func NewTileController(svc services.TileService) *TileController {
|
||||
return &TileController{service: svc}
|
||||
}
|
||||
|
||||
// GetMetadata godoc
|
||||
// @Summary Get tile metadata
|
||||
// @Description Retrieve map metadata
|
||||
// @Tags Tile
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /tiles/metadata [get]
|
||||
func (h *TileController) GetMetadata(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := h.service.GetMetadata(ctx)
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// GetTile godoc
|
||||
// @Summary Get a map tile
|
||||
// @Description Fetch vector or raster map tile data by Z, X, Y coordinates
|
||||
// @Tags Tile
|
||||
// @Produce application/octet-stream
|
||||
// @Param z path int true "Zoom level (0-22)"
|
||||
// @Param x path int true "X coordinate"
|
||||
// @Param y path int true "Y coordinate"
|
||||
// @Success 200 {file} byte
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /tiles/{z}/{x}/{y} [get]
|
||||
func (h *TileController) GetTile(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
z, x, y, err := h.parseTileParams(c)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
data, headers, err := h.service.GetTile(ctx, z, x, y)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
c.Set(k, v)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).Send(data)
|
||||
}
|
||||
|
||||
func (h *TileController) parseTileParams(c fiber.Ctx) (int, int, int, error) {
|
||||
z, err := strconv.Atoi(c.Params("z"))
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("invalid z")
|
||||
}
|
||||
|
||||
x, err := strconv.Atoi(c.Params("x"))
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("invalid x")
|
||||
}
|
||||
|
||||
y, err := strconv.Atoi(c.Params("y"))
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("invalid y")
|
||||
}
|
||||
|
||||
if z < 0 || x < 0 || y < 0 {
|
||||
return 0, 0, 0, fmt.Errorf("coordinates must be positive")
|
||||
}
|
||||
|
||||
if z > 22 {
|
||||
return 0, 0, 0, fmt.Errorf("zoom level too large")
|
||||
}
|
||||
|
||||
return z, x, y, nil
|
||||
}
|
||||
11
internal/dtos/request/auth.go
Normal file
11
internal/dtos/request/auth.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package request
|
||||
|
||||
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"`
|
||||
}
|
||||
type SignInDto struct {
|
||||
Email string `json:"email" validate:"required,min=5,max=255,email"`
|
||||
Password string `json:"password" validate:"required,min=8,max=64"`
|
||||
}
|
||||
21
internal/dtos/request/media.go
Normal file
21
internal/dtos/request/media.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package request
|
||||
|
||||
type PreSignedDto struct {
|
||||
FileName string `json:"fileName" validate:"required"`
|
||||
ContentType string `json:"contentType" validate:"required"`
|
||||
}
|
||||
|
||||
type PreSignedCompleteDto struct {
|
||||
FileName string `json:"fileName" validate:"required"`
|
||||
MediaId string `json:"mediaId" validate:"required"`
|
||||
PublicUrl string `json:"publicUrl" validate:"required"`
|
||||
}
|
||||
|
||||
type SearchMediaDto struct {
|
||||
MediaId string `query:"media_id" validate:"omitempty"`
|
||||
FileName string `query:"file_name" 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"`
|
||||
}
|
||||
26
internal/dtos/request/user.go
Normal file
26
internal/dtos/request/user.go
Normal file
@@ -0,0 +1,26 @@
|
||||
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 UpdateUserDto struct {
|
||||
Password *string `json:"password" validate:"omitempty"`
|
||||
DiscordUserId *string `json:"discord_user_id" validate:"omitempty"`
|
||||
Role *[]constant.Role `json:"role" validate:"omitempty"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
6
internal/dtos/response/auth.go
Normal file
6
internal/dtos/response/auth.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package response
|
||||
|
||||
type AuthResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
19
internal/dtos/response/common.go
Normal file
19
internal/dtos/response/common.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"history-api/pkg/constant"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type CommonResponse struct {
|
||||
Status bool `json:"status"`
|
||||
Data any `json:"data"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type JWTClaims struct {
|
||||
UId string `json:"uid"`
|
||||
Roles []constant.Role `json:"roles"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
9
internal/dtos/response/media.go
Normal file
9
internal/dtos/response/media.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package response
|
||||
|
||||
type PreSignedResponse struct {
|
||||
UploadUrl string `json:"uploadUrl"`
|
||||
PublicUrl string `json:"publicUrl"`
|
||||
FileName string `json:"fileName"`
|
||||
MediaId string `json:"mediaId"`
|
||||
SignedHeaders map[string]string `json:"signedHeaders"`
|
||||
}
|
||||
16
internal/dtos/response/role.go
Normal file
16
internal/dtos/response/role.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package response
|
||||
|
||||
import "time"
|
||||
|
||||
type RoleSimpleResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type RoleResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
}
|
||||
11
internal/dtos/response/token.go
Normal file
11
internal/dtos/response/token.go
Normal file
@@ -0,0 +1,11 @@
|
||||
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"`
|
||||
}
|
||||
26
internal/dtos/response/user.go
Normal file
26
internal/dtos/response/user.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package response
|
||||
|
||||
import "time"
|
||||
|
||||
type UserResponse struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Profile *UserProfileSimpleResponse `json:"profile"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
Roles []*RoleSimpleResponse `json:"roles"`
|
||||
}
|
||||
|
||||
type UserProfileSimpleResponse struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
FullName string `json:"full_name"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
Bio string `json:"bio"`
|
||||
Location string `json:"location"`
|
||||
Website string `json:"website"`
|
||||
CountryCode string `json:"country_code"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
@@ -11,26 +11,39 @@ import (
|
||||
type Role struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
IsActive pgtype.Bool `json:"is_active"`
|
||||
IsVerified pgtype.Bool `json:"is_verified"`
|
||||
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"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type UserProfile struct {
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
DisplayName pgtype.Text `json:"display_name"`
|
||||
FullName pgtype.Text `json:"full_name"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
Bio pgtype.Text `json:"bio"`
|
||||
Location pgtype.Text `json:"location"`
|
||||
Website pgtype.Text `json:"website"`
|
||||
CountryCode pgtype.Text `json:"country_code"`
|
||||
Phone pgtype.Text `json:"phone"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type UserRole struct {
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
RoleID pgtype.UUID `json:"role_id"`
|
||||
@@ -40,7 +53,19 @@ 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"`
|
||||
VerifyType int16 `json:"verify_type"`
|
||||
DocumentUrl string `json:"document_url"`
|
||||
Status int16 `json:"status"`
|
||||
ReviewedBy pgtype.UUID `json:"reviewed_by"`
|
||||
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
@@ -11,44 +11,36 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (
|
||||
name,
|
||||
email,
|
||||
password_hash,
|
||||
const createUserProfile = `-- name: CreateUserProfile :one
|
||||
INSERT INTO user_profiles (
|
||||
user_id,
|
||||
display_name,
|
||||
avatar_url
|
||||
) VALUES (
|
||||
$1, $2, $3, $4
|
||||
$1, $2, $3
|
||||
)
|
||||
RETURNING id, name, email, password_hash, avatar_url, is_active, is_verified, token_version, refresh_token, is_deleted, created_at, updated_at
|
||||
RETURNING user_id, display_name, full_name, avatar_url, bio, location, website, country_code, phone, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
type CreateUserProfileParams struct {
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
DisplayName pgtype.Text `json:"display_name"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
||||
row := q.db.QueryRow(ctx, createUser,
|
||||
arg.Name,
|
||||
arg.Email,
|
||||
arg.PasswordHash,
|
||||
arg.AvatarUrl,
|
||||
)
|
||||
var i User
|
||||
func (q *Queries) CreateUserProfile(ctx context.Context, arg CreateUserProfileParams) (UserProfile, error) {
|
||||
row := q.db.QueryRow(ctx, createUserProfile, arg.UserID, arg.DisplayName, arg.AvatarUrl)
|
||||
var i UserProfile
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.UserID,
|
||||
&i.DisplayName,
|
||||
&i.FullName,
|
||||
&i.AvatarUrl,
|
||||
&i.IsActive,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
&i.Bio,
|
||||
&i.Location,
|
||||
&i.Website,
|
||||
&i.CountryCode,
|
||||
&i.Phone,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
@@ -58,8 +50,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
||||
const deleteUser = `-- name: DeleteUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
is_deleted = true,
|
||||
updated_at = now()
|
||||
is_deleted = true
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
@@ -68,59 +59,69 @@ func (q *Queries) DeleteUser(ctx context.Context, id pgtype.UUID) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const existsUserByEmail = `-- name: ExistsUserByEmail :one
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE email = $1
|
||||
AND is_deleted = false
|
||||
)
|
||||
const getTokenVersion = `-- name: GetTokenVersion :one
|
||||
SELECT token_version
|
||||
FROM users
|
||||
WHERE id = $1 AND is_deleted = false
|
||||
`
|
||||
|
||||
func (q *Queries) ExistsUserByEmail(ctx context.Context, email string) (bool, error) {
|
||||
row := q.db.QueryRow(ctx, existsUserByEmail, email)
|
||||
var exists bool
|
||||
err := row.Scan(&exists)
|
||||
return exists, err
|
||||
func (q *Queries) GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, getTokenVersion, id)
|
||||
var token_version int32
|
||||
err := row.Scan(&token_version)
|
||||
return token_version, err
|
||||
}
|
||||
|
||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.avatar_url,
|
||||
u.is_active,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object('id', r.id, 'name', r.name)
|
||||
) FILTER (WHERE r.id IS NOT NULL),
|
||||
'[]'
|
||||
)::json AS roles
|
||||
|
||||
(
|
||||
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,
|
||||
|
||||
(
|
||||
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
|
||||
LEFT JOIN user_roles ur ON u.id = ur.user_id
|
||||
LEFT JOIN roles r ON ur.role_id = r.id
|
||||
WHERE u.email = $1 AND u.is_deleted = false
|
||||
GROUP BY u.id
|
||||
`
|
||||
|
||||
type GetUserByEmailRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
IsActive pgtype.Bool `json:"is_active"`
|
||||
IsVerified pgtype.Bool `json:"is_verified"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -129,16 +130,14 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEm
|
||||
var i GetUserByEmailRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.AvatarUrl,
|
||||
&i.IsActive,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Profile,
|
||||
&i.Roles,
|
||||
)
|
||||
return i, err
|
||||
@@ -146,44 +145,58 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEm
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.avatar_url,
|
||||
u.is_active,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object('id', r.id, 'name', r.name)
|
||||
) FILTER (WHERE r.id IS NOT NULL),
|
||||
'[]'
|
||||
)::json AS roles
|
||||
|
||||
-- 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
|
||||
LEFT JOIN user_roles ur ON u.id = ur.user_id
|
||||
LEFT JOIN roles r ON ur.role_id = r.id
|
||||
WHERE u.id = $1 AND u.is_deleted = false
|
||||
GROUP BY u.id
|
||||
`
|
||||
|
||||
type GetUserByIDRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
IsActive pgtype.Bool `json:"is_active"`
|
||||
IsVerified pgtype.Bool `json:"is_verified"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -192,17 +205,15 @@ func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (GetUserByIDR
|
||||
var i GetUserByIDRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.AvatarUrl,
|
||||
&i.IsActive,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Profile,
|
||||
&i.Roles,
|
||||
)
|
||||
return i, err
|
||||
@@ -210,44 +221,56 @@ func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (GetUserByIDR
|
||||
|
||||
const getUsers = `-- name: GetUsers :many
|
||||
SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.avatar_url,
|
||||
u.is_active,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.id,
|
||||
u.email,
|
||||
u.password_hash,
|
||||
u.is_verified,
|
||||
u.token_version,
|
||||
u.refresh_token,
|
||||
u.is_deleted,
|
||||
u.created_at,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object('id', r.id, 'name', r.name)
|
||||
) FILTER (WHERE r.id IS NOT NULL),
|
||||
'[]'
|
||||
)::json AS roles
|
||||
|
||||
(
|
||||
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,
|
||||
|
||||
(
|
||||
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
|
||||
LEFT JOIN user_roles ur ON u.id = ur.user_id
|
||||
LEFT JOIN roles r ON ur.role_id = r.id
|
||||
WHERE u.is_deleted = false
|
||||
GROUP BY u.id
|
||||
`
|
||||
|
||||
type GetUsersRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
IsActive pgtype.Bool `json:"is_active"`
|
||||
IsVerified pgtype.Bool `json:"is_verified"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -262,17 +285,15 @@ func (q *Queries) GetUsers(ctx context.Context) ([]GetUsersRow, error) {
|
||||
var i GetUsersRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.AvatarUrl,
|
||||
&i.IsActive,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Profile,
|
||||
&i.Roles,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
@@ -288,8 +309,7 @@ func (q *Queries) GetUsers(ctx context.Context) ([]GetUsersRow, error) {
|
||||
const restoreUser = `-- name: RestoreUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
is_deleted = false,
|
||||
updated_at = now()
|
||||
is_deleted = false
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
@@ -298,99 +318,33 @@ func (q *Queries) RestoreUser(ctx context.Context, id pgtype.UUID) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const updateUser = `-- name: UpdateUser :one
|
||||
const updateTokenVersion = `-- name: UpdateTokenVersion :exec
|
||||
UPDATE users
|
||||
SET
|
||||
name = $1,
|
||||
avatar_url = $2,
|
||||
is_active = $3,
|
||||
is_verified = $4,
|
||||
updated_at = now()
|
||||
WHERE users.id = $5 AND users.is_deleted = false
|
||||
RETURNING
|
||||
users.id,
|
||||
users.name,
|
||||
users.email,
|
||||
users.password_hash,
|
||||
users.avatar_url,
|
||||
users.is_active,
|
||||
users.is_verified,
|
||||
users.token_version,
|
||||
users.refresh_token,
|
||||
users.is_deleted,
|
||||
users.created_at,
|
||||
users.updated_at,
|
||||
(
|
||||
SELECT COALESCE(json_agg(json_build_object('id', roles.id, 'name', roles.name)), '[]')::json
|
||||
FROM user_roles
|
||||
JOIN roles ON user_roles.role_id = roles.id
|
||||
WHERE user_roles.user_id = users.id
|
||||
) AS roles
|
||||
SET token_version = $2
|
||||
WHERE id = $1 AND is_deleted = false
|
||||
`
|
||||
|
||||
type UpdateUserParams struct {
|
||||
Name string `json:"name"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
IsActive pgtype.Bool `json:"is_active"`
|
||||
IsVerified pgtype.Bool `json:"is_verified"`
|
||||
ID pgtype.UUID `json:"id"`
|
||||
type UpdateTokenVersionParams struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
}
|
||||
|
||||
type UpdateUserRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
IsActive pgtype.Bool `json:"is_active"`
|
||||
IsVerified pgtype.Bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
Roles []byte `json:"roles"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error) {
|
||||
row := q.db.QueryRow(ctx, updateUser,
|
||||
arg.Name,
|
||||
arg.AvatarUrl,
|
||||
arg.IsActive,
|
||||
arg.IsVerified,
|
||||
arg.ID,
|
||||
)
|
||||
var i UpdateUserRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.AvatarUrl,
|
||||
&i.IsActive,
|
||||
&i.IsVerified,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Roles,
|
||||
)
|
||||
return i, err
|
||||
func (q *Queries) UpdateTokenVersion(ctx context.Context, arg UpdateTokenVersionParams) error {
|
||||
_, err := q.db.Exec(ctx, updateTokenVersion, arg.ID, arg.TokenVersion)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateUserPassword = `-- name: UpdateUserPassword :exec
|
||||
UPDATE users
|
||||
SET
|
||||
password_hash = $2,
|
||||
updated_at = now()
|
||||
password_hash = $2
|
||||
WHERE id = $1
|
||||
AND is_deleted = false
|
||||
`
|
||||
|
||||
type UpdateUserPasswordParams struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error {
|
||||
@@ -398,11 +352,137 @@ func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPassword
|
||||
return err
|
||||
}
|
||||
|
||||
const updateUserProfile = `-- name: UpdateUserProfile :one
|
||||
UPDATE user_profiles
|
||||
SET
|
||||
display_name = $1,
|
||||
full_name = $2,
|
||||
avatar_url = $3,
|
||||
bio = $4,
|
||||
location = $5,
|
||||
website = $6,
|
||||
country_code = $7,
|
||||
phone = $8,
|
||||
updated_at = now()
|
||||
WHERE user_id = $9
|
||||
RETURNING user_id, display_name, full_name, avatar_url, bio, location, website, country_code, phone, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateUserProfileParams struct {
|
||||
DisplayName pgtype.Text `json:"display_name"`
|
||||
FullName pgtype.Text `json:"full_name"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
Bio pgtype.Text `json:"bio"`
|
||||
Location pgtype.Text `json:"location"`
|
||||
Website pgtype.Text `json:"website"`
|
||||
CountryCode pgtype.Text `json:"country_code"`
|
||||
Phone pgtype.Text `json:"phone"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (UserProfile, error) {
|
||||
row := q.db.QueryRow(ctx, updateUserProfile,
|
||||
arg.DisplayName,
|
||||
arg.FullName,
|
||||
arg.AvatarUrl,
|
||||
arg.Bio,
|
||||
arg.Location,
|
||||
arg.Website,
|
||||
arg.CountryCode,
|
||||
arg.Phone,
|
||||
arg.UserID,
|
||||
)
|
||||
var i UserProfile
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.DisplayName,
|
||||
&i.FullName,
|
||||
&i.AvatarUrl,
|
||||
&i.Bio,
|
||||
&i.Location,
|
||||
&i.Website,
|
||||
&i.CountryCode,
|
||||
&i.Phone,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUserRefreshToken = `-- name: UpdateUserRefreshToken :exec
|
||||
UPDATE users
|
||||
SET
|
||||
refresh_token = $2
|
||||
WHERE id = $1
|
||||
AND is_deleted = false
|
||||
`
|
||||
|
||||
type UpdateUserRefreshTokenParams struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUserRefreshToken(ctx context.Context, arg UpdateUserRefreshTokenParams) error {
|
||||
_, err := q.db.Exec(ctx, updateUserRefreshToken, arg.ID, arg.RefreshToken)
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertUser = `-- name: UpsertUser :one
|
||||
INSERT INTO users (
|
||||
email,
|
||||
password_hash,
|
||||
google_id,
|
||||
auth_provider,
|
||||
is_verified
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5
|
||||
)
|
||||
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
|
||||
`
|
||||
|
||||
type UpsertUserParams struct {
|
||||
Email string `json:"email"`
|
||||
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) {
|
||||
row := q.db.QueryRow(ctx, upsertUser,
|
||||
arg.Email,
|
||||
arg.PasswordHash,
|
||||
arg.GoogleID,
|
||||
arg.AuthProvider,
|
||||
arg.IsVerified,
|
||||
)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.PasswordHash,
|
||||
&i.GoogleID,
|
||||
&i.AuthProvider,
|
||||
&i.IsVerified,
|
||||
&i.IsDeleted,
|
||||
&i.TokenVersion,
|
||||
&i.RefreshToken,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const verifyUser = `-- name: VerifyUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
is_verified = true,
|
||||
updated_at = now()
|
||||
is_verified = true
|
||||
WHERE id = $1
|
||||
AND is_deleted = false
|
||||
`
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/config"
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/dtos/response"
|
||||
"slices"
|
||||
|
||||
jwtware "github.com/gofiber/contrib/v3/jwt"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/constant"
|
||||
"history-api/pkg/dtos/response"
|
||||
"slices"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
|
||||
27
internal/models/profile.go
Normal file
27
internal/models/profile.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
import "history-api/internal/dtos/response"
|
||||
|
||||
type UserProfileSimple struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
FullName string `json:"full_name"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
Bio string `json:"bio"`
|
||||
Location string `json:"location"`
|
||||
Website string `json:"website"`
|
||||
CountryCode string `json:"country_code"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
func (p *UserProfileSimple) ToResponse() *response.UserProfileSimpleResponse {
|
||||
return &response.UserProfileSimpleResponse{
|
||||
DisplayName: p.DisplayName,
|
||||
FullName: p.FullName,
|
||||
AvatarUrl: p.AvatarUrl,
|
||||
Bio: p.Bio,
|
||||
Location: p.Location,
|
||||
Website: p.Website,
|
||||
CountryCode: p.CountryCode,
|
||||
Phone: p.Phone,
|
||||
}
|
||||
}
|
||||
65
internal/models/role.go
Normal file
65
internal/models/role.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/constant"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RoleSimple struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (r *RoleSimple) ToResponse() *response.RoleSimpleResponse {
|
||||
return &response.RoleSimpleResponse{
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func RolesToResponse(rs []*RoleSimple) []*response.RoleSimpleResponse {
|
||||
out := make([]*response.RoleSimpleResponse, len(rs))
|
||||
for i := range rs {
|
||||
out[i] = rs[i].ToResponse()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type RoleEntity struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (r *RoleEntity) ToResponse() *response.RoleResponse {
|
||||
return &response.RoleResponse{
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
IsDeleted: r.IsDeleted,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func RolesEntityToResponse(rs []*RoleEntity) []*response.RoleResponse {
|
||||
out := make([]*response.RoleResponse, len(rs))
|
||||
for i := range rs {
|
||||
out[i] = rs[i].ToResponse()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func RolesEntityToRoleConstant(rs []*RoleSimple) []constant.Role {
|
||||
out := make([]constant.Role, len(rs))
|
||||
for i := range rs {
|
||||
data, ok := constant.ParseRole(rs[i].Name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[i] = data
|
||||
}
|
||||
return out
|
||||
}
|
||||
35
internal/models/token.go
Normal file
35
internal/models/token.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/pkg/convert"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
61
internal/models/user.go
Normal file
61
internal/models/user.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"history-api/internal/dtos/response"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserEntity struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
Profile *UserProfileSimple `json:"profile"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
GoogleID string `json:"google_id"`
|
||||
AuthProvider string `json:"auth_provider"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
Roles []*RoleSimple `json:"roles"`
|
||||
}
|
||||
|
||||
func (u *UserEntity) ParseRoles(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
u.Roles = []*RoleSimple{}
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, &u.Roles)
|
||||
}
|
||||
|
||||
func (u *UserEntity) ParseProfile(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
u.Profile = &UserProfileSimple{}
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, &u.Profile)
|
||||
}
|
||||
|
||||
func (u *UserEntity) ToResponse() *response.UserResponse {
|
||||
return &response.UserResponse{
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
IsVerified: u.IsVerified,
|
||||
TokenVersion: u.TokenVersion,
|
||||
IsDeleted: u.IsDeleted,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
Roles: RolesToResponse(u.Roles),
|
||||
Profile: u.Profile.ToResponse(),
|
||||
}
|
||||
}
|
||||
|
||||
func UsersEntityToResponse(rs []*UserEntity) []*response.UserResponse {
|
||||
out := make([]*response.UserResponse, len(rs))
|
||||
for i := range rs {
|
||||
out[i] = rs[i].ToResponse()
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -2,11 +2,15 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/pkg/models"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/convert"
|
||||
)
|
||||
|
||||
type RoleRepository interface {
|
||||
@@ -25,43 +29,63 @@ type RoleRepository interface {
|
||||
|
||||
type roleRepository struct {
|
||||
q *sqlc.Queries
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewRoleRepository(db sqlc.DBTX) RoleRepository {
|
||||
func NewRoleRepository(db sqlc.DBTX, c cache.Cache) RoleRepository {
|
||||
return &roleRepository{
|
||||
q: sqlc.New(db),
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
err := r.c.Get(ctx, cacheId, &role)
|
||||
if err == nil {
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
row, err := r.q.GetRoleByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
role := &models.RoleEntity{
|
||||
ID: row.ID,
|
||||
role = models.RoleEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Name: row.Name,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
return role, nil
|
||||
_ = r.c.Set(ctx, cacheId, role, 5*time.Minute)
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) GetByname(ctx context.Context, name string) (*models.RoleEntity, error) {
|
||||
cacheId := fmt.Sprintf("role:name:%s", name)
|
||||
var role models.RoleEntity
|
||||
err := r.c.Get(ctx, cacheId, &role)
|
||||
if err == nil {
|
||||
return &role, nil
|
||||
}
|
||||
row, err := r.q.GetRoleByName(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
role := &models.RoleEntity{
|
||||
ID: row.ID,
|
||||
role = models.RoleEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Name: row.Name,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
return role, nil
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, role, 5*time.Minute)
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleEntity, error) {
|
||||
@@ -69,14 +93,19 @@ func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleE
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
role := &models.RoleEntity{
|
||||
ID: row.ID,
|
||||
role := models.RoleEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Name: row.Name,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
return role, nil
|
||||
mapCache := map[string]any{
|
||||
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)
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParams) (*models.RoleEntity, error) {
|
||||
@@ -84,14 +113,20 @@ func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParam
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
role := &models.RoleEntity{
|
||||
ID: row.ID,
|
||||
role := models.RoleEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Name: row.Name,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
return role, nil
|
||||
|
||||
mapCache := map[string]any{
|
||||
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)
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error) {
|
||||
@@ -103,11 +138,11 @@ func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error)
|
||||
var users []*models.RoleEntity
|
||||
for _, row := range rows {
|
||||
user := &models.RoleEntity{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Name: row.Name,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
@@ -116,33 +151,44 @@ func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error)
|
||||
}
|
||||
|
||||
func (r *roleRepository) Delete(ctx context.Context, id pgtype.UUID) error {
|
||||
err := r.q.DeleteRole(ctx, id)
|
||||
return err
|
||||
role, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.q.DeleteRole(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = r.c.Del(ctx, fmt.Sprintf("role:id:%s", role.ID), fmt.Sprintf("role:name:%s", role.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) Restore(ctx context.Context, id pgtype.UUID) error {
|
||||
err := r.q.RestoreRole(ctx, id)
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = r.c.Del(ctx, fmt.Sprintf("role:id:%s", convert.UUIDToString(id)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *roleRepository) AddUserRole(ctx context.Context, params sqlc.AddUserRoleParams) error {
|
||||
err := r.q.AddUserRole(ctx, params)
|
||||
return err
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *roleRepository) RemoveUserRole(ctx context.Context, params sqlc.RemoveUserRoleParams) error {
|
||||
err := r.q.RemoveUserRole(ctx, params)
|
||||
return err
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *roleRepository) RemoveAllUsersFromRole(ctx context.Context, roleId pgtype.UUID) error {
|
||||
err := r.q.RemoveAllUsersFromRole(ctx, roleId)
|
||||
return err
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *roleRepository) RemoveAllRolesFromUser(ctx context.Context, roleId pgtype.UUID) error {
|
||||
err := r.q.RemoveAllRolesFromUser(ctx, roleId)
|
||||
return err
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
|
||||
98
internal/repositories/tileRepository.go
Normal file
98
internal/repositories/tileRepository.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"history-api/pkg/cache"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TileRepository interface {
|
||||
GetMetadata(ctx context.Context) (map[string]string, error)
|
||||
GetTile(ctx context.Context, z, x, y int) ([]byte, string, bool, error)
|
||||
}
|
||||
|
||||
type tileRepository struct {
|
||||
db *sql.DB
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewTileRepository(db *sql.DB, c cache.Cache) TileRepository {
|
||||
return &tileRepository{
|
||||
db: db,
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *tileRepository) GetMetadata(ctx context.Context) (map[string]string, error) {
|
||||
cacheId := "mbtiles:metadata"
|
||||
|
||||
var cached map[string]string
|
||||
err := r.c.Get(ctx, cacheId, &cached)
|
||||
if err == nil {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, "SELECT name, value FROM metadata")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
metadata := make(map[string]string)
|
||||
|
||||
for rows.Next() {
|
||||
var name, value string
|
||||
if err := rows.Scan(&name, &value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metadata[name] = value
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, metadata, 10*time.Minute)
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func (r *tileRepository) GetTile(ctx context.Context, z, x, y int) ([]byte, string, bool, error) {
|
||||
if z < 0 || x < 0 || y < 0 {
|
||||
return nil, "", false, fmt.Errorf("invalid tile coordinates")
|
||||
}
|
||||
|
||||
// cache key
|
||||
cacheId := fmt.Sprintf("tile:%d:%d:%d", z, x, y)
|
||||
|
||||
var cached []byte
|
||||
err := r.c.Get(ctx, cacheId, &cached)
|
||||
if err == nil {
|
||||
meta, _ := r.GetMetadata(ctx)
|
||||
return cached, meta["format"], meta["format"] == "pbf", nil
|
||||
}
|
||||
|
||||
// XYZ -> TMS
|
||||
tmsY := (1 << z) - 1 - y
|
||||
|
||||
var tileData []byte
|
||||
|
||||
err = r.db.QueryRowContext(ctx, `
|
||||
SELECT tile_data
|
||||
FROM tiles
|
||||
WHERE zoom_level = ?
|
||||
AND tile_column = ?
|
||||
AND tile_row = ?
|
||||
`, z, x, tmsY).Scan(&tileData)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
|
||||
meta, err := r.GetMetadata(ctx)
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, tileData, 5*time.Minute)
|
||||
|
||||
return tileData, meta["format"], meta["format"] == "pbf", nil
|
||||
}
|
||||
@@ -2,136 +2,187 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/pkg/models"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/convert"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
GetByID(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)
|
||||
Create(ctx context.Context, params sqlc.CreateUserParams) (*models.UserEntity, error)
|
||||
Update(ctx context.Context, params sqlc.UpdateUserParams) (*models.UserEntity, error)
|
||||
UpdatePassword(ctx context.Context, params sqlc.UpdateUserPasswordParams) error
|
||||
ExistEmail(ctx context.Context, email string) (bool, 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
|
||||
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)
|
||||
UpdatePassword(ctx context.Context, params sqlc.UpdateUserPasswordParams) error
|
||||
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
|
||||
}
|
||||
|
||||
type userRepository struct {
|
||||
q *sqlc.Queries
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewUserRepository(db sqlc.DBTX) UserRepository {
|
||||
func NewUserRepository(db sqlc.DBTX, c cache.Cache) UserRepository {
|
||||
return &userRepository{
|
||||
q: sqlc.New(db),
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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: row.ID,
|
||||
Name: row.Name,
|
||||
user = models.UserEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: row.PasswordHash,
|
||||
AvatarUrl: row.AvatarUrl,
|
||||
IsActive: row.IsActive,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
|
||||
if err := user.ParseRoles(row.Roles); err != nil {
|
||||
return nil, err
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
|
||||
if err := user.ParseProfile(row.Profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.UserEntity, error) {
|
||||
cacheId := fmt.Sprintf("user:email:%s", email)
|
||||
|
||||
var user models.UserEntity
|
||||
err := r.c.Get(ctx, cacheId, &user)
|
||||
if err == nil {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
row, err := r.q.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := &models.UserEntity{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
|
||||
user = models.UserEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: row.PasswordHash,
|
||||
AvatarUrl: row.AvatarUrl,
|
||||
IsActive: row.IsActive,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
|
||||
if err := user.ParseRoles(row.Roles); err != nil {
|
||||
return nil, err
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
|
||||
if err := user.ParseProfile(row.Profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Create(ctx context.Context, params sqlc.CreateUserParams) (*models.UserEntity, error) {
|
||||
row, err := r.q.CreateUser(ctx, params)
|
||||
func (r *userRepository) UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error) {
|
||||
row, err := r.q.UpsertUser(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UserEntity{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: row.PasswordHash,
|
||||
AvatarUrl: row.AvatarUrl,
|
||||
IsActive: row.IsActive,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
RefreshToken: row.RefreshToken,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
Roles: make([]*models.RoleSimple, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Update(ctx context.Context, params sqlc.UpdateUserParams) (*models.UserEntity, error) {
|
||||
row, err := r.q.UpdateUser(ctx, params)
|
||||
func (r *userRepository) UpdateProfile(ctx context.Context, params sqlc.UpdateUserProfileParams) (*models.UserEntity, error) {
|
||||
user, err := r.GetByID(ctx, params.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := &models.UserEntity{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
Email: row.Email,
|
||||
PasswordHash: row.PasswordHash,
|
||||
AvatarUrl: row.AvatarUrl,
|
||||
IsActive: row.IsActive,
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
|
||||
row, err := r.q.UpdateUserProfile(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile := models.UserProfileSimple{
|
||||
DisplayName: convert.TextToString(row.DisplayName),
|
||||
FullName: convert.TextToString(row.FullName),
|
||||
AvatarUrl: convert.TextToString(row.AvatarUrl),
|
||||
Bio: convert.TextToString(row.Bio),
|
||||
Location: convert.TextToString(row.Location),
|
||||
Website: convert.TextToString(row.Website),
|
||||
CountryCode: convert.TextToString(row.CountryCode),
|
||||
Phone: convert.TextToString(row.Phone),
|
||||
}
|
||||
|
||||
if err := user.ParseRoles(row.Roles); err != nil {
|
||||
return nil, err
|
||||
user.Profile = &profile
|
||||
mapCache := map[string]any{
|
||||
fmt.Sprintf("user:email:%s", user.Email): user,
|
||||
fmt.Sprintf("user:id:%s", user.ID): user,
|
||||
}
|
||||
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error) {
|
||||
row, err := r.q.CreateUserProfile(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UserProfileSimple{
|
||||
DisplayName: convert.TextToString(row.DisplayName),
|
||||
FullName: convert.TextToString(row.FullName),
|
||||
AvatarUrl: convert.TextToString(row.AvatarUrl),
|
||||
Bio: convert.TextToString(row.Bio),
|
||||
Location: convert.TextToString(row.Location),
|
||||
Website: convert.TextToString(row.Website),
|
||||
CountryCode: convert.TextToString(row.CountryCode),
|
||||
Phone: convert.TextToString(row.Phone),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error) {
|
||||
rows, err := r.q.GetUsers(ctx)
|
||||
if err != nil {
|
||||
@@ -141,23 +192,24 @@ func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error)
|
||||
var users []*models.UserEntity
|
||||
for _, row := range rows {
|
||||
user := &models.UserEntity{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Email: row.Email,
|
||||
PasswordHash: row.PasswordHash,
|
||||
AvatarUrl: row.AvatarUrl,
|
||||
IsActive: row.IsActive,
|
||||
PasswordHash: convert.TextToString(row.PasswordHash),
|
||||
IsVerified: row.IsVerified,
|
||||
TokenVersion: row.TokenVersion,
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -165,29 +217,138 @@ func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error)
|
||||
}
|
||||
|
||||
func (r *userRepository) Verify(ctx context.Context, id pgtype.UUID) error {
|
||||
err := r.q.VerifyUser(ctx, id)
|
||||
return err
|
||||
user, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.q.VerifyUser(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = r.q.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
|
||||
ID: id,
|
||||
TokenVersion: user.TokenVersion + 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.IsVerified = true
|
||||
user.TokenVersion += 1
|
||||
|
||||
mapCache := map[string]any{
|
||||
fmt.Sprintf("user:email:%s", user.Email): user,
|
||||
fmt.Sprintf("user:id:%s", user.ID): user,
|
||||
}
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Delete(ctx context.Context, id pgtype.UUID) error {
|
||||
err := r.q.DeleteUser(ctx, id)
|
||||
return err
|
||||
user, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.q.DeleteUser(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = r.c.Del(
|
||||
ctx,
|
||||
fmt.Sprintf("user:id:%s", user.ID),
|
||||
fmt.Sprintf("user:email:%s", user.Email),
|
||||
fmt.Sprintf("user:token:%s", user.ID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Restore(ctx context.Context, id pgtype.UUID) error {
|
||||
err := r.q.RestoreUser(ctx, id)
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = r.c.Del(ctx, fmt.Sprintf("user:id:%s", convert.UUIDToString(id)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, error) {
|
||||
cacheId := fmt.Sprintf("user:token:%s", convert.UUIDToString(id))
|
||||
var token int32
|
||||
err := r.c.Get(ctx, cacheId, &token)
|
||||
if err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
raw, err := r.q.GetTokenVersion(ctx, id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, raw, 5*time.Minute)
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) UpdateTokenVersion(ctx context.Context, params sqlc.UpdateTokenVersionParams) error {
|
||||
err := r.q.UpdateTokenVersion(ctx, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cacheId := fmt.Sprintf("user:token:%s", convert.UUIDToString(params.ID))
|
||||
_ = r.c.Set(ctx, cacheId, params.TokenVersion, 5*time.Minute)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepository) UpdatePassword(ctx context.Context, params sqlc.UpdateUserPasswordParams) error {
|
||||
err := r.q.UpdateUserPassword(ctx, params)
|
||||
return err
|
||||
user, err := r.GetByID(ctx, params.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = r.q.UpdateUserPassword(ctx, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = r.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
|
||||
ID: params.ID,
|
||||
TokenVersion: user.TokenVersion + 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.PasswordHash = convert.TextToString(params.PasswordHash)
|
||||
user.TokenVersion += 1
|
||||
mapCache := map[string]any{
|
||||
fmt.Sprintf("user:email:%s", user.Email): user,
|
||||
fmt.Sprintf("user:id:%s", user.ID): user,
|
||||
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
|
||||
}
|
||||
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userRepository) ExistEmail(ctx context.Context, email string) (bool, error) {
|
||||
row, err := r.q.ExistsUserByEmail(ctx, email)
|
||||
func (r *userRepository) UpdateRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error {
|
||||
user, err := r.GetByID(ctx, params.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
err = r.q.UpdateUserRefreshToken(ctx, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.RefreshToken = convert.TextToString(params.RefreshToken)
|
||||
mapCache := map[string]any{
|
||||
fmt.Sprintf("user:email:%s", user.Email): user,
|
||||
fmt.Sprintf("user:id:%s", user.ID): user,
|
||||
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
|
||||
}
|
||||
|
||||
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package routers
|
||||
15
internal/routes/authRoute.go
Normal file
15
internal/routes/authRoute.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"history-api/internal/controllers"
|
||||
"history-api/internal/middlewares"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func AuthRoutes(app *fiber.App, controller *controllers.AuthController) {
|
||||
route := app.Group("/auth")
|
||||
route.Post("/signin", controller.Signin)
|
||||
route.Post("/signup", controller.Signup)
|
||||
route.Post("/refresh", middlewares.JwtRefresh(), controller.RefreshToken)
|
||||
}
|
||||
18
internal/routes/notFoundRoute.go
Normal file
18
internal/routes/notFoundRoute.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func NotFoundRoute(app *fiber.App) {
|
||||
app.Use(
|
||||
func(c fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusNotFound).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: "sorry, endpoint is not found",
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
13
internal/routes/tileRoute.go
Normal file
13
internal/routes/tileRoute.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"history-api/internal/controllers"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func TileRoutes(app *fiber.App, controller *controllers.TileController) {
|
||||
route := app.Group("/tiles")
|
||||
route.Get("/metadata", controller.GetMetadata)
|
||||
route.Get("/:z/:x/:y", controller.GetTile)
|
||||
}
|
||||
@@ -1 +1,294 @@
|
||||
package services
|
||||
package services
|
||||
|
||||
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/config"
|
||||
"history-api/pkg/constant"
|
||||
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
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
|
||||
RefreshToken(ctx context.Context, id string) (*response.AuthResponse, error)
|
||||
}
|
||||
|
||||
type authService struct {
|
||||
userRepo repositories.UserRepository
|
||||
roleRepo repositories.RoleRepository
|
||||
}
|
||||
|
||||
func NewAuthService(
|
||||
userRepo repositories.UserRepository,
|
||||
roleRepo repositories.RoleRepository,
|
||||
) AuthService {
|
||||
return &authService{
|
||||
userRepo: userRepo,
|
||||
roleRepo: roleRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *authService) genToken(Uid string, role []constant.Role) (*response.AuthResponse, error) {
|
||||
jwtSecret, err := config.GetConfig("JWT_SECRET")
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "missing JWT_SECRET in environment")
|
||||
}
|
||||
jwtRefreshSecret, err := config.GetConfig("JWT_REFRESH_SECRET")
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "missing JWT_REFRESH_SECRET in environment")
|
||||
}
|
||||
|
||||
if jwtSecret == "" || jwtRefreshSecret == "" {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "missing JWT secrets in environment")
|
||||
}
|
||||
|
||||
claimsAccess := &response.JWTClaims{
|
||||
UId: Uid,
|
||||
Roles: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
|
||||
},
|
||||
}
|
||||
|
||||
claimsRefresh := &response.JWTClaims{
|
||||
UId: Uid,
|
||||
Roles: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * 24 * time.Hour)),
|
||||
},
|
||||
}
|
||||
|
||||
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimsAccess)
|
||||
at, err := accessToken.SignedString([]byte(jwtSecret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimsRefresh)
|
||||
rt, err := refreshToken.SignedString([]byte(jwtRefreshSecret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := response.AuthResponse{
|
||||
AccessToken: at,
|
||||
RefreshToken: rt,
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (a *authService) saveNewRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error {
|
||||
err := a.userRepo.UpdateRefreshToken(ctx, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *authService) Signin(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error) {
|
||||
if !constant.EMAIL_REGEX.MatchString(dto.Email) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid email")
|
||||
}
|
||||
|
||||
err := constant.ValidatePassword(dto.Password)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
user, err := a.userRepo.GetByEmail(ctx, dto.Email)
|
||||
if err != nil || user == nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.Password)); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusUnauthorized, "Invalid identity or password!")
|
||||
}
|
||||
|
||||
data, err := a.genToken(user.ID, models.RolesEntityToRoleConstant(user.Roles))
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
|
||||
}
|
||||
var pgID pgtype.UUID
|
||||
err = pgID.Scan(user.ID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
err = a.saveNewRefreshToken(
|
||||
ctx,
|
||||
sqlc.UpdateUserRefreshTokenParams{
|
||||
ID: pgID,
|
||||
RefreshToken: pgtype.Text{
|
||||
String: data.RefreshToken,
|
||||
Valid: data.RefreshToken != "",
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
||||
}
|
||||
|
||||
func (a *authService) RefreshToken(ctx context.Context, id string) (*response.AuthResponse, error) {
|
||||
var pgID pgtype.UUID
|
||||
err := pgID.Scan(id)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
user, err := a.userRepo.GetByID(ctx, pgID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid user data")
|
||||
}
|
||||
roles := models.RolesEntityToRoleConstant(user.Roles)
|
||||
|
||||
if slices.Contains(roles, constant.BANNED) {
|
||||
return nil, fiber.NewError(fiber.StatusUnauthorized, "User is banned!")
|
||||
}
|
||||
|
||||
data, err := a.genToken(id, roles)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
err = a.saveNewRefreshToken(
|
||||
ctx,
|
||||
sqlc.UpdateUserRefreshTokenParams{
|
||||
ID: pgID,
|
||||
RefreshToken: pgtype.Text{
|
||||
String: data.RefreshToken,
|
||||
Valid: data.RefreshToken != "",
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*response.AuthResponse, error) {
|
||||
if !constant.EMAIL_REGEX.MatchString(dto.Email) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid email")
|
||||
}
|
||||
err := constant.ValidatePassword(dto.Password)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
user, err := a.userRepo.GetByEmail(ctx, dto.Email)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
user, err = a.userRepo.UpsertUser(
|
||||
ctx,
|
||||
sqlc.UpsertUserParams{
|
||||
Email: dto.Email,
|
||||
PasswordHash: pgtype.Text{
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
_, err = a.userRepo.CreateProfile(
|
||||
ctx,
|
||||
sqlc.CreateUserProfileParams{
|
||||
UserID: userId,
|
||||
DisplayName: pgtype.Text{
|
||||
String: dto.DisplayName,
|
||||
Valid: dto.DisplayName != "",
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
err = a.roleRepo.AddUserRole(
|
||||
ctx,
|
||||
sqlc.AddUserRoleParams{
|
||||
UserID: userId,
|
||||
Name: constant.USER.String(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
data, err := a.genToken(user.ID, constant.USER.ToSlice())
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
err = a.saveNewRefreshToken(
|
||||
ctx,
|
||||
sqlc.UpdateUserRefreshTokenParams{
|
||||
ID: userId,
|
||||
RefreshToken: pgtype.Text{
|
||||
String: data.RefreshToken,
|
||||
Valid: data.RefreshToken != "",
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ForgotPassword implements [AuthService].
|
||||
func (a *authService) ForgotPassword(ctx context.Context) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// SigninWith3rd implements [AuthService].
|
||||
func (a *authService) SigninWith3rd(ctx context.Context) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// CreateToken implements [AuthService].
|
||||
func (a *authService) CreateToken(ctx context.Context) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Verify implements [AuthService].
|
||||
func (a *authService) VerifyToken(ctx context.Context) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
57
internal/services/tileService.go
Normal file
57
internal/services/tileService.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"history-api/internal/repositories"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type TileService interface {
|
||||
GetMetadata(ctx context.Context) (map[string]string, error)
|
||||
GetTile(ctx context.Context, z, x, y int) ([]byte, map[string]string, error)
|
||||
}
|
||||
|
||||
type tileService struct {
|
||||
tileRepo repositories.TileRepository
|
||||
}
|
||||
|
||||
func NewTileService(
|
||||
TileRepo repositories.TileRepository,
|
||||
) TileService {
|
||||
return &tileService{
|
||||
tileRepo: TileRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tileService) GetMetadata(ctx context.Context) (map[string]string, error) {
|
||||
metaData, err := t.tileRepo.GetMetadata(ctx)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return metaData, nil
|
||||
}
|
||||
|
||||
|
||||
func (t *tileService) GetTile(ctx context.Context, z, x, y int) ([]byte, map[string]string, error) {
|
||||
contentType := make(map[string]string)
|
||||
|
||||
data, format, isPBF, err := t.tileRepo.GetTile(ctx, z, x, y)
|
||||
if err != nil {
|
||||
return nil, contentType, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
contentType["Content-Type"] = "image/png"
|
||||
if format == "jpg" {
|
||||
contentType["Content-Type"] = "image/jpeg"
|
||||
}
|
||||
if format == "pbf" {
|
||||
contentType["Content-Type"] = "application/x-protobuf"
|
||||
}
|
||||
|
||||
if isPBF {
|
||||
contentType["Content-Encoding"] = "gzip"
|
||||
}
|
||||
|
||||
return data, contentType, nil
|
||||
|
||||
}
|
||||
83
internal/services/userService.go
Normal file
83
internal/services/userService.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"history-api/internal/dtos/request"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/repositories"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
userRepo repositories.UserRepository
|
||||
roleRepo repositories.RoleRepository
|
||||
}
|
||||
|
||||
func NewUserService(
|
||||
userRepo repositories.UserRepository,
|
||||
roleRepo repositories.RoleRepository,
|
||||
) UserService {
|
||||
return &userService{
|
||||
userRepo: userRepo,
|
||||
roleRepo: roleRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// ChangePassword implements [UserService].
|
||||
func (u *userService) ChangePassword(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// ChangeRoleUser implements [UserService].
|
||||
func (u *userService) ChangeRoleUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// DeleteUser implements [UserService].
|
||||
func (u *userService) DeleteUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetAllUser implements [UserService].
|
||||
func (u *userService) GetAllUser(ctx context.Context, id string) ([]*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetUserByID implements [UserService].
|
||||
func (u *userService) GetUserByID(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetUserCurrent implements [UserService].
|
||||
func (u *userService) GetUserCurrent(ctx context.Context, dto *request.SignInDto) (*response.AuthResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// RestoreUser implements [UserService].
|
||||
func (u *userService) RestoreUser(ctx context.Context, id string) (*response.UserResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
Reference in New Issue
Block a user