From af76d2a26a00c1e312110d3d809a97248fee0aa8 Mon Sep 17 00:00:00 2001 From: AzenKain Date: Fri, 10 Apr 2026 15:52:09 +0700 Subject: [PATCH] UPDATE: Fix bug --- db/migrations/000005_verifications.up.sql | 1 + db/query/roles.sql | 8 +- db/query/users.sql | 16 +- db/query/verification.sql | 92 +++++++- db/schema.sql | 1 + internal/dtos/request/user.go | 17 +- internal/dtos/request/verification.go | 25 ++ internal/dtos/response/media.go | 10 + internal/dtos/response/verification.go | 15 ++ internal/gen/sqlc/models.go | 1 + internal/gen/sqlc/roles.sql.go | 94 ++++---- internal/gen/sqlc/users.sql.go | 16 +- internal/gen/sqlc/verification.sql.go | 220 +++++++++++++++++- internal/models/media.go | 10 + internal/models/verification.go | 66 ++++++ internal/repositories/roleRepository.go | 24 +- .../repositories/verificationRepository.go | 32 +++ internal/services/authService.go | 8 +- internal/services/userService.go | 20 +- pkg/constants/verify.go | 16 +- pkg/convert/convert.go | 10 + pkg/database/seed.go | 13 +- 22 files changed, 586 insertions(+), 129 deletions(-) create mode 100644 internal/dtos/request/verification.go create mode 100644 internal/dtos/response/verification.go create mode 100644 internal/models/verification.go create mode 100644 internal/repositories/verificationRepository.go diff --git a/db/migrations/000005_verifications.up.sql b/db/migrations/000005_verifications.up.sql index dc6f6e8..5d506cb 100644 --- a/db/migrations/000005_verifications.up.sql +++ b/db/migrations/000005_verifications.up.sql @@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS user_verifications ( id UUID PRIMARY KEY DEFAULT uuidv7(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, verify_type SMALLINT NOT NULL, + content TEXT, is_deleted BOOLEAN NOT NULL DEFAULT false, status SMALLINT NOT NULL DEFAULT 1, reviewed_by UUID REFERENCES users(id), diff --git a/db/query/roles.sql b/db/query/roles.sql index 9af1e55..c3f27f9 100644 --- a/db/query/roles.sql +++ b/db/query/roles.sql @@ -16,23 +16,23 @@ SELECT id, name, is_deleted, created_at, updated_at FROM roles WHERE id = ANY($1::uuid[]) AND is_deleted = false; --- name: AddUserRole :exec +-- name: CreateUserRole :exec INSERT INTO user_roles (user_id, role_id) SELECT $1, unnest($2::uuid[]) ON CONFLICT DO NOTHING; --- name: RemoveUserRole :exec +-- 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; --- name: RemoveAllRolesFromUser :exec +-- name: BulkDeleteRolesFromUser :exec DELETE FROM user_roles WHERE user_id = $1; --- name: RemoveAllUsersFromRole :exec +-- name: BulkDeleteUsersFromRole :exec DELETE FROM user_roles WHERE role_id = $1; diff --git a/db/query/users.sql b/db/query/users.sql index 52f75a1..8242791 100644 --- a/db/query/users.sql +++ b/db/query/users.sql @@ -26,14 +26,14 @@ RETURNING *; -- 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, + display_name = COALESCE($1, display_name), + full_name = COALESCE($2, full_name), + avatar_url = COALESCE($3, avatar_url), + bio = COALESCE($4, bio), + location = COALESCE($5, location), + website = COALESCE($6, website), + country_code = COALESCE($7, country_code), + phone = COALESCE($8, phone), updated_at = now() WHERE user_id = $9 RETURNING *; diff --git a/db/query/verification.sql b/db/query/verification.sql index 7183562..f659442 100644 --- a/db/query/verification.sql +++ b/db/query/verification.sql @@ -1,8 +1,8 @@ -- name: CreateUserVerification :one INSERT INTO user_verifications ( - user_id, verify_type + user_id, verify_type, content ) VALUES ( - $1, $2 + $1, $2, $3 ) RETURNING *; @@ -11,6 +11,7 @@ SELECT uv.id, uv.user_id, uv.verify_type, + uv.content, uv.is_deleted, uv.status, uv.reviewed_by, @@ -43,6 +44,7 @@ SELECT uv.id, uv.user_id, uv.verify_type, + uv.content, uv.is_deleted, uv.status, uv.reviewed_by, @@ -91,14 +93,86 @@ WHERE verification_id = $1 AND media_id = $2; -- name: CreateVerificationMedia :exec INSERT INTO verification_medias ( 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 -WHERE verification_id = $1; +WHERE media_id = $1; --- name: BulkDeleteVerificationMedias :exec +-- name: DeleteVerificationMedias :exec DELETE FROM verification_medias -WHERE verification_id = $1 AND media_id = ANY($2::uuid[]); \ No newline at end of file +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 || '%' + ); \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql index fd4f032..1adc4d1 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS user_verifications ( id UUID PRIMARY KEY DEFAULT uuidv7(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, verify_type SMALLINT NOT NULL, + content TEXT, is_deleted BOOLEAN NOT NULL DEFAULT false, status SMALLINT NOT NULL DEFAULT 1, reviewed_by UUID REFERENCES users(id), diff --git a/internal/dtos/request/user.go b/internal/dtos/request/user.go index b3e19c3..73411be 100644 --- a/internal/dtos/request/user.go +++ b/internal/dtos/request/user.go @@ -3,16 +3,15 @@ package request import "time" type UpdateProfileDto struct { - DisplayName string `json:"display_name" validate:"omitempty,min=2,max=50"` - FullName string `json:"full_name" validate:"omitempty,min=2,max=100"` - AvatarUrl string `json:"avatar_url" validate:"omitempty,url,image_url"` - Bio string `json:"bio" validate:"omitempty,max=255"` - Location string `json:"location" validate:"omitempty,max=100"` - Website string `json:"website" validate:"omitempty,url"` - CountryCode string `json:"country_code" validate:"omitempty,len=2"` - Phone string `json:"phone" validate:"omitempty,min=8,max=20"` + DisplayName *string `json:"display_name" validate:"omitempty,min=2,max=50"` + FullName *string `json:"full_name" validate:"omitempty,min=2,max=100"` + AvatarUrl *string `json:"avatar_url" validate:"omitempty,url,image_url"` + Bio *string `json:"bio" validate:"omitempty,max=255"` + Location *string `json:"location" validate:"omitempty,max=100"` + Website *string `json:"website" validate:"omitempty,url"` + CountryCode *string `json:"country_code" validate:"omitempty,len=2"` + Phone *string `json:"phone" validate:"omitempty,min=8,max=20"` } - type ChangePasswordDto struct { OldPassword string `json:"old_password" validate:"required,min=8,max=64"` NewPassword string `json:"new_password" validate:"required,min=8,max=64,nefield=OldPassword"` diff --git a/internal/dtos/request/verification.go b/internal/dtos/request/verification.go new file mode 100644 index 0000000..a38e8b4 --- /dev/null +++ b/internal/dtos/request/verification.go @@ -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"` +} \ No newline at end of file diff --git a/internal/dtos/response/media.go b/internal/dtos/response/media.go index ec516c8..7eb2759 100644 --- a/internal/dtos/response/media.go +++ b/internal/dtos/response/media.go @@ -20,3 +20,13 @@ type MediaResponse struct { CreatedAt *time.Time `json:"created_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"` +} diff --git a/internal/dtos/response/verification.go b/internal/dtos/response/verification.go new file mode 100644 index 0000000..ae5c582 --- /dev/null +++ b/internal/dtos/response/verification.go @@ -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"` +} diff --git a/internal/gen/sqlc/models.go b/internal/gen/sqlc/models.go index 2255adc..33f0539 100644 --- a/internal/gen/sqlc/models.go +++ b/internal/gen/sqlc/models.go @@ -64,6 +64,7 @@ type UserVerification 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"` diff --git a/internal/gen/sqlc/roles.sql.go b/internal/gen/sqlc/roles.sql.go index c53c1a7..e0531d7 100644 --- a/internal/gen/sqlc/roles.sql.go +++ b/internal/gen/sqlc/roles.sql.go @@ -11,19 +11,23 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -const addUserRole = `-- name: AddUserRole :exec -INSERT INTO user_roles (user_id, role_id) -SELECT $1, unnest($2::uuid[]) -ON CONFLICT DO NOTHING +const bulkDeleteRolesFromUser = `-- name: BulkDeleteRolesFromUser :exec +DELETE FROM user_roles +WHERE user_id = $1 ` -type AddUserRoleParams struct { - UserID pgtype.UUID `json:"user_id"` - Column2 []pgtype.UUID `json:"column_2"` +func (q *Queries) BulkDeleteRolesFromUser(ctx context.Context, userID pgtype.UUID) error { + _, err := q.db.Exec(ctx, bulkDeleteRolesFromUser, userID) + return err } -func (q *Queries) AddUserRole(ctx context.Context, arg AddUserRoleParams) error { - _, err := q.db.Exec(ctx, addUserRole, arg.UserID, arg.Column2) +const bulkDeleteUsersFromRole = `-- name: BulkDeleteUsersFromRole :exec +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 } @@ -46,6 +50,22 @@ func (q *Queries) CreateRole(ctx context.Context, name string) (Role, error) { 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 UPDATE roles SET @@ -59,6 +79,24 @@ func (q *Queries) DeleteRole(ctx context.Context, id pgtype.UUID) error { 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 SELECT id, name, is_deleted, created_at, updated_at FROM roles 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 } -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 UPDATE roles SET diff --git a/internal/gen/sqlc/users.sql.go b/internal/gen/sqlc/users.sql.go index 3d5a9e0..dd744ef 100644 --- a/internal/gen/sqlc/users.sql.go +++ b/internal/gen/sqlc/users.sql.go @@ -542,14 +542,14 @@ func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPassword 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, + display_name = COALESCE($1, display_name), + full_name = COALESCE($2, full_name), + avatar_url = COALESCE($3, avatar_url), + bio = COALESCE($4, bio), + location = COALESCE($5, location), + website = COALESCE($6, website), + country_code = COALESCE($7, country_code), + phone = COALESCE($8, phone), 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 diff --git a/internal/gen/sqlc/verification.sql.go b/internal/gen/sqlc/verification.sql.go index 9c2f3b6..65ec182 100644 --- a/internal/gen/sqlc/verification.sql.go +++ b/internal/gen/sqlc/verification.sql.go @@ -11,27 +11,82 @@ import ( "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 INSERT INTO user_verifications ( - user_id, verify_type + user_id, verify_type, content ) 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 { UserID pgtype.UUID `json:"user_id"` VerifyType int16 `json:"verify_type"` + Content pgtype.Text `json:"content"` } 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 err := row.Scan( &i.ID, &i.UserID, &i.VerifyType, + &i.Content, &i.IsDeleted, &i.Status, &i.ReviewedBy, @@ -44,18 +99,18 @@ func (q *Queries) CreateUserVerification(ctx context.Context, arg CreateUserVeri const createVerificationMedia = `-- name: CreateVerificationMedia :exec INSERT INTO verification_medias ( verification_id, media_id -) VALUES ( - $1, $2 ) +SELECT $1, unnest($2::uuid[]) +ON CONFLICT DO NOTHING ` type CreateVerificationMediaParams struct { - VerificationID pgtype.UUID `json:"verification_id"` - MediaID pgtype.UUID `json:"media_id"` + VerificationID pgtype.UUID `json:"verification_id"` + Column2 []pgtype.UUID `json:"column_2"` } 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 } @@ -85,11 +140,27 @@ func (q *Queries) DeleteVerificationMedia(ctx context.Context, arg DeleteVerific 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 SELECT uv.id, uv.user_id, uv.verify_type, + uv.content, uv.is_deleted, uv.status, uv.reviewed_by, @@ -122,6 +193,7 @@ type GetUserVerificationByIDRow 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"` @@ -137,6 +209,7 @@ func (q *Queries) GetUserVerificationByID(ctx context.Context, id pgtype.UUID) ( &i.ID, &i.UserID, &i.VerifyType, + &i.Content, &i.IsDeleted, &i.Status, &i.ReviewedBy, @@ -152,6 +225,7 @@ SELECT uv.id, uv.user_id, uv.verify_type, + uv.content, uv.is_deleted, uv.status, uv.reviewed_by, @@ -185,6 +259,7 @@ type GetUserVerificationsRow 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"` @@ -206,6 +281,133 @@ func (q *Queries) GetUserVerifications(ctx context.Context, userID pgtype.UUID) &i.ID, &i.UserID, &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.Status, &i.ReviewedBy, diff --git a/internal/models/media.go b/internal/models/media.go index 02e8c93..9ea4d7e 100644 --- a/internal/models/media.go +++ b/internal/models/media.go @@ -16,6 +16,16 @@ type MediaEntity struct { CreatedAt *time.Time `json:"created_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 { ID string `json:"id"` diff --git a/internal/models/verification.go b/internal/models/verification.go new file mode 100644 index 0000000..7cdb141 --- /dev/null +++ b/internal/models/verification.go @@ -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 +} diff --git a/internal/repositories/roleRepository.go b/internal/repositories/roleRepository.go index 539e946..d14b2b6 100644 --- a/internal/repositories/roleRepository.go +++ b/internal/repositories/roleRepository.go @@ -24,10 +24,10 @@ type RoleRepository interface { Update(ctx context.Context, params sqlc.UpdateRoleParams) (*models.RoleEntity, error) Delete(ctx context.Context, id pgtype.UUID) error Restore(ctx context.Context, id pgtype.UUID) error - AddUserRole(ctx context.Context, params sqlc.AddUserRoleParams) error - RemoveUserRole(ctx context.Context, params sqlc.RemoveUserRoleParams) error - RemoveAllRolesFromUser(ctx context.Context, userId pgtype.UUID) error - RemoveAllUsersFromRole(ctx context.Context, roleId pgtype.UUID) error + CreateUserRole(ctx context.Context, params sqlc.CreateUserRoleParams) error + DeleteUserRole(ctx context.Context, params sqlc.DeleteUserRoleParams) error + BulkDeleteRolesFromUser(ctx context.Context, userId pgtype.UUID) error + BulkDeleteUsersFromRole(ctx context.Context, roleId pgtype.UUID) error } type roleRepository struct { @@ -261,22 +261,22 @@ func (r *roleRepository) Restore(ctx context.Context, id pgtype.UUID) error { return nil } -func (r *roleRepository) AddUserRole(ctx context.Context, params sqlc.AddUserRoleParams) error { - err := r.q.AddUserRole(ctx, params) +func (r *roleRepository) CreateUserRole(ctx context.Context, params sqlc.CreateUserRoleParams) error { + err := r.q.CreateUserRole(ctx, params) return err } -func (r *roleRepository) RemoveUserRole(ctx context.Context, params sqlc.RemoveUserRoleParams) error { - err := r.q.RemoveUserRole(ctx, params) +func (r *roleRepository) DeleteUserRole(ctx context.Context, params sqlc.DeleteUserRoleParams) error { + err := r.q.DeleteUserRole(ctx, params) return err } -func (r *roleRepository) RemoveAllUsersFromRole(ctx context.Context, roleId pgtype.UUID) error { - err := r.q.RemoveAllUsersFromRole(ctx, roleId) +func (r *roleRepository) BulkDeleteUsersFromRole(ctx context.Context, roleId pgtype.UUID) error { + err := r.q.BulkDeleteUsersFromRole(ctx, roleId) return err } -func (r *roleRepository) RemoveAllRolesFromUser(ctx context.Context, roleId pgtype.UUID) error { - err := r.q.RemoveAllRolesFromUser(ctx, roleId) +func (r *roleRepository) BulkDeleteRolesFromUser(ctx context.Context, roleId pgtype.UUID) error { + err := r.q.BulkDeleteRolesFromUser(ctx, roleId) return err } diff --git a/internal/repositories/verificationRepository.go b/internal/repositories/verificationRepository.go new file mode 100644 index 0000000..d3aa4ed --- /dev/null +++ b/internal/repositories/verificationRepository.go @@ -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, +// } +// } diff --git a/internal/services/authService.go b/internal/services/authService.go index c81e68a..5fcdff4 100644 --- a/internal/services/authService.go +++ b/internal/services/authService.go @@ -318,9 +318,9 @@ func (a *authService) Signup(ctx context.Context, dto *request.SignUpDto) (*resp return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - err = a.roleRepo.AddUserRole( + err = a.roleRepo.CreateUserRole( ctx, - sqlc.AddUserRoleParams{ + sqlc.CreateUserRoleParams{ UserID: userId, 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()) } - err = a.roleRepo.AddUserRole( + err = a.roleRepo.CreateUserRole( ctx, - sqlc.AddUserRoleParams{ + sqlc.CreateUserRoleParams{ UserID: userId, Column2: []pgtype.UUID{roleId}, }, diff --git a/internal/services/userService.go b/internal/services/userService.go index 89ecf84..f1a4ece 100644 --- a/internal/services/userService.go +++ b/internal/services/userService.go @@ -172,12 +172,12 @@ func (u *userService) ChangeRoleUser(ctx context.Context, claims *response.JWTCl user.Roles = append(user.Roles, role.ToRoleSimple()) } - err = u.roleRepo.RemoveAllRolesFromUser(ctx, userId) + err = u.roleRepo.BulkDeleteRolesFromUser(ctx, userId) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - err = u.roleRepo.AddUserRole(ctx, sqlc.AddUserRoleParams{ + err = u.roleRepo.CreateUserRole(ctx, sqlc.CreateUserRoleParams{ UserID: userId, Column2: roleIdList, }) @@ -239,14 +239,14 @@ func (u *userService) UpdateProfile(ctx context.Context, userId string, dto *req newUser, err := u.userRepo.UpdateProfile( ctx, sqlc.UpdateUserProfileParams{ - DisplayName: pgtype.Text{String: dto.DisplayName, Valid: len(dto.DisplayName) > 0}, - FullName: pgtype.Text{String: dto.FullName, Valid: len(dto.FullName) > 0}, - AvatarUrl: pgtype.Text{String: dto.AvatarUrl, Valid: len(dto.AvatarUrl) > 0}, - Bio: pgtype.Text{String: dto.Bio, Valid: len(dto.Bio) > 0}, - Location: pgtype.Text{String: dto.Location, Valid: len(dto.Location) > 0}, - Website: pgtype.Text{String: dto.Website, Valid: len(dto.Website) > 0}, - CountryCode: pgtype.Text{String: dto.CountryCode, Valid: len(dto.CountryCode) > 0}, - Phone: pgtype.Text{String: dto.Phone, Valid: len(dto.Phone) > 0}, + DisplayName: convert.PtrToText(dto.DisplayName), + FullName: convert.PtrToText(dto.FullName), + AvatarUrl: convert.PtrToText(dto.AvatarUrl), + Bio: convert.PtrToText(dto.Bio), + Location: convert.PtrToText(dto.Location), + Website: convert.PtrToText(dto.Website), + CountryCode: convert.PtrToText(dto.CountryCode), + Phone: convert.PtrToText(dto.Phone), UserID: pgID, }, ) diff --git a/pkg/constants/verify.go b/pkg/constants/verify.go index 23c70fe..8f56ac3 100644 --- a/pkg/constants/verify.go +++ b/pkg/constants/verify.go @@ -3,9 +3,11 @@ package constants type VerifyType int16 const ( + VerifyUnknown VerifyType = 0 VerifyIdCard VerifyType = 1 VerifyEducation VerifyType = 2 VerifyExpert VerifyType = 3 + VerifyOther VerifyType = 4 ) func (t VerifyType) String() string { @@ -16,20 +18,24 @@ func (t VerifyType) String() string { return "EDUCATION" case VerifyExpert: return "EXPERT" + case VerifyOther: + return "OTHER" default: return "UNKNOWN" } } -func ParseVerifyType(v int16) VerifyType { +func ParseVerifyType(v string) VerifyType { switch v { - case 1: + case "ID_CARD": return VerifyIdCard - case 2: + case "EDUCATION": return VerifyEducation - case 3: + case "EXPERT": return VerifyExpert + case "OTHER": + return VerifyOther default: - return 0 + return VerifyUnknown } } diff --git a/pkg/convert/convert.go b/pkg/convert/convert.go index 6841684..52f21ea 100644 --- a/pkg/convert/convert.go +++ b/pkg/convert/convert.go @@ -43,3 +43,13 @@ func TimeToPtr(v pgtype.Timestamptz) *time.Time { t := v.Time return &t } + +func PtrToText(s *string) pgtype.Text { + if s == nil { + return pgtype.Text{Valid: false} + } + return pgtype.Text{ + String: *s, + Valid: true, + } +} diff --git a/pkg/database/seed.go b/pkg/database/seed.go index 0e662f8..6ab22a1 100644 --- a/pkg/database/seed.go +++ b/pkg/database/seed.go @@ -70,16 +70,21 @@ func SeedSuperAdmin(pool *pgxpool.Pool) error { return err } - role, err := q.GetRoleByName(ctx, constants.ADMIN.String()) + adminRole, err := q.GetRoleByName(ctx, constants.ADMIN.String()) if err != nil { return err } - err = q.AddUserRole( + useRole, err := q.GetRoleByName(ctx, constants.USER.String()) + if err != nil { + return err + } + + err = q.CreateUserRole( ctx, - sqlc.AddUserRoleParams{ + sqlc.CreateUserRoleParams{ UserID: user.ID, - Column2: []pgtype.UUID{role.ID}, + Column2: []pgtype.UUID{adminRole.ID, useRole.ID}, }, ) if err != nil {