UPDATE: Fix bug
All checks were successful
Build and Release / release (push) Successful in 1m27s

This commit is contained in:
2026-04-10 15:52:09 +07:00
parent 0896fd587e
commit af76d2a26a
22 changed files with 586 additions and 129 deletions

View File

@@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS user_verifications (
id UUID PRIMARY KEY DEFAULT uuidv7(), id UUID PRIMARY KEY DEFAULT uuidv7(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE CASCADE,
verify_type SMALLINT NOT NULL, verify_type SMALLINT NOT NULL,
content TEXT,
is_deleted BOOLEAN NOT NULL DEFAULT false, is_deleted BOOLEAN NOT NULL DEFAULT false,
status SMALLINT NOT NULL DEFAULT 1, status SMALLINT NOT NULL DEFAULT 1,
reviewed_by UUID REFERENCES users(id), reviewed_by UUID REFERENCES users(id),

View File

@@ -16,23 +16,23 @@ SELECT id, name, is_deleted, created_at, updated_at
FROM roles FROM roles
WHERE id = ANY($1::uuid[]) AND is_deleted = false; WHERE id = ANY($1::uuid[]) AND is_deleted = false;
-- name: AddUserRole :exec -- name: CreateUserRole :exec
INSERT INTO user_roles (user_id, role_id) INSERT INTO user_roles (user_id, role_id)
SELECT $1, unnest($2::uuid[]) SELECT $1, unnest($2::uuid[])
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;
-- name: RemoveUserRole :exec -- name: DeleteUserRole :exec
DELETE FROM user_roles ur DELETE FROM user_roles ur
USING roles r USING roles r
WHERE ur.role_id = r.id WHERE ur.role_id = r.id
AND ur.user_id = $1 AND ur.user_id = $1
AND r.name = $2; AND r.name = $2;
-- name: RemoveAllRolesFromUser :exec -- name: BulkDeleteRolesFromUser :exec
DELETE FROM user_roles DELETE FROM user_roles
WHERE user_id = $1; WHERE user_id = $1;
-- name: RemoveAllUsersFromRole :exec -- name: BulkDeleteUsersFromRole :exec
DELETE FROM user_roles DELETE FROM user_roles
WHERE role_id = $1; WHERE role_id = $1;

View File

@@ -26,14 +26,14 @@ RETURNING *;
-- name: UpdateUserProfile :one -- name: UpdateUserProfile :one
UPDATE user_profiles UPDATE user_profiles
SET SET
display_name = $1, display_name = COALESCE($1, display_name),
full_name = $2, full_name = COALESCE($2, full_name),
avatar_url = $3, avatar_url = COALESCE($3, avatar_url),
bio = $4, bio = COALESCE($4, bio),
location = $5, location = COALESCE($5, location),
website = $6, website = COALESCE($6, website),
country_code = $7, country_code = COALESCE($7, country_code),
phone = $8, phone = COALESCE($8, phone),
updated_at = now() updated_at = now()
WHERE user_id = $9 WHERE user_id = $9
RETURNING *; RETURNING *;

View File

@@ -1,8 +1,8 @@
-- name: CreateUserVerification :one -- name: CreateUserVerification :one
INSERT INTO user_verifications ( INSERT INTO user_verifications (
user_id, verify_type user_id, verify_type, content
) VALUES ( ) VALUES (
$1, $2 $1, $2, $3
) )
RETURNING *; RETURNING *;
@@ -11,6 +11,7 @@ SELECT
uv.id, uv.id,
uv.user_id, uv.user_id,
uv.verify_type, uv.verify_type,
uv.content,
uv.is_deleted, uv.is_deleted,
uv.status, uv.status,
uv.reviewed_by, uv.reviewed_by,
@@ -43,6 +44,7 @@ SELECT
uv.id, uv.id,
uv.user_id, uv.user_id,
uv.verify_type, uv.verify_type,
uv.content,
uv.is_deleted, uv.is_deleted,
uv.status, uv.status,
uv.reviewed_by, uv.reviewed_by,
@@ -91,14 +93,86 @@ WHERE verification_id = $1 AND media_id = $2;
-- name: CreateVerificationMedia :exec -- name: CreateVerificationMedia :exec
INSERT INTO verification_medias ( INSERT INTO verification_medias (
verification_id, media_id verification_id, media_id
) VALUES ( )
$1, $2 SELECT $1, unnest($2::uuid[])
); ON CONFLICT DO NOTHING;
-- name: DeleteAllVerificationMedias :exec -- name: BulkDeleteVerificationByMediaId :exec
DELETE FROM verification_medias DELETE FROM verification_medias
WHERE verification_id = $1; WHERE media_id = $1;
-- name: BulkDeleteVerificationMedias :exec -- name: DeleteVerificationMedias :exec
DELETE FROM verification_medias DELETE FROM verification_medias
WHERE verification_id = $1 AND media_id = ANY($2::uuid[]); WHERE verification_id = $1 AND media_id = ANY($2::uuid[]);
-- name: SearchUserVerifications :many
SELECT
uv.id,
uv.user_id,
uv.verify_type,
uv.content,
uv.is_deleted,
uv.status,
uv.reviewed_by,
uv.reviewed_at,
uv.created_at,
(
SELECT COALESCE(
json_agg(
json_build_object(
'id', m.id,
'storage_key', m.storage_key,
'original_name', m.original_name,
'mime_type', m.mime_type,
'size', m.size,
'file_metadata', m.file_metadata,
'created_at', m.created_at
)
),
'[]'
)::json
FROM verification_medias vm
JOIN medias m ON vm.media_id = m.id
WHERE vm.verification_id = uv.id
) AS medias
FROM user_verifications uv
WHERE
uv.is_deleted = false
AND (sqlc.narg('user_ids')::uuid[] IS NULL OR uv.user_id = ANY(sqlc.narg('user_ids')::uuid[]))
AND (sqlc.narg('verify_types')::text[] IS NULL OR uv.verify_type = ANY(sqlc.narg('verify_types')::text[]))
AND (sqlc.narg('statuses')::text[] IS NULL OR uv.status = ANY(sqlc.narg('statuses')::text[]))
AND (sqlc.narg('reviewed_by')::uuid IS NULL OR uv.reviewed_by = sqlc.narg('reviewed_by')::uuid)
AND (sqlc.narg('created_after')::timestamptz IS NULL OR uv.created_at >= sqlc.narg('created_after')::timestamptz)
AND (sqlc.narg('created_before')::timestamptz IS NULL OR uv.created_at <= sqlc.narg('created_before')::timestamptz)
AND (
sqlc.narg('search_text')::text IS NULL OR
uv.id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' OR
uv.content::text ILIKE '%' || sqlc.narg('search_text')::text || '%'
)
ORDER BY
CASE WHEN sqlc.narg('sort') = 'created_at' AND sqlc.narg('order') = 'asc' THEN uv.created_at END ASC,
CASE WHEN sqlc.narg('sort') = 'created_at' AND sqlc.narg('order') = 'desc' THEN uv.created_at END DESC,
CASE WHEN sqlc.narg('sort') = 'reviewed_at' AND sqlc.narg('order') = 'asc' THEN uv.reviewed_at END ASC,
CASE WHEN sqlc.narg('sort') = 'reviewed_at' AND sqlc.narg('order') = 'desc' THEN uv.reviewed_at END DESC,
CASE WHEN sqlc.narg('sort') = 'status' AND sqlc.narg('order') = 'asc' THEN uv.status END ASC,
CASE WHEN sqlc.narg('sort') = 'status' AND sqlc.narg('order') = 'desc' THEN uv.status END DESC,
CASE WHEN sqlc.narg('sort') IS NULL THEN uv.created_at END DESC
LIMIT sqlc.arg('limit')
OFFSET sqlc.arg('offset');
-- name: CountUserVerifications :one
SELECT count(*)
FROM user_verifications uv
WHERE
uv.is_deleted = false
AND (sqlc.narg('user_ids')::uuid[] IS NULL OR uv.user_id = ANY(sqlc.narg('user_ids')::uuid[]))
AND (sqlc.narg('verify_types')::text[] IS NULL OR uv.verify_type = ANY(sqlc.narg('verify_types')::text[]))
AND (sqlc.narg('statuses')::text[] IS NULL OR uv.status = ANY(sqlc.narg('statuses')::text[]))
AND (sqlc.narg('reviewed_by')::uuid IS NULL OR uv.reviewed_by = sqlc.narg('reviewed_by')::uuid)
AND (sqlc.narg('created_after')::timestamptz IS NULL OR uv.created_at >= sqlc.narg('created_after')::timestamptz)
AND (sqlc.narg('created_before')::timestamptz IS NULL OR uv.created_at <= sqlc.narg('created_before')::timestamptz)
AND (
sqlc.narg('search_text')::text IS NULL OR
uv.id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' OR
uv.content::text ILIKE '%' || sqlc.narg('search_text')::text || '%'
);

View File

@@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS user_verifications (
id UUID PRIMARY KEY DEFAULT uuidv7(), id UUID PRIMARY KEY DEFAULT uuidv7(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE CASCADE,
verify_type SMALLINT NOT NULL, verify_type SMALLINT NOT NULL,
content TEXT,
is_deleted BOOLEAN NOT NULL DEFAULT false, is_deleted BOOLEAN NOT NULL DEFAULT false,
status SMALLINT NOT NULL DEFAULT 1, status SMALLINT NOT NULL DEFAULT 1,
reviewed_by UUID REFERENCES users(id), reviewed_by UUID REFERENCES users(id),

View File

@@ -3,16 +3,15 @@ package request
import "time" import "time"
type UpdateProfileDto struct { type UpdateProfileDto struct {
DisplayName string `json:"display_name" validate:"omitempty,min=2,max=50"` DisplayName *string `json:"display_name" validate:"omitempty,min=2,max=50"`
FullName string `json:"full_name" validate:"omitempty,min=2,max=100"` FullName *string `json:"full_name" validate:"omitempty,min=2,max=100"`
AvatarUrl string `json:"avatar_url" validate:"omitempty,url,image_url"` AvatarUrl *string `json:"avatar_url" validate:"omitempty,url,image_url"`
Bio string `json:"bio" validate:"omitempty,max=255"` Bio *string `json:"bio" validate:"omitempty,max=255"`
Location string `json:"location" validate:"omitempty,max=100"` Location *string `json:"location" validate:"omitempty,max=100"`
Website string `json:"website" validate:"omitempty,url"` Website *string `json:"website" validate:"omitempty,url"`
CountryCode string `json:"country_code" validate:"omitempty,len=2"` CountryCode *string `json:"country_code" validate:"omitempty,len=2"`
Phone string `json:"phone" validate:"omitempty,min=8,max=20"` Phone *string `json:"phone" validate:"omitempty,min=8,max=20"`
} }
type ChangePasswordDto struct { type ChangePasswordDto struct {
OldPassword string `json:"old_password" validate:"required,min=8,max=64"` OldPassword string `json:"old_password" validate:"required,min=8,max=64"`
NewPassword string `json:"new_password" validate:"required,min=8,max=64,nefield=OldPassword"` NewPassword string `json:"new_password" validate:"required,min=8,max=64,nefield=OldPassword"`

View File

@@ -0,0 +1,25 @@
package request
import "time"
type SearchUserVerificationDto struct {
PaginationDto
Sort string `json:"sort" query:"sort" validate:"omitempty,oneof=id created_at reviewed_at status"`
Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"`
UserIDs []string `json:"user_ids" query:"user_ids" validate:"omitempty,dive,uuid"`
VerifyTypes []string `json:"verify_types" query:"verify_types" validate:"omitempty,dive,ascii"`
Statuses []string `json:"statuses" query:"statuses" validate:"omitempty,dive,ascii"`
ReviewedBy *string `json:"reviewed_by" query:"reviewed_by" validate:"omitempty,uuid"`
CreatedAfter *time.Time `json:"created_after" query:"created_after" validate:"omitempty"`
CreatedBefore *time.Time `json:"created_before" query:"created_before" validate:"omitempty,gtfield=CreatedAfter"`
}
type CreateUserVerificationDto struct {
VerifyType string `json:"verify_type" validate:"required,oneof=ID_CARD EDUCATION EXPERT OTHER"`
Content string `json:"content" validate:"required"`
MediaIDs []string `json:"media_ids" validate:"omitempty,dive,uuid"`
}
type UpdateVerificationStatusDto struct {
Status string `json:"status" validate:"required,oneof=PENDING APPROVED REJECTED"`
}

View File

@@ -20,3 +20,13 @@ type MediaResponse struct {
CreatedAt *time.Time `json:"created_at"` CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"` UpdatedAt *time.Time `json:"updated_at"`
} }
type MediaSimpleResponse struct {
ID string `json:"id"`
StorageKey string `json:"storage_key"`
OriginalName string `json:"original_name"`
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
FileMetadata []byte `json:"file_metadata"`
CreatedAt time.Time `json:"created_at"`
}

View File

@@ -0,0 +1,15 @@
package response
import "time"
type UserVerificationResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
VerifyType string `json:"verify_type"`
Content string `json:"content"`
Status string `json:"status"`
ReviewedBy *string `json:"reviewed_by"`
ReviewedAt *time.Time `json:"reviewed_at"`
CreatedAt time.Time `json:"created_at"`
Medias []*MediaSimpleResponse `json:"media"`
}

View File

@@ -64,6 +64,7 @@ type UserVerification struct {
ID pgtype.UUID `json:"id"` ID pgtype.UUID `json:"id"`
UserID pgtype.UUID `json:"user_id"` UserID pgtype.UUID `json:"user_id"`
VerifyType int16 `json:"verify_type"` VerifyType int16 `json:"verify_type"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
Status int16 `json:"status"` Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"` ReviewedBy pgtype.UUID `json:"reviewed_by"`

View File

@@ -11,19 +11,23 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
const addUserRole = `-- name: AddUserRole :exec const bulkDeleteRolesFromUser = `-- name: BulkDeleteRolesFromUser :exec
INSERT INTO user_roles (user_id, role_id) DELETE FROM user_roles
SELECT $1, unnest($2::uuid[]) WHERE user_id = $1
ON CONFLICT DO NOTHING
` `
type AddUserRoleParams struct { func (q *Queries) BulkDeleteRolesFromUser(ctx context.Context, userID pgtype.UUID) error {
UserID pgtype.UUID `json:"user_id"` _, err := q.db.Exec(ctx, bulkDeleteRolesFromUser, userID)
Column2 []pgtype.UUID `json:"column_2"` return err
} }
func (q *Queries) AddUserRole(ctx context.Context, arg AddUserRoleParams) error { const bulkDeleteUsersFromRole = `-- name: BulkDeleteUsersFromRole :exec
_, err := q.db.Exec(ctx, addUserRole, arg.UserID, arg.Column2) DELETE FROM user_roles
WHERE role_id = $1
`
func (q *Queries) BulkDeleteUsersFromRole(ctx context.Context, roleID pgtype.UUID) error {
_, err := q.db.Exec(ctx, bulkDeleteUsersFromRole, roleID)
return err return err
} }
@@ -46,6 +50,22 @@ func (q *Queries) CreateRole(ctx context.Context, name string) (Role, error) {
return i, err return i, err
} }
const createUserRole = `-- name: CreateUserRole :exec
INSERT INTO user_roles (user_id, role_id)
SELECT $1, unnest($2::uuid[])
ON CONFLICT DO NOTHING
`
type CreateUserRoleParams struct {
UserID pgtype.UUID `json:"user_id"`
Column2 []pgtype.UUID `json:"column_2"`
}
func (q *Queries) CreateUserRole(ctx context.Context, arg CreateUserRoleParams) error {
_, err := q.db.Exec(ctx, createUserRole, arg.UserID, arg.Column2)
return err
}
const deleteRole = `-- name: DeleteRole :exec const deleteRole = `-- name: DeleteRole :exec
UPDATE roles UPDATE roles
SET SET
@@ -59,6 +79,24 @@ func (q *Queries) DeleteRole(ctx context.Context, id pgtype.UUID) error {
return err return err
} }
const deleteUserRole = `-- name: DeleteUserRole :exec
DELETE FROM user_roles ur
USING roles r
WHERE ur.role_id = r.id
AND ur.user_id = $1
AND r.name = $2
`
type DeleteUserRoleParams struct {
UserID pgtype.UUID `json:"user_id"`
Name string `json:"name"`
}
func (q *Queries) DeleteUserRole(ctx context.Context, arg DeleteUserRoleParams) error {
_, err := q.db.Exec(ctx, deleteUserRole, arg.UserID, arg.Name)
return err
}
const getRoleByID = `-- name: GetRoleByID :one const getRoleByID = `-- name: GetRoleByID :one
SELECT id, name, is_deleted, created_at, updated_at FROM roles SELECT id, name, is_deleted, created_at, updated_at FROM roles
WHERE id = $1 AND is_deleted = false WHERE id = $1 AND is_deleted = false
@@ -159,44 +197,6 @@ func (q *Queries) GetRolesByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]
return items, nil return items, nil
} }
const removeAllRolesFromUser = `-- name: RemoveAllRolesFromUser :exec
DELETE FROM user_roles
WHERE user_id = $1
`
func (q *Queries) RemoveAllRolesFromUser(ctx context.Context, userID pgtype.UUID) error {
_, err := q.db.Exec(ctx, removeAllRolesFromUser, userID)
return err
}
const removeAllUsersFromRole = `-- name: RemoveAllUsersFromRole :exec
DELETE FROM user_roles
WHERE role_id = $1
`
func (q *Queries) RemoveAllUsersFromRole(ctx context.Context, roleID pgtype.UUID) error {
_, err := q.db.Exec(ctx, removeAllUsersFromRole, roleID)
return err
}
const removeUserRole = `-- name: RemoveUserRole :exec
DELETE FROM user_roles ur
USING roles r
WHERE ur.role_id = r.id
AND ur.user_id = $1
AND r.name = $2
`
type RemoveUserRoleParams struct {
UserID pgtype.UUID `json:"user_id"`
Name string `json:"name"`
}
func (q *Queries) RemoveUserRole(ctx context.Context, arg RemoveUserRoleParams) error {
_, err := q.db.Exec(ctx, removeUserRole, arg.UserID, arg.Name)
return err
}
const restoreRole = `-- name: RestoreRole :exec const restoreRole = `-- name: RestoreRole :exec
UPDATE roles UPDATE roles
SET SET

View File

@@ -542,14 +542,14 @@ func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPassword
const updateUserProfile = `-- name: UpdateUserProfile :one const updateUserProfile = `-- name: UpdateUserProfile :one
UPDATE user_profiles UPDATE user_profiles
SET SET
display_name = $1, display_name = COALESCE($1, display_name),
full_name = $2, full_name = COALESCE($2, full_name),
avatar_url = $3, avatar_url = COALESCE($3, avatar_url),
bio = $4, bio = COALESCE($4, bio),
location = $5, location = COALESCE($5, location),
website = $6, website = COALESCE($6, website),
country_code = $7, country_code = COALESCE($7, country_code),
phone = $8, phone = COALESCE($8, phone),
updated_at = now() updated_at = now()
WHERE user_id = $9 WHERE user_id = $9
RETURNING user_id, display_name, full_name, avatar_url, bio, location, website, country_code, phone, created_at, updated_at RETURNING user_id, display_name, full_name, avatar_url, bio, location, website, country_code, phone, created_at, updated_at

View File

@@ -11,27 +11,82 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
const bulkDeleteVerificationByMediaId = `-- name: BulkDeleteVerificationByMediaId :exec
DELETE FROM verification_medias
WHERE media_id = $1
`
func (q *Queries) BulkDeleteVerificationByMediaId(ctx context.Context, mediaID pgtype.UUID) error {
_, err := q.db.Exec(ctx, bulkDeleteVerificationByMediaId, mediaID)
return err
}
const countUserVerifications = `-- name: CountUserVerifications :one
SELECT count(*)
FROM user_verifications uv
WHERE
uv.is_deleted = false
AND ($1::uuid[] IS NULL OR uv.user_id = ANY($1::uuid[]))
AND ($2::text[] IS NULL OR uv.verify_type = ANY($2::text[]))
AND ($3::text[] IS NULL OR uv.status = ANY($3::text[]))
AND ($4::uuid IS NULL OR uv.reviewed_by = $4::uuid)
AND ($5::timestamptz IS NULL OR uv.created_at >= $5::timestamptz)
AND ($6::timestamptz IS NULL OR uv.created_at <= $6::timestamptz)
AND (
$7::text IS NULL OR
uv.id::text ILIKE '%' || $7::text || '%' OR
uv.content::text ILIKE '%' || $7::text || '%'
)
`
type CountUserVerificationsParams struct {
UserIds []pgtype.UUID `json:"user_ids"`
VerifyTypes []string `json:"verify_types"`
Statuses []string `json:"statuses"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
CreatedAfter pgtype.Timestamptz `json:"created_after"`
CreatedBefore pgtype.Timestamptz `json:"created_before"`
SearchText pgtype.Text `json:"search_text"`
}
func (q *Queries) CountUserVerifications(ctx context.Context, arg CountUserVerificationsParams) (int64, error) {
row := q.db.QueryRow(ctx, countUserVerifications,
arg.UserIds,
arg.VerifyTypes,
arg.Statuses,
arg.ReviewedBy,
arg.CreatedAfter,
arg.CreatedBefore,
arg.SearchText,
)
var count int64
err := row.Scan(&count)
return count, err
}
const createUserVerification = `-- name: CreateUserVerification :one const createUserVerification = `-- name: CreateUserVerification :one
INSERT INTO user_verifications ( INSERT INTO user_verifications (
user_id, verify_type user_id, verify_type, content
) VALUES ( ) VALUES (
$1, $2 $1, $2, $3
) )
RETURNING id, user_id, verify_type, is_deleted, status, reviewed_by, reviewed_at, created_at RETURNING id, user_id, verify_type, content, is_deleted, status, reviewed_by, reviewed_at, created_at
` `
type CreateUserVerificationParams struct { type CreateUserVerificationParams struct {
UserID pgtype.UUID `json:"user_id"` UserID pgtype.UUID `json:"user_id"`
VerifyType int16 `json:"verify_type"` VerifyType int16 `json:"verify_type"`
Content pgtype.Text `json:"content"`
} }
func (q *Queries) CreateUserVerification(ctx context.Context, arg CreateUserVerificationParams) (UserVerification, error) { func (q *Queries) CreateUserVerification(ctx context.Context, arg CreateUserVerificationParams) (UserVerification, error) {
row := q.db.QueryRow(ctx, createUserVerification, arg.UserID, arg.VerifyType) row := q.db.QueryRow(ctx, createUserVerification, arg.UserID, arg.VerifyType, arg.Content)
var i UserVerification var i UserVerification
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.UserID, &i.UserID,
&i.VerifyType, &i.VerifyType,
&i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.Status, &i.Status,
&i.ReviewedBy, &i.ReviewedBy,
@@ -44,18 +99,18 @@ func (q *Queries) CreateUserVerification(ctx context.Context, arg CreateUserVeri
const createVerificationMedia = `-- name: CreateVerificationMedia :exec const createVerificationMedia = `-- name: CreateVerificationMedia :exec
INSERT INTO verification_medias ( INSERT INTO verification_medias (
verification_id, media_id verification_id, media_id
) VALUES (
$1, $2
) )
SELECT $1, unnest($2::uuid[])
ON CONFLICT DO NOTHING
` `
type CreateVerificationMediaParams struct { type CreateVerificationMediaParams struct {
VerificationID pgtype.UUID `json:"verification_id"` VerificationID pgtype.UUID `json:"verification_id"`
MediaID pgtype.UUID `json:"media_id"` Column2 []pgtype.UUID `json:"column_2"`
} }
func (q *Queries) CreateVerificationMedia(ctx context.Context, arg CreateVerificationMediaParams) error { func (q *Queries) CreateVerificationMedia(ctx context.Context, arg CreateVerificationMediaParams) error {
_, err := q.db.Exec(ctx, createVerificationMedia, arg.VerificationID, arg.MediaID) _, err := q.db.Exec(ctx, createVerificationMedia, arg.VerificationID, arg.Column2)
return err return err
} }
@@ -85,11 +140,27 @@ func (q *Queries) DeleteVerificationMedia(ctx context.Context, arg DeleteVerific
return err return err
} }
const deleteVerificationMedias = `-- name: DeleteVerificationMedias :exec
DELETE FROM verification_medias
WHERE verification_id = $1 AND media_id = ANY($2::uuid[])
`
type DeleteVerificationMediasParams struct {
VerificationID pgtype.UUID `json:"verification_id"`
Column2 []pgtype.UUID `json:"column_2"`
}
func (q *Queries) DeleteVerificationMedias(ctx context.Context, arg DeleteVerificationMediasParams) error {
_, err := q.db.Exec(ctx, deleteVerificationMedias, arg.VerificationID, arg.Column2)
return err
}
const getUserVerificationByID = `-- name: GetUserVerificationByID :one const getUserVerificationByID = `-- name: GetUserVerificationByID :one
SELECT SELECT
uv.id, uv.id,
uv.user_id, uv.user_id,
uv.verify_type, uv.verify_type,
uv.content,
uv.is_deleted, uv.is_deleted,
uv.status, uv.status,
uv.reviewed_by, uv.reviewed_by,
@@ -122,6 +193,7 @@ type GetUserVerificationByIDRow struct {
ID pgtype.UUID `json:"id"` ID pgtype.UUID `json:"id"`
UserID pgtype.UUID `json:"user_id"` UserID pgtype.UUID `json:"user_id"`
VerifyType int16 `json:"verify_type"` VerifyType int16 `json:"verify_type"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
Status int16 `json:"status"` Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"` ReviewedBy pgtype.UUID `json:"reviewed_by"`
@@ -137,6 +209,7 @@ func (q *Queries) GetUserVerificationByID(ctx context.Context, id pgtype.UUID) (
&i.ID, &i.ID,
&i.UserID, &i.UserID,
&i.VerifyType, &i.VerifyType,
&i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.Status, &i.Status,
&i.ReviewedBy, &i.ReviewedBy,
@@ -152,6 +225,7 @@ SELECT
uv.id, uv.id,
uv.user_id, uv.user_id,
uv.verify_type, uv.verify_type,
uv.content,
uv.is_deleted, uv.is_deleted,
uv.status, uv.status,
uv.reviewed_by, uv.reviewed_by,
@@ -185,6 +259,7 @@ type GetUserVerificationsRow struct {
ID pgtype.UUID `json:"id"` ID pgtype.UUID `json:"id"`
UserID pgtype.UUID `json:"user_id"` UserID pgtype.UUID `json:"user_id"`
VerifyType int16 `json:"verify_type"` VerifyType int16 `json:"verify_type"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
Status int16 `json:"status"` Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"` ReviewedBy pgtype.UUID `json:"reviewed_by"`
@@ -206,6 +281,133 @@ func (q *Queries) GetUserVerifications(ctx context.Context, userID pgtype.UUID)
&i.ID, &i.ID,
&i.UserID, &i.UserID,
&i.VerifyType, &i.VerifyType,
&i.Content,
&i.IsDeleted,
&i.Status,
&i.ReviewedBy,
&i.ReviewedAt,
&i.CreatedAt,
&i.Medias,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const searchUserVerifications = `-- name: SearchUserVerifications :many
SELECT
uv.id,
uv.user_id,
uv.verify_type,
uv.content,
uv.is_deleted,
uv.status,
uv.reviewed_by,
uv.reviewed_at,
uv.created_at,
(
SELECT COALESCE(
json_agg(
json_build_object(
'id', m.id,
'storage_key', m.storage_key,
'original_name', m.original_name,
'mime_type', m.mime_type,
'size', m.size,
'file_metadata', m.file_metadata,
'created_at', m.created_at
)
),
'[]'
)::json
FROM verification_medias vm
JOIN medias m ON vm.media_id = m.id
WHERE vm.verification_id = uv.id
) AS medias
FROM user_verifications uv
WHERE
uv.is_deleted = false
AND ($1::uuid[] IS NULL OR uv.user_id = ANY($1::uuid[]))
AND ($2::text[] IS NULL OR uv.verify_type = ANY($2::text[]))
AND ($3::text[] IS NULL OR uv.status = ANY($3::text[]))
AND ($4::uuid IS NULL OR uv.reviewed_by = $4::uuid)
AND ($5::timestamptz IS NULL OR uv.created_at >= $5::timestamptz)
AND ($6::timestamptz IS NULL OR uv.created_at <= $6::timestamptz)
AND (
$7::text IS NULL OR
uv.id::text ILIKE '%' || $7::text || '%' OR
uv.content::text ILIKE '%' || $7::text || '%'
)
ORDER BY
CASE WHEN $8 = 'created_at' AND $9 = 'asc' THEN uv.created_at END ASC,
CASE WHEN $8 = 'created_at' AND $9 = 'desc' THEN uv.created_at END DESC,
CASE WHEN $8 = 'reviewed_at' AND $9 = 'asc' THEN uv.reviewed_at END ASC,
CASE WHEN $8 = 'reviewed_at' AND $9 = 'desc' THEN uv.reviewed_at END DESC,
CASE WHEN $8 = 'status' AND $9 = 'asc' THEN uv.status END ASC,
CASE WHEN $8 = 'status' AND $9 = 'desc' THEN uv.status END DESC,
CASE WHEN $8 IS NULL THEN uv.created_at END DESC
LIMIT $11
OFFSET $10
`
type SearchUserVerificationsParams struct {
UserIds []pgtype.UUID `json:"user_ids"`
VerifyTypes []string `json:"verify_types"`
Statuses []string `json:"statuses"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
CreatedAfter pgtype.Timestamptz `json:"created_after"`
CreatedBefore pgtype.Timestamptz `json:"created_before"`
SearchText pgtype.Text `json:"search_text"`
Sort interface{} `json:"sort"`
Order interface{} `json:"order"`
Offset int32 `json:"offset"`
Limit int32 `json:"limit"`
}
type SearchUserVerificationsRow struct {
ID pgtype.UUID `json:"id"`
UserID pgtype.UUID `json:"user_id"`
VerifyType int16 `json:"verify_type"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"`
Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
Medias []byte `json:"medias"`
}
func (q *Queries) SearchUserVerifications(ctx context.Context, arg SearchUserVerificationsParams) ([]SearchUserVerificationsRow, error) {
rows, err := q.db.Query(ctx, searchUserVerifications,
arg.UserIds,
arg.VerifyTypes,
arg.Statuses,
arg.ReviewedBy,
arg.CreatedAfter,
arg.CreatedBefore,
arg.SearchText,
arg.Sort,
arg.Order,
arg.Offset,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SearchUserVerificationsRow{}
for rows.Next() {
var i SearchUserVerificationsRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.VerifyType,
&i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.Status, &i.Status,
&i.ReviewedBy, &i.ReviewedBy,

View File

@@ -16,6 +16,16 @@ type MediaEntity struct {
CreatedAt *time.Time `json:"created_at"` CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"` UpdatedAt *time.Time `json:"updated_at"`
} }
type MediaSimpleEntity struct {
ID string `json:"id"`
StorageKey string `json:"storage_key"`
OriginalName string `json:"original_name"`
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
FileMetadata []byte `json:"file_metadata"`
CreatedAt time.Time `json:"created_at"`
}
type MediaStorageEntity struct { type MediaStorageEntity struct {
ID string `json:"id"` ID string `json:"id"`

View File

@@ -0,0 +1,66 @@
package models
import (
"encoding/json"
"history-api/internal/dtos/response"
"history-api/pkg/constants"
"time"
)
type UserVerificationEntity struct {
ID string `json:"id"`
UserID string `json:"user_id"`
VerifyType string `json:"verify_type"`
Content string `json:"content"`
IsDeleted bool `json:"is_deleted"`
Status constants.VerifyType `json:"status"`
ReviewedBy *string `json:"reviewed_by"`
ReviewedAt *time.Time `json:"reviewed_at"`
CreatedAt time.Time `json:"created_at"`
Media []*MediaSimpleEntity `json:"media"`
}
func (u *UserVerificationEntity) ParseMedia(data []byte) error {
if len(data) == 0 {
u.Media = []*MediaSimpleEntity{}
return nil
}
return json.Unmarshal(data, &u.Media)
}
func (u *UserVerificationEntity) ToResponse() *response.UserVerificationResponse {
mediaResponses := make([]*response.MediaSimpleResponse, 0)
for _, m := range u.Media {
if m != nil {
mediaResponses = append(mediaResponses, &response.MediaSimpleResponse{
ID: m.ID,
StorageKey: m.StorageKey,
OriginalName: m.OriginalName,
MimeType: m.MimeType,
Size: m.Size,
FileMetadata: m.FileMetadata,
CreatedAt: m.CreatedAt,
})
}
}
res := &response.UserVerificationResponse{
ID: u.ID,
UserID: u.UserID,
VerifyType: u.VerifyType,
Content: u.Content,
Status: u.Status.String(),
CreatedAt: u.CreatedAt,
Medias: mediaResponses,
}
if u.ReviewedBy != nil {
res.ReviewedBy = u.ReviewedBy
}
if u.ReviewedAt != nil {
res.ReviewedAt = u.ReviewedAt
}
return res
}

View File

@@ -24,10 +24,10 @@ type RoleRepository interface {
Update(ctx context.Context, params sqlc.UpdateRoleParams) (*models.RoleEntity, error) Update(ctx context.Context, params sqlc.UpdateRoleParams) (*models.RoleEntity, error)
Delete(ctx context.Context, id pgtype.UUID) error Delete(ctx context.Context, id pgtype.UUID) error
Restore(ctx context.Context, id pgtype.UUID) error Restore(ctx context.Context, id pgtype.UUID) error
AddUserRole(ctx context.Context, params sqlc.AddUserRoleParams) error CreateUserRole(ctx context.Context, params sqlc.CreateUserRoleParams) error
RemoveUserRole(ctx context.Context, params sqlc.RemoveUserRoleParams) error DeleteUserRole(ctx context.Context, params sqlc.DeleteUserRoleParams) error
RemoveAllRolesFromUser(ctx context.Context, userId pgtype.UUID) error BulkDeleteRolesFromUser(ctx context.Context, userId pgtype.UUID) error
RemoveAllUsersFromRole(ctx context.Context, roleId pgtype.UUID) error BulkDeleteUsersFromRole(ctx context.Context, roleId pgtype.UUID) error
} }
type roleRepository struct { type roleRepository struct {
@@ -261,22 +261,22 @@ func (r *roleRepository) Restore(ctx context.Context, id pgtype.UUID) error {
return nil return nil
} }
func (r *roleRepository) AddUserRole(ctx context.Context, params sqlc.AddUserRoleParams) error { func (r *roleRepository) CreateUserRole(ctx context.Context, params sqlc.CreateUserRoleParams) error {
err := r.q.AddUserRole(ctx, params) err := r.q.CreateUserRole(ctx, params)
return err return err
} }
func (r *roleRepository) RemoveUserRole(ctx context.Context, params sqlc.RemoveUserRoleParams) error { func (r *roleRepository) DeleteUserRole(ctx context.Context, params sqlc.DeleteUserRoleParams) error {
err := r.q.RemoveUserRole(ctx, params) err := r.q.DeleteUserRole(ctx, params)
return err return err
} }
func (r *roleRepository) RemoveAllUsersFromRole(ctx context.Context, roleId pgtype.UUID) error { func (r *roleRepository) BulkDeleteUsersFromRole(ctx context.Context, roleId pgtype.UUID) error {
err := r.q.RemoveAllUsersFromRole(ctx, roleId) err := r.q.BulkDeleteUsersFromRole(ctx, roleId)
return err return err
} }
func (r *roleRepository) RemoveAllRolesFromUser(ctx context.Context, roleId pgtype.UUID) error { func (r *roleRepository) BulkDeleteRolesFromUser(ctx context.Context, roleId pgtype.UUID) error {
err := r.q.RemoveAllRolesFromUser(ctx, roleId) err := r.q.BulkDeleteRolesFromUser(ctx, roleId)
return err return err
} }

View File

@@ -0,0 +1,32 @@
package repositories
import (
"context"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/pkg/cache"
"github.com/jackc/pgx/v5/pgtype"
)
type VerificationRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.UserVerificationEntity, error)
GetByUserID(ctx context.Context, id pgtype.UUID) ([]*models.UserVerificationEntity, error)
Count(ctx context.Context, params sqlc.CountUserVerificationsParams) (int64, error)
Search(ctx context.Context, params sqlc.SearchUserVerificationsParams) ([]*models.UserVerificationEntity, error)
Delete(ctx context.Context, id pgtype.UUID) error
CreateVerificationMedia(ctx context.Context, params sqlc.CreateVerificationMediaParams) error
DeleteVerificationMedia(ctx context.Context, params sqlc.DeleteVerificationMediasParams) error
}
type verificationRepository struct {
q *sqlc.Queries
c cache.Cache
}
// func NewVerificationRepository(db sqlc.DBTX, c cache.Cache) VerificationRepository {
// return &verificationRepository{
// q: sqlc.New(db),
// c: c,
// }
// }

View File

@@ -318,9 +318,9 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
} }
err = a.roleRepo.AddUserRole( err = a.roleRepo.CreateUserRole(
ctx, ctx,
sqlc.AddUserRoleParams{ sqlc.CreateUserRoleParams{
UserID: userId, UserID: userId,
Column2: []pgtype.UUID{roleId}, Column2: []pgtype.UUID{roleId},
}, },
@@ -464,9 +464,9 @@ func (a *authService) SigninWithGoogle(ctx context.Context, dto *request.SigninW
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
} }
err = a.roleRepo.AddUserRole( err = a.roleRepo.CreateUserRole(
ctx, ctx,
sqlc.AddUserRoleParams{ sqlc.CreateUserRoleParams{
UserID: userId, UserID: userId,
Column2: []pgtype.UUID{roleId}, Column2: []pgtype.UUID{roleId},
}, },

View File

@@ -172,12 +172,12 @@ func (u *userService) ChangeRoleUser(ctx context.Context, claims *response.JWTCl
user.Roles = append(user.Roles, role.ToRoleSimple()) user.Roles = append(user.Roles, role.ToRoleSimple())
} }
err = u.roleRepo.RemoveAllRolesFromUser(ctx, userId) err = u.roleRepo.BulkDeleteRolesFromUser(ctx, userId)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
} }
err = u.roleRepo.AddUserRole(ctx, sqlc.AddUserRoleParams{ err = u.roleRepo.CreateUserRole(ctx, sqlc.CreateUserRoleParams{
UserID: userId, UserID: userId,
Column2: roleIdList, Column2: roleIdList,
}) })
@@ -239,14 +239,14 @@ func (u *userService) UpdateProfile(ctx context.Context, userId string, dto *req
newUser, err := u.userRepo.UpdateProfile( newUser, err := u.userRepo.UpdateProfile(
ctx, ctx,
sqlc.UpdateUserProfileParams{ sqlc.UpdateUserProfileParams{
DisplayName: pgtype.Text{String: dto.DisplayName, Valid: len(dto.DisplayName) > 0}, DisplayName: convert.PtrToText(dto.DisplayName),
FullName: pgtype.Text{String: dto.FullName, Valid: len(dto.FullName) > 0}, FullName: convert.PtrToText(dto.FullName),
AvatarUrl: pgtype.Text{String: dto.AvatarUrl, Valid: len(dto.AvatarUrl) > 0}, AvatarUrl: convert.PtrToText(dto.AvatarUrl),
Bio: pgtype.Text{String: dto.Bio, Valid: len(dto.Bio) > 0}, Bio: convert.PtrToText(dto.Bio),
Location: pgtype.Text{String: dto.Location, Valid: len(dto.Location) > 0}, Location: convert.PtrToText(dto.Location),
Website: pgtype.Text{String: dto.Website, Valid: len(dto.Website) > 0}, Website: convert.PtrToText(dto.Website),
CountryCode: pgtype.Text{String: dto.CountryCode, Valid: len(dto.CountryCode) > 0}, CountryCode: convert.PtrToText(dto.CountryCode),
Phone: pgtype.Text{String: dto.Phone, Valid: len(dto.Phone) > 0}, Phone: convert.PtrToText(dto.Phone),
UserID: pgID, UserID: pgID,
}, },
) )

View File

@@ -3,9 +3,11 @@ package constants
type VerifyType int16 type VerifyType int16
const ( const (
VerifyUnknown VerifyType = 0
VerifyIdCard VerifyType = 1 VerifyIdCard VerifyType = 1
VerifyEducation VerifyType = 2 VerifyEducation VerifyType = 2
VerifyExpert VerifyType = 3 VerifyExpert VerifyType = 3
VerifyOther VerifyType = 4
) )
func (t VerifyType) String() string { func (t VerifyType) String() string {
@@ -16,20 +18,24 @@ func (t VerifyType) String() string {
return "EDUCATION" return "EDUCATION"
case VerifyExpert: case VerifyExpert:
return "EXPERT" return "EXPERT"
case VerifyOther:
return "OTHER"
default: default:
return "UNKNOWN" return "UNKNOWN"
} }
} }
func ParseVerifyType(v int16) VerifyType { func ParseVerifyType(v string) VerifyType {
switch v { switch v {
case 1: case "ID_CARD":
return VerifyIdCard return VerifyIdCard
case 2: case "EDUCATION":
return VerifyEducation return VerifyEducation
case 3: case "EXPERT":
return VerifyExpert return VerifyExpert
case "OTHER":
return VerifyOther
default: default:
return 0 return VerifyUnknown
} }
} }

View File

@@ -43,3 +43,13 @@ func TimeToPtr(v pgtype.Timestamptz) *time.Time {
t := v.Time t := v.Time
return &t return &t
} }
func PtrToText(s *string) pgtype.Text {
if s == nil {
return pgtype.Text{Valid: false}
}
return pgtype.Text{
String: *s,
Valid: true,
}
}

View File

@@ -70,16 +70,21 @@ func SeedSuperAdmin(pool *pgxpool.Pool) error {
return err return err
} }
role, err := q.GetRoleByName(ctx, constants.ADMIN.String()) adminRole, err := q.GetRoleByName(ctx, constants.ADMIN.String())
if err != nil { if err != nil {
return err return err
} }
err = q.AddUserRole( useRole, err := q.GetRoleByName(ctx, constants.USER.String())
if err != nil {
return err
}
err = q.CreateUserRole(
ctx, ctx,
sqlc.AddUserRoleParams{ sqlc.CreateUserRoleParams{
UserID: user.ID, UserID: user.ID,
Column2: []pgtype.UUID{role.ID}, Column2: []pgtype.UUID{adminRole.ID, useRole.ID},
}, },
) )
if err != nil { if err != nil {