init
Some checks failed
Build and Release / release (push) Failing after 51s

This commit is contained in:
2026-03-25 22:29:07 +07:00
parent eedd300861
commit 79199f627d
65 changed files with 3215 additions and 689 deletions

View File

@@ -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,
})
}

View 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
}

View 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"`
}

View 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"`
}

View 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"`
}

View File

@@ -0,0 +1,6 @@
package response
type AuthResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}

View 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
}

View 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"`
}

View 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"`
}

View 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"`
}

View 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"`
}

View File

@@ -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"`
}

View File

@@ -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
`

View File

@@ -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"

View File

@@ -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"

View 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
View 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
View 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
View 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
}

View File

@@ -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
}

View 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
}

View File

@@ -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
}

View File

@@ -1 +0,0 @@
package routers

View 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)
}

View 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",
})
},
)
}

View 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)
}

View File

@@ -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")
}

View 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
}

View 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")
}