UPDATE: Historian module
All checks were successful
Build and Release / release (push) Successful in 1m20s

This commit is contained in:
2026-04-12 00:35:14 +07:00
parent af76d2a26a
commit 03415782d1
38 changed files with 2759 additions and 231 deletions

View File

@@ -0,0 +1,71 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Application Approved</title>
</head>
<body
style="
margin: 0;
padding: 0;
background: #f6f9fc;
font-family: Arial, sans-serif;
"
>
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" style="padding: 40px 0">
<table
width="600"
cellpadding="0"
cellspacing="0"
style="background: #ffffff; border-radius: 8px; padding: 30px"
>
<tr>
<td
align="center"
style="font-size: 24px; font-weight: bold; color: #22c55e"
>
🎉 Application Approved
</td>
</tr>
<tr>
<td style="padding: 20px 0; font-size: 16px; color: #333">
Hello <b>{{name}}</b>,
</td>
</tr>
<tr>
<td style="font-size: 15px; color: #555; line-height: 1.6">
Your application to become a <b>Historian</b> has been
<b style="color: #22c55e">approved</b>. <br /><br />
You can now access historian features and start contributing
content.
</td>
</tr>
<tr>
<td align="center" style="padding: 30px 0">
<a
href="{{app_url}}"
style="
background: #22c55e;
color: #fff;
padding: 12px 20px;
border-radius: 6px;
text-decoration: none;
"
>
Go to Dashboard
</a>
</td>
</tr>
<tr>
<td style="font-size: 12px; color: #999; text-align: center">
&copy; 2026 Black Cat Studio. All rights reserved.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Application Rejected</title>
</head>
<body style="margin:0;padding:0;background:#f6f9fc;font-family:Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" style="padding:40px 0;">
<table width="600" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;padding:30px;">
<tr>
<td align="center" style="font-size:24px;font-weight:bold;color:#ef4444;">
❌ Application Rejected
</td>
</tr>
<tr>
<td style="padding:20px 0;font-size:16px;color:#333;">
Hello <b>{{name}}</b>,
</td>
</tr>
<tr>
<td style="font-size:15px;color:#555;line-height:1.6;">
Unfortunately, your application to become a <b>Historian</b> has been
<b style="color:#ef4444;">rejected</b>.
<br/><br/>
<b>Reason:</b><br/>
{{reason}}
<br/><br/>
You can update your information and reapply anytime.
</td>
</tr>
<tr>
<td align="center" style="padding:30px 0;">
<a href="{{app_url}}" style="background:#3b82f6;color:#fff;padding:12px 20px;border-radius:6px;text-decoration:none;">
Reapply
</a>
</td>
</tr>
<tr>
<td style="font-size:12px;color:#999;text-align:center;">
&copy; 2026 Black Cat Studio. All rights reserved.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -77,6 +77,7 @@ func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache.
tileRepo := repositories.NewTileRepository(sqlTile, redis) tileRepo := repositories.NewTileRepository(sqlTile, redis)
tokenRepo := repositories.NewTokenRepository(redis) tokenRepo := repositories.NewTokenRepository(redis)
mediaRepo := repositories.NewMediaRepository(sqlPg, redis) mediaRepo := repositories.NewMediaRepository(sqlPg, redis)
verificationRepo := repositories.NewVerificationRepository(sqlPg, redis)
// service setup // service setup
authService := services.NewAuthService(userRepo, roleRepo, tokenRepo, redis) authService := services.NewAuthService(userRepo, roleRepo, tokenRepo, redis)
@@ -84,19 +85,22 @@ func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache.
roleService := services.NewRoleService(roleRepo) roleService := services.NewRoleService(roleRepo)
tileService := services.NewTileService(tileRepo) tileService := services.NewTileService(tileRepo)
mediaService := services.NewMediaService(mediaRepo, tokenRepo, sclient, redis) mediaService := services.NewMediaService(mediaRepo, tokenRepo, sclient, redis)
verificationService := services.NewVerificationService(verificationRepo, mediaRepo, userRepo, roleRepo, redis)
// controller setup // controller setup
authController := controllers.NewAuthController(authService, oauth) authController := controllers.NewAuthController(authService, oauth)
userController := controllers.NewUserController(userService, mediaService) userController := controllers.NewUserController(userService, mediaService, verificationService)
tileController := controllers.NewTileController(tileService) tileController := controllers.NewTileController(tileService)
roleController := controllers.NewRoleController(roleService) roleController := controllers.NewRoleController(roleService)
mediaController := controllers.NewMediaController(mediaService) mediaController := controllers.NewMediaController(mediaService)
verificationController := controllers.NewVerificationController(verificationService)
// route setup // route setup
routes.AuthRoutes(s.App, authController, userRepo) routes.AuthRoutes(s.App, authController, userRepo)
routes.UserRoutes(s.App, userController, userRepo) routes.UserRoutes(s.App, userController, userRepo)
routes.MediaRoutes(s.App, mediaController, userRepo) routes.MediaRoutes(s.App, mediaController, userRepo)
routes.RoleRoutes(s.App, roleController, userRepo) routes.RoleRoutes(s.App, roleController, userRepo)
routes.VerificationRoutes(s.App, verificationController, userRepo)
routes.TileRoutes(s.App, tileController) routes.TileRoutes(s.App, tileController)
routes.NotFoundRoute(s.App) routes.NotFoundRoute(s.App)
} }

View File

@@ -49,7 +49,7 @@ func runSingleWorker(ctx context.Context, rdb *redis.Client, consumerID int) {
rdb.XAck(ctx, constants.StreamEmailName, constants.GroupEmailName, message.ID) rdb.XAck(ctx, constants.StreamEmailName, constants.GroupEmailName, message.ID)
continue continue
} }
if taskType == constants.TaskTypeSendEmailOTP.String() { if taskType == constants.TaskTypeSendEmailOTP.String() {
var data models.TokenEntity var data models.TokenEntity
if err := json.Unmarshal([]byte(payloadStr), &data); err != nil { if err := json.Unmarshal([]byte(payloadStr), &data); err != nil {
@@ -62,7 +62,26 @@ func runSingleWorker(ctx context.Context, rdb *redis.Client, consumerID int) {
Str("email", data.Email). Str("email", data.Email).
Msg("Processing email task") Msg("Processing email task")
errSend := email.SendMailOTP(data.Email, data.Token, data.TokenType) errSend := email.SendMailOTP(&data)
if errSend != nil {
log.Error().Err(errSend).Str("email", data.Email).Msg("Failed to send email")
continue
}
}
if taskType == constants.TaskTypeNotifyHistorianReview.String() {
var data models.UserVerificationStorageEntity
if err := json.Unmarshal([]byte(payloadStr), &data); err != nil {
log.Error().Err(err).Msg("Failed to unmarshal payload")
continue
}
log.Info().
Str("worker", consumerName).
Str("email", data.Email).
Msg("Processing email task")
errSend := email.SendHistorianReviewMail(&data)
if errSend != nil { if errSend != nil {
log.Error().Err(errSend).Str("email", data.Email).Msg("Failed to send email") log.Error().Err(errSend).Str("email", data.Email).Msg("Failed to send email")
continue continue

View File

@@ -20,7 +20,7 @@ ON user_profiles USING gin (display_name gin_trgm_ops);
CREATE INDEX idx_user_profiles_country CREATE INDEX idx_user_profiles_country
ON user_profiles(country_code); ON user_profiles(country_code);
CREATE UNIQUE INDEX idx_user_profiles_phone CREATE INDEX idx_user_profiles_phone
ON user_profiles(phone); ON user_profiles(phone);
CREATE TRIGGER trigger_user_profiles_updated_at CREATE TRIGGER trigger_user_profiles_updated_at

View File

@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS user_verifications (
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),
review_note TEXT,
reviewed_at TIMESTAMPTZ, reviewed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now() created_at TIMESTAMPTZ DEFAULT now()
); );

View File

@@ -242,8 +242,8 @@ WHERE
) )
) )
AND (sqlc.narg('auth_provider')::text IS NULL OR u.auth_provider = sqlc.narg('auth_provider')::text) AND (sqlc.narg('auth_provider')::text IS NULL OR u.auth_provider = sqlc.narg('auth_provider')::text)
AND (sqlc.narg('created_from')::timestamp IS NULL OR u.created_at >= sqlc.narg('created_from')::timestamp) AND (sqlc.narg('created_from')::timestamptz IS NULL OR u.created_at >= sqlc.narg('created_from')::timestamptz)
AND (sqlc.narg('created_to')::timestamp IS NULL OR u.created_at <= sqlc.narg('created_to')::timestamp) AND (sqlc.narg('created_to')::timestamptz IS NULL OR u.created_at <= sqlc.narg('created_to')::timestamptz)
AND ( AND (
sqlc.narg('search_text')::text IS NULL OR sqlc.narg('search_text')::text IS NULL OR
u.id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' OR u.id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' OR
@@ -289,8 +289,8 @@ WHERE
) )
) )
AND (sqlc.narg('auth_provider')::text IS NULL OR u.auth_provider = sqlc.narg('auth_provider')::text) AND (sqlc.narg('auth_provider')::text IS NULL OR u.auth_provider = sqlc.narg('auth_provider')::text)
AND (sqlc.narg('created_from')::timestamp IS NULL OR u.created_at >= sqlc.narg('created_from')::timestamp) AND (sqlc.narg('created_from')::timestamptz IS NULL OR u.created_at >= sqlc.narg('created_from')::timestamptz)
AND (sqlc.narg('created_to')::timestamp IS NULL OR u.created_at <= sqlc.narg('created_to')::timestamp) AND (sqlc.narg('created_to')::timestamptz IS NULL OR u.created_at <= sqlc.narg('created_to')::timestamptz)
AND ( AND (
sqlc.narg('search_text')::text IS NULL OR sqlc.narg('search_text')::text IS NULL OR
u.id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' OR u.id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' OR

View File

@@ -15,6 +15,7 @@ SELECT
uv.is_deleted, uv.is_deleted,
uv.status, uv.status,
uv.reviewed_by, uv.reviewed_by,
uv.review_note,
uv.reviewed_at, uv.reviewed_at,
uv.created_at, uv.created_at,
( (
@@ -48,7 +49,8 @@ SELECT
uv.is_deleted, uv.is_deleted,
uv.status, uv.status,
uv.reviewed_by, uv.reviewed_by,
uv.reviewed_at, uv.reviewed_at,
uv.review_note,
uv.created_at, uv.created_at,
( (
SELECT COALESCE( SELECT COALESCE(
@@ -77,7 +79,8 @@ ORDER BY uv.created_at DESC;
UPDATE user_verifications UPDATE user_verifications
SET SET
status = $2, status = $2,
reviewed_by = $3, review_note = $3,
reviewed_by = $4,
reviewed_at = now() reviewed_at = now()
WHERE id = $1 AND is_deleted = false; WHERE id = $1 AND is_deleted = false;
@@ -97,13 +100,10 @@ INSERT INTO verification_medias (
SELECT $1, unnest($2::uuid[]) SELECT $1, unnest($2::uuid[])
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;
-- name: BulkDeleteVerificationByMediaId :exec -- name: BulkDeleteVerificationMediaByMediaId :many
DELETE FROM verification_medias DELETE FROM verification_medias
WHERE media_id = $1; WHERE media_id = $1
RETURNING verification_id;
-- name: DeleteVerificationMedias :exec
DELETE FROM verification_medias
WHERE verification_id = $1 AND media_id = ANY($2::uuid[]);
-- name: SearchUserVerifications :many -- name: SearchUserVerifications :many
SELECT SELECT
@@ -113,7 +113,8 @@ SELECT
uv.content, uv.content,
uv.is_deleted, uv.is_deleted,
uv.status, uv.status,
uv.reviewed_by, uv.reviewed_by,
uv.review_note,
uv.reviewed_at, uv.reviewed_at,
uv.created_at, uv.created_at,
( (
@@ -139,11 +140,17 @@ FROM user_verifications uv
WHERE WHERE
uv.is_deleted = false 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('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 (
AND (sqlc.narg('statuses')::text[] IS NULL OR uv.status = ANY(sqlc.narg('statuses')::text[])) sqlc.narg('verify_types')::smallint[] IS NULL
OR uv.verify_type = ANY(sqlc.narg('verify_types')::smallint[])
)
AND (
sqlc.narg('statuses')::smallint[] IS NULL
OR uv.status = ANY(sqlc.narg('statuses')::smallint[])
)
AND (sqlc.narg('reviewed_by')::uuid IS NULL OR uv.reviewed_by = sqlc.narg('reviewed_by')::uuid) 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_from')::timestamptz IS NULL OR uv.created_at >= sqlc.narg('created_from')::timestamptz)
AND (sqlc.narg('created_before')::timestamptz IS NULL OR uv.created_at <= sqlc.narg('created_before')::timestamptz) AND (sqlc.narg('created_to')::timestamptz IS NULL OR uv.created_at <= sqlc.narg('created_to')::timestamptz)
AND ( AND (
sqlc.narg('search_text')::text IS NULL OR sqlc.narg('search_text')::text IS NULL OR
uv.id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' OR uv.id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' OR
@@ -166,11 +173,17 @@ FROM user_verifications uv
WHERE WHERE
uv.is_deleted = false 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('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 (
AND (sqlc.narg('statuses')::text[] IS NULL OR uv.status = ANY(sqlc.narg('statuses')::text[])) sqlc.narg('verify_types')::smallint[] IS NULL
OR uv.verify_type = ANY(sqlc.narg('verify_types')::smallint[])
)
AND (
sqlc.narg('statuses')::smallint[] IS NULL
OR uv.status = ANY(sqlc.narg('statuses')::smallint[])
)
AND (sqlc.narg('reviewed_by')::uuid IS NULL OR uv.reviewed_by = sqlc.narg('reviewed_by')::uuid) 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_from')::timestamptz IS NULL OR uv.created_at >= sqlc.narg('created_from')::timestamptz)
AND (sqlc.narg('created_before')::timestamptz IS NULL OR uv.created_at <= sqlc.narg('created_before')::timestamptz) AND (sqlc.narg('created_to')::timestamptz IS NULL OR uv.created_at <= sqlc.narg('created_to')::timestamptz)
AND ( AND (
sqlc.narg('search_text')::text IS NULL OR sqlc.narg('search_text')::text IS NULL OR
uv.id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' OR uv.id::text ILIKE '%' || sqlc.narg('search_text')::text || '%' OR

View File

@@ -59,6 +59,7 @@ CREATE TABLE IF NOT EXISTS user_verifications (
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),
review_note TEXT,
reviewed_at TIMESTAMPTZ, reviewed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now() created_at TIMESTAMPTZ DEFAULT now()
); );

View File

@@ -399,8 +399,322 @@ const docTemplate = `{
} }
} }
}, },
"/historian/application": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get list of historian applications with filters",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Historian Application"
],
"summary": "Search historian applications",
"parameters": [
{
"type": "string",
"name": "created_from",
"in": "query"
},
{
"type": "string",
"name": "created_to",
"in": "query"
},
{
"maximum": 100,
"minimum": 1,
"type": "integer",
"name": "limit",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"name": "order",
"in": "query"
},
{
"minimum": 1,
"type": "integer",
"name": "page",
"in": "query"
},
{
"type": "string",
"name": "reviewed_by",
"in": "query"
},
{
"maxLength": 200,
"minLength": 2,
"type": "string",
"name": "search",
"in": "query"
},
{
"enum": [
"id",
"created_at",
"reviewed_at",
"status"
],
"type": "string",
"name": "sort",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "csv",
"name": "statuses",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "csv",
"name": "user_ids",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "csv",
"name": "verify_types",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Submit application to become historian",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Historian Application"
],
"summary": "Create historian application",
"parameters": [
{
"description": "Application data",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_request.CreateUserVerificationDto"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/historian/application/{id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get historian application detail",
"produces": [
"application/json"
],
"tags": [
"Historian Application"
],
"summary": "Get application by ID",
"parameters": [
{
"type": "string",
"description": "Verification ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Delete historian application",
"produces": [
"application/json"
],
"tags": [
"Historian Application"
],
"summary": "Delete application",
"parameters": [
{
"type": "string",
"description": "Verification ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/historian/application/{id}/status": {
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Approve or reject historian application",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Historian Application"
],
"summary": "Update application status",
"parameters": [
{
"type": "string",
"description": "Verification ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Status update",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_request.UpdateVerificationStatusDto"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/media": { "/media": {
"get": { "get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Search media with filters, pagination", "description": "Search media with filters, pagination",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -414,21 +728,73 @@ const docTemplate = `{
"summary": "Search media", "summary": "Search media",
"parameters": [ "parameters": [
{ {
"maximum": 100,
"minimum": 1,
"type": "integer", "type": "integer",
"description": "Page number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "Items per page",
"name": "limit", "name": "limit",
"in": "query" "in": "query"
}, },
{ {
"minimum": 0,
"type": "integer",
"name": "max_size",
"in": "query"
},
{
"maxLength": 100,
"type": "string", "type": "string",
"description": "Search keyword", "name": "mime_type",
"name": "keyword", "in": "query"
},
{
"minimum": 0,
"type": "integer",
"name": "min_size",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"name": "order",
"in": "query"
},
{
"minimum": 1,
"type": "integer",
"name": "page",
"in": "query"
},
{
"maxLength": 200,
"minLength": 2,
"type": "string",
"name": "search",
"in": "query"
},
{
"enum": [
"id",
"created_at",
"updated_at",
"size",
"original_name",
"storage_key",
"mime_type"
],
"type": "string",
"name": "sort",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "csv",
"name": "user_ids",
"in": "query" "in": "query"
} }
], ],
@@ -518,15 +884,19 @@ const docTemplate = `{
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "File name", "name": "content_type",
"name": "filename",
"in": "query", "in": "query",
"required": true "required": true
}, },
{ {
"type": "string", "type": "string",
"description": "Content type", "name": "fileName",
"name": "contentType", "in": "query",
"required": true
},
{
"type": "integer",
"name": "size",
"in": "query", "in": "query",
"required": true "required": true
} }
@@ -573,11 +943,13 @@ const docTemplate = `{
"summary": "Confirm presigned upload", "summary": "Confirm presigned upload",
"parameters": [ "parameters": [
{ {
"type": "string", "description": "Request body",
"description": "Storage key", "name": "data",
"name": "key", "in": "body",
"in": "query", "required": true,
"required": true "schema": {
"$ref": "#/definitions/history-api_internal_dtos_request.PreSignedCompleteDto"
}
} }
], ],
"responses": { "responses": {
@@ -653,6 +1025,11 @@ const docTemplate = `{
}, },
"/media/{id}": { "/media/{id}": {
"get": { "get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Retrieve a media file by its ID", "description": "Retrieve a media file by its ID",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -1218,6 +1595,44 @@ const docTemplate = `{
} }
} }
}, },
"/users/{id}/application": {
"get": {
"description": "Retrieve media list by specific user ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get user's media by user ID",
"parameters": [
{
"type": "string",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/users/{id}/media": { "/users/{id}/media": {
"get": { "get": {
"description": "Retrieve media list by specific user ID", "description": "Retrieve media list by specific user ID",
@@ -1480,6 +1895,34 @@ const docTemplate = `{
} }
} }
}, },
"history-api_internal_dtos_request.CreateUserVerificationDto": {
"type": "object",
"required": [
"content",
"verify_type"
],
"properties": {
"content": {
"type": "string",
"minLength": 10
},
"media_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"verify_type": {
"type": "string",
"enum": [
"ID_CARD",
"EDUCATION",
"EXPERT",
"OTHER"
]
}
}
},
"history-api_internal_dtos_request.ForgotPasswordDto": { "history-api_internal_dtos_request.ForgotPasswordDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -1517,6 +1960,17 @@ const docTemplate = `{
} }
} }
}, },
"history-api_internal_dtos_request.PreSignedCompleteDto": {
"type": "object",
"required": [
"token_id"
],
"properties": {
"token_id": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.SignInDto": { "history-api_internal_dtos_request.SignInDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -1602,6 +2056,28 @@ const docTemplate = `{
} }
} }
}, },
"history-api_internal_dtos_request.UpdateVerificationStatusDto": {
"type": "object",
"required": [
"review_note",
"status"
],
"properties": {
"review_note": {
"type": "string",
"maxLength": 3000,
"minLength": 5
},
"status": {
"type": "string",
"enum": [
"PENDING",
"APPROVED",
"REJECTED"
]
}
}
},
"history-api_internal_dtos_request.VerifyTokenDto": { "history-api_internal_dtos_request.VerifyTokenDto": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@@ -392,8 +392,322 @@
} }
} }
}, },
"/historian/application": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get list of historian applications with filters",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Historian Application"
],
"summary": "Search historian applications",
"parameters": [
{
"type": "string",
"name": "created_from",
"in": "query"
},
{
"type": "string",
"name": "created_to",
"in": "query"
},
{
"maximum": 100,
"minimum": 1,
"type": "integer",
"name": "limit",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"name": "order",
"in": "query"
},
{
"minimum": 1,
"type": "integer",
"name": "page",
"in": "query"
},
{
"type": "string",
"name": "reviewed_by",
"in": "query"
},
{
"maxLength": 200,
"minLength": 2,
"type": "string",
"name": "search",
"in": "query"
},
{
"enum": [
"id",
"created_at",
"reviewed_at",
"status"
],
"type": "string",
"name": "sort",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "csv",
"name": "statuses",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "csv",
"name": "user_ids",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "csv",
"name": "verify_types",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Submit application to become historian",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Historian Application"
],
"summary": "Create historian application",
"parameters": [
{
"description": "Application data",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_request.CreateUserVerificationDto"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/historian/application/{id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get historian application detail",
"produces": [
"application/json"
],
"tags": [
"Historian Application"
],
"summary": "Get application by ID",
"parameters": [
{
"type": "string",
"description": "Verification ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Delete historian application",
"produces": [
"application/json"
],
"tags": [
"Historian Application"
],
"summary": "Delete application",
"parameters": [
{
"type": "string",
"description": "Verification ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/historian/application/{id}/status": {
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Approve or reject historian application",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Historian Application"
],
"summary": "Update application status",
"parameters": [
{
"type": "string",
"description": "Verification ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Status update",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_request.UpdateVerificationStatusDto"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/media": { "/media": {
"get": { "get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Search media with filters, pagination", "description": "Search media with filters, pagination",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -407,21 +721,73 @@
"summary": "Search media", "summary": "Search media",
"parameters": [ "parameters": [
{ {
"maximum": 100,
"minimum": 1,
"type": "integer", "type": "integer",
"description": "Page number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "Items per page",
"name": "limit", "name": "limit",
"in": "query" "in": "query"
}, },
{ {
"minimum": 0,
"type": "integer",
"name": "max_size",
"in": "query"
},
{
"maxLength": 100,
"type": "string", "type": "string",
"description": "Search keyword", "name": "mime_type",
"name": "keyword", "in": "query"
},
{
"minimum": 0,
"type": "integer",
"name": "min_size",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"name": "order",
"in": "query"
},
{
"minimum": 1,
"type": "integer",
"name": "page",
"in": "query"
},
{
"maxLength": 200,
"minLength": 2,
"type": "string",
"name": "search",
"in": "query"
},
{
"enum": [
"id",
"created_at",
"updated_at",
"size",
"original_name",
"storage_key",
"mime_type"
],
"type": "string",
"name": "sort",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "csv",
"name": "user_ids",
"in": "query" "in": "query"
} }
], ],
@@ -511,15 +877,19 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "File name", "name": "content_type",
"name": "filename",
"in": "query", "in": "query",
"required": true "required": true
}, },
{ {
"type": "string", "type": "string",
"description": "Content type", "name": "fileName",
"name": "contentType", "in": "query",
"required": true
},
{
"type": "integer",
"name": "size",
"in": "query", "in": "query",
"required": true "required": true
} }
@@ -566,11 +936,13 @@
"summary": "Confirm presigned upload", "summary": "Confirm presigned upload",
"parameters": [ "parameters": [
{ {
"type": "string", "description": "Request body",
"description": "Storage key", "name": "data",
"name": "key", "in": "body",
"in": "query", "required": true,
"required": true "schema": {
"$ref": "#/definitions/history-api_internal_dtos_request.PreSignedCompleteDto"
}
} }
], ],
"responses": { "responses": {
@@ -646,6 +1018,11 @@
}, },
"/media/{id}": { "/media/{id}": {
"get": { "get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Retrieve a media file by its ID", "description": "Retrieve a media file by its ID",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -1211,6 +1588,44 @@
} }
} }
}, },
"/users/{id}/application": {
"get": {
"description": "Retrieve media list by specific user ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get user's media by user ID",
"parameters": [
{
"type": "string",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/users/{id}/media": { "/users/{id}/media": {
"get": { "get": {
"description": "Retrieve media list by specific user ID", "description": "Retrieve media list by specific user ID",
@@ -1473,6 +1888,34 @@
} }
} }
}, },
"history-api_internal_dtos_request.CreateUserVerificationDto": {
"type": "object",
"required": [
"content",
"verify_type"
],
"properties": {
"content": {
"type": "string",
"minLength": 10
},
"media_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"verify_type": {
"type": "string",
"enum": [
"ID_CARD",
"EDUCATION",
"EXPERT",
"OTHER"
]
}
}
},
"history-api_internal_dtos_request.ForgotPasswordDto": { "history-api_internal_dtos_request.ForgotPasswordDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -1510,6 +1953,17 @@
} }
} }
}, },
"history-api_internal_dtos_request.PreSignedCompleteDto": {
"type": "object",
"required": [
"token_id"
],
"properties": {
"token_id": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.SignInDto": { "history-api_internal_dtos_request.SignInDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -1595,6 +2049,28 @@
} }
} }
}, },
"history-api_internal_dtos_request.UpdateVerificationStatusDto": {
"type": "object",
"required": [
"review_note",
"status"
],
"properties": {
"review_note": {
"type": "string",
"maxLength": 3000,
"minLength": 5
},
"status": {
"type": "string",
"enum": [
"PENDING",
"APPROVED",
"REJECTED"
]
}
}
},
"history-api_internal_dtos_request.VerifyTokenDto": { "history-api_internal_dtos_request.VerifyTokenDto": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@@ -43,6 +43,26 @@ definitions:
- email - email
- token_type - token_type
type: object type: object
history-api_internal_dtos_request.CreateUserVerificationDto:
properties:
content:
minLength: 10
type: string
media_ids:
items:
type: string
type: array
verify_type:
enum:
- ID_CARD
- EDUCATION
- EXPERT
- OTHER
type: string
required:
- content
- verify_type
type: object
history-api_internal_dtos_request.ForgotPasswordDto: history-api_internal_dtos_request.ForgotPasswordDto:
properties: properties:
email: email:
@@ -69,6 +89,13 @@ definitions:
required: required:
- media_ids - media_ids
type: object type: object
history-api_internal_dtos_request.PreSignedCompleteDto:
properties:
token_id:
type: string
required:
- token_id
type: object
history-api_internal_dtos_request.SignInDto: history-api_internal_dtos_request.SignInDto:
properties: properties:
email: email:
@@ -132,6 +159,22 @@ definitions:
website: website:
type: string type: string
type: object type: object
history-api_internal_dtos_request.UpdateVerificationStatusDto:
properties:
review_note:
maxLength: 3000
minLength: 5
type: string
status:
enum:
- PENDING
- APPROVED
- REJECTED
type: string
required:
- review_note
- status
type: object
history-api_internal_dtos_request.VerifyTokenDto: history-api_internal_dtos_request.VerifyTokenDto:
properties: properties:
email: email:
@@ -451,6 +494,204 @@ paths:
summary: Verify a security token summary: Verify a security token
tags: tags:
- Auth - Auth
/historian/application:
get:
consumes:
- application/json
description: Get list of historian applications with filters
parameters:
- in: query
name: created_from
type: string
- in: query
name: created_to
type: string
- in: query
maximum: 100
minimum: 1
name: limit
type: integer
- enum:
- asc
- desc
in: query
name: order
type: string
- in: query
minimum: 1
name: page
type: integer
- in: query
name: reviewed_by
type: string
- in: query
maxLength: 200
minLength: 2
name: search
type: string
- enum:
- id
- created_at
- reviewed_at
- status
in: query
name: sort
type: string
- collectionFormat: csv
in: query
items:
type: string
name: statuses
type: array
- collectionFormat: csv
in: query
items:
type: string
name: user_ids
type: array
- collectionFormat: csv
in: query
items:
type: string
name: verify_types
type: array
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
security:
- BearerAuth: []
summary: Search historian applications
tags:
- Historian Application
post:
consumes:
- application/json
description: Submit application to become historian
parameters:
- description: Application data
in: body
name: body
required: true
schema:
$ref: '#/definitions/history-api_internal_dtos_request.CreateUserVerificationDto'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
security:
- BearerAuth: []
summary: Create historian application
tags:
- Historian Application
/historian/application/{id}:
delete:
description: Delete historian application
parameters:
- description: Verification ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
security:
- BearerAuth: []
summary: Delete application
tags:
- Historian Application
get:
description: Get historian application detail
parameters:
- description: Verification ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
security:
- BearerAuth: []
summary: Get application by ID
tags:
- Historian Application
/historian/application/{id}/status:
put:
consumes:
- application/json
description: Approve or reject historian application
parameters:
- description: Verification ID
in: path
name: id
required: true
type: string
- description: Status update
in: body
name: body
required: true
schema:
$ref: '#/definitions/history-api_internal_dtos_request.UpdateVerificationStatusDto'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
security:
- BearerAuth: []
summary: Update application status
tags:
- Historian Application
/media: /media:
delete: delete:
consumes: consumes:
@@ -484,18 +725,55 @@ paths:
- application/json - application/json
description: Search media with filters, pagination description: Search media with filters, pagination
parameters: parameters:
- description: Page number - in: query
in: query maximum: 100
name: page minimum: 1
type: integer
- description: Items per page
in: query
name: limit name: limit
type: integer type: integer
- description: Search keyword - in: query
in: query minimum: 0
name: keyword name: max_size
type: integer
- in: query
maxLength: 100
name: mime_type
type: string type: string
- in: query
minimum: 0
name: min_size
type: integer
- enum:
- asc
- desc
in: query
name: order
type: string
- in: query
minimum: 1
name: page
type: integer
- in: query
maxLength: 200
minLength: 2
name: search
type: string
- enum:
- id
- created_at
- updated_at
- size
- original_name
- storage_key
- mime_type
in: query
name: sort
type: string
- collectionFormat: csv
in: query
items:
type: string
name: user_ids
type: array
produces: produces:
- application/json - application/json
responses: responses:
@@ -511,6 +789,8 @@ paths:
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
security:
- BearerAuth: []
summary: Search media summary: Search media
tags: tags:
- Media - Media
@@ -562,6 +842,8 @@ paths:
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
security:
- BearerAuth: []
summary: Get media by ID summary: Get media by ID
tags: tags:
- Media - Media
@@ -571,16 +853,18 @@ paths:
- application/json - application/json
description: Generate a presigned URL for direct upload to storage description: Generate a presigned URL for direct upload to storage
parameters: parameters:
- description: File name - in: query
in: query name: content_type
name: filename
required: true required: true
type: string type: string
- description: Content type - in: query
in: query name: fileName
name: contentType
required: true required: true
type: string type: string
- in: query
name: size
required: true
type: integer
produces: produces:
- application/json - application/json
responses: responses:
@@ -607,11 +891,12 @@ paths:
- application/json - application/json
description: Confirm that upload via presigned URL is completed description: Confirm that upload via presigned URL is completed
parameters: parameters:
- description: Storage key - description: Request body
in: query in: body
name: key name: data
required: true required: true
type: string schema:
$ref: '#/definitions/history-api_internal_dtos_request.PreSignedCompleteDto'
produces: produces:
- application/json - application/json
responses: responses:
@@ -935,6 +1220,31 @@ paths:
summary: Update user profile summary: Update user profile
tags: tags:
- Users - Users
/users/{id}/application:
get:
consumes:
- application/json
description: Retrieve media list by specific user ID
parameters:
- description: User ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
summary: Get user's media by user ID
tags:
- Users
/users/{id}/media: /users/{id}/media:
get: get:
consumes: consumes:

View File

@@ -8,6 +8,7 @@ import (
"history-api/internal/dtos/response" "history-api/internal/dtos/response"
"history-api/internal/models" "history-api/internal/models"
"history-api/internal/services" "history-api/internal/services"
"history-api/pkg/config"
"history-api/pkg/validator" "history-api/pkg/validator"
"strings" "strings"
"time" "time"
@@ -137,7 +138,6 @@ func (h *AuthController) Signup(c fiber.Ctx) error {
}) })
} }
func (h *AuthController) getRefreshToken(c fiber.Ctx) string { func (h *AuthController) getRefreshToken(c fiber.Ctx) string {
auth := c.Get("Authorization") auth := c.Get("Authorization")
if auth != "" { if auth != "" {
@@ -326,9 +326,10 @@ func (h *AuthController) GoogleLogin(c fiber.Ctx) error {
state := uuid.New().String() state := uuid.New().String()
feUrl := config.GetConfigWithDefault("FRONTEND_URL", "http://localhost:3000")
redirect := c.Query("redirect") redirect := c.Query("redirect")
if redirect == "" { if redirect == "" {
redirect = "http://localhost:3000" redirect = feUrl
} }
data := models.OAuthState{ data := models.OAuthState{
@@ -443,10 +444,10 @@ func (h *AuthController) GoogleCallback(c fiber.Ctx) error {
"https://localhost:3001": true, "https://localhost:3001": true,
"http://localhost:5500": true, "http://localhost:5500": true,
} }
feUrl := config.GetConfigWithDefault("FRONTEND_URL", "http://localhost:3000")
redirectURL := data.RedirectURL redirectURL := data.RedirectURL
if !allowed[redirectURL] { if !allowed[redirectURL] {
redirectURL = "http://localhost:3000" redirectURL = feUrl
} }
return c.Redirect().To(redirectURL) return c.Redirect().To(redirectURL)

View File

@@ -25,6 +25,7 @@ func NewMediaController(svc services.MediaService) *MediaController {
// @Tags Media // @Tags Media
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security BearerAuth
// @Param id path string true "Media ID" // @Param id path string true "Media ID"
// @Success 200 {object} response.CommonResponse // @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse // @Failure 500 {object} response.CommonResponse
@@ -52,9 +53,8 @@ func (m *MediaController) GetMediaByID(c fiber.Ctx) error {
// @Tags Media // @Tags Media
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param page query int false "Page number" // @Security BearerAuth
// @Param limit query int false "Items per page" // @Param query query request.SearchMediaDto false "Search Query"
// @Param keyword query string false "Search keyword"
// @Success 200 {object} response.PaginatedResponse // @Success 200 {object} response.PaginatedResponse
// @Failure 400 {object} response.CommonResponse // @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse // @Failure 500 {object} response.CommonResponse
@@ -220,9 +220,7 @@ func (m *MediaController) UploadServerSide(c fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security BearerAuth // @Security BearerAuth
// @Param fileName query string true "File name" // @Param query query request.PreSignedDto false "PreSigned"
// @Param content_type query string true "Content type"
// @Param size query int true "File size"
// @Success 200 {object} response.CommonResponse // @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse // @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse // @Failure 500 {object} response.CommonResponse
@@ -255,7 +253,7 @@ func (m *MediaController) GeneratePresignedURL(c fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security BearerAuth // @Security BearerAuth
// @Param data body PreSignedCompleteDto true "Request body" // @Param data body request.PreSignedCompleteDto true "Request body"
// @Success 200 {object} response.CommonResponse // @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse // @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse // @Failure 500 {object} response.CommonResponse

View File

@@ -14,12 +14,18 @@ import (
type UserController struct { type UserController struct {
service services.UserService service services.UserService
mediaService services.MediaService mediaService services.MediaService
verificationService services.VerificationService
} }
func NewUserController(svc services.UserService, mediaSvc services.MediaService) *UserController { func NewUserController(
svc services.UserService,
mediaSvc services.MediaService,
verificationSvc services.VerificationService,
) *UserController {
return &UserController{ return &UserController{
service: svc, service: svc,
mediaService: mediaSvc, mediaService: mediaSvc,
verificationService: verificationSvc,
} }
} }
@@ -106,6 +112,33 @@ func (h *UserController) GetMediaByUserID(c fiber.Ctx) error {
}) })
} }
// GetApplicationUserID godoc
// @Summary Get user's media by user ID
// @Description Retrieve media list by specific user ID
// @Tags Users
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /users/{id}/application [get]
func (h *UserController) GetVerificationByUserID(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
userId := c.Params("id")
res, err := h.verificationService.GetVerificationByUserID(ctx, userId)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// UpdateProfile godoc // UpdateProfile godoc
// @Summary Update user profile // @Summary Update user profile
// @Description Update the profile details of the currently authenticated user // @Description Update the profile details of the currently authenticated user

View File

@@ -0,0 +1,186 @@
package controllers
import (
"context"
"history-api/internal/dtos/response"
"history-api/internal/dtos/request"
"history-api/internal/services"
"history-api/pkg/validator"
"time"
"github.com/gofiber/fiber/v3"
)
type VerificationController struct {
service services.VerificationService
}
func NewVerificationController(svc services.VerificationService) *VerificationController {
return &VerificationController{service: svc}
}
// @Summary Get application by ID
// @Description Get historian application detail
// @Tags Historian Application
// @Produce json
// @Param id path string true "Verification ID"
// @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Security BearerAuth
// @Router /historian/application/{id} [get]
func (m *VerificationController) GetVerificationByID(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
verificationId := c.Params("id")
res, err := m.service.GetVerificationByID(ctx, verificationId)
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,
})
}
// @Summary Search historian applications
// @Description Get list of historian applications with filters
// @Tags Historian Application
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param query query request.SearchUserVerificationDto false "Search Query"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /historian/application [get]
func (m *VerificationController) SearchVerification(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dto := &request.SearchUserVerificationDto{}
if err := validator.ValidateQueryDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
res, err := m.service.SearchVerification(ctx, dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(res)
}
// @Summary Delete application
// @Description Delete historian application
// @Tags Historian Application
// @Produce json
// @Param id path string true "Verification ID"
// @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Security BearerAuth
// @Router /historian/application/{id} [delete]
func (m *VerificationController) DeleteVerification(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
claimsVal := c.Locals("user_claims")
if claimsVal == nil {
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
Status: false,
Message: "Unauthorized",
})
}
claims, ok := claimsVal.(*response.JWTClaims)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
Status: false,
Message: "Invalid user claims",
})
}
verificationId := c.Params("id")
err := m.service.DeleteVerification(ctx, claims, verificationId)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Message: "Verification deleted successfully",
})
}
// @Summary Create historian application
// @Description Submit application to become historian
// @Tags Historian Application
// @Accept json
// @Produce json
// @Param body body request.CreateUserVerificationDto true "Application data"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Security BearerAuth
// @Router /historian/application [post]
func (m *VerificationController) CreateVerification(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dto := &request.CreateUserVerificationDto{}
if err := validator.ValidateBodyDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
res, err := m.service.CreateVerification(ctx, c.Locals("uid").(string), dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(res)
}
// @Summary Update application status
// @Description Approve or reject historian application
// @Tags Historian Application
// @Accept json
// @Produce json
// @Param id path string true "Verification ID"
// @Param body body request.UpdateVerificationStatusDto true "Status update"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Security BearerAuth
// @Router /historian/application/{id}/status [put]
func (m *VerificationController) UpdateVerificationStatus(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dto := &request.UpdateVerificationStatusDto{}
if err := validator.ValidateBodyDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
verificationId := c.Params("id")
res, err := m.service.UpdateStatusVerification(ctx, c.Locals("uid").(string), verificationId, dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(res)
}

View File

@@ -21,5 +21,5 @@ type SearchMediaDto struct {
} }
type MediaBulkDeleteDto struct { type MediaBulkDeleteDto struct {
MediaIDs []string `json:"media_ids" validate:"required,dive,uuid"` MediaIDs []string `json:"media_ids" validate:"required,dive,uuid"`
} }

View File

@@ -1,25 +1,31 @@
package request package request
import "time" import (
"time"
)
// SearchUserVerificationDto swagger model
type SearchUserVerificationDto struct { type SearchUserVerificationDto struct {
PaginationDto PaginationDto
Sort string `json:"sort" query:"sort" validate:"omitempty,oneof=id created_at reviewed_at status"` 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"` Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"`
UserIDs []string `json:"user_ids" query:"user_ids" validate:"omitempty,dive,uuid"` UserIDs []string `json:"user_ids" query:"user_ids" validate:"omitempty,dive,uuid"`
VerifyTypes []string `json:"verify_types" query:"verify_types" validate:"omitempty,dive,ascii"` VerifyTypes []string `json:"verify_types" query:"verify_types" validate:"omitempty,dive,oneof=ID_CARD EDUCATION EXPERT OTHER"`
Statuses []string `json:"statuses" query:"statuses" validate:"omitempty,dive,ascii"` Statuses []string `json:"statuses" query:"statuses" validate:"omitempty,dive,oneof=PENDING APPROVED REJECTED"`
ReviewedBy *string `json:"reviewed_by" query:"reviewed_by" validate:"omitempty,uuid"` ReviewedBy *string `json:"reviewed_by" query:"reviewed_by" validate:"omitempty,uuid"`
CreatedAfter *time.Time `json:"created_after" query:"created_after" validate:"omitempty"` CreatedFrom *time.Time `json:"created_from" query:"created_from"`
CreatedBefore *time.Time `json:"created_before" query:"created_before" validate:"omitempty,gtfield=CreatedAfter"` CreatedTo *time.Time `json:"created_to" query:"created_to"`
} }
// CreateUserVerificationDto swagger model
type CreateUserVerificationDto struct { type CreateUserVerificationDto struct {
VerifyType string `json:"verify_type" validate:"required,oneof=ID_CARD EDUCATION EXPERT OTHER"` VerifyType string `json:"verify_type" validate:"required,oneof=ID_CARD EDUCATION EXPERT OTHER"`
Content string `json:"content" validate:"required"` Content string `json:"content" validate:"required,min=10"`
MediaIDs []string `json:"media_ids" validate:"omitempty,dive,uuid"` MediaIDs []string `json:"media_ids" validate:"omitempty,dive,uuid"`
} }
// UpdateVerificationStatusDto swagger model
type UpdateVerificationStatusDto struct { type UpdateVerificationStatusDto struct {
Status string `json:"status" validate:"required,oneof=PENDING APPROVED REJECTED"` Status string `json:"status" validate:"required,oneof=PENDING APPROVED REJECTED"`
ReviewNote string `json:"review_note" validate:"required,min=5,max=3000"`
} }

View File

@@ -28,5 +28,5 @@ type MediaSimpleResponse struct {
MimeType string `json:"mime_type"` MimeType string `json:"mime_type"`
Size int64 `json:"size"` Size int64 `json:"size"`
FileMetadata []byte `json:"file_metadata"` FileMetadata []byte `json:"file_metadata"`
CreatedAt time.Time `json:"created_at"` CreatedAt *time.Time `json:"created_at"`
} }

View File

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

View File

@@ -68,6 +68,7 @@ type UserVerification struct {
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"`
ReviewNote pgtype.Text `json:"review_note"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"` ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
} }

View File

@@ -25,8 +25,8 @@ WHERE
) )
) )
AND ($3::text IS NULL OR u.auth_provider = $3::text) AND ($3::text IS NULL OR u.auth_provider = $3::text)
AND ($4::timestamp IS NULL OR u.created_at >= $4::timestamp) AND ($4::timestamptz IS NULL OR u.created_at >= $4::timestamptz)
AND ($5::timestamp IS NULL OR u.created_at <= $5::timestamp) AND ($5::timestamptz IS NULL OR u.created_at <= $5::timestamptz)
AND ( AND (
$6::text IS NULL OR $6::text IS NULL OR
u.id::text ILIKE '%' || $6::text || '%' OR u.id::text ILIKE '%' || $6::text || '%' OR
@@ -43,12 +43,12 @@ WHERE
` `
type CountUsersParams struct { type CountUsersParams struct {
IsDeleted pgtype.Bool `json:"is_deleted"` IsDeleted pgtype.Bool `json:"is_deleted"`
RoleIds []pgtype.UUID `json:"role_ids"` RoleIds []pgtype.UUID `json:"role_ids"`
AuthProvider pgtype.Text `json:"auth_provider"` AuthProvider pgtype.Text `json:"auth_provider"`
CreatedFrom pgtype.Timestamp `json:"created_from"` CreatedFrom pgtype.Timestamptz `json:"created_from"`
CreatedTo pgtype.Timestamp `json:"created_to"` CreatedTo pgtype.Timestamptz `json:"created_to"`
SearchText pgtype.Text `json:"search_text"` SearchText pgtype.Text `json:"search_text"`
} }
func (q *Queries) CountUsers(ctx context.Context, arg CountUsersParams) (int64, error) { func (q *Queries) CountUsers(ctx context.Context, arg CountUsersParams) (int64, error) {
@@ -399,8 +399,8 @@ WHERE
) )
) )
AND ($3::text IS NULL OR u.auth_provider = $3::text) AND ($3::text IS NULL OR u.auth_provider = $3::text)
AND ($4::timestamp IS NULL OR u.created_at >= $4::timestamp) AND ($4::timestamptz IS NULL OR u.created_at >= $4::timestamptz)
AND ($5::timestamp IS NULL OR u.created_at <= $5::timestamp) AND ($5::timestamptz IS NULL OR u.created_at <= $5::timestamptz)
AND ( AND (
$6::text IS NULL OR $6::text IS NULL OR
u.id::text ILIKE '%' || $6::text || '%' OR u.id::text ILIKE '%' || $6::text || '%' OR
@@ -434,16 +434,16 @@ OFFSET $9
` `
type SearchUsersParams struct { type SearchUsersParams struct {
IsDeleted pgtype.Bool `json:"is_deleted"` IsDeleted pgtype.Bool `json:"is_deleted"`
RoleIds []pgtype.UUID `json:"role_ids"` RoleIds []pgtype.UUID `json:"role_ids"`
AuthProvider pgtype.Text `json:"auth_provider"` AuthProvider pgtype.Text `json:"auth_provider"`
CreatedFrom pgtype.Timestamp `json:"created_from"` CreatedFrom pgtype.Timestamptz `json:"created_from"`
CreatedTo pgtype.Timestamp `json:"created_to"` CreatedTo pgtype.Timestamptz `json:"created_to"`
SearchText pgtype.Text `json:"search_text"` SearchText pgtype.Text `json:"search_text"`
Sort interface{} `json:"sort"` Sort interface{} `json:"sort"`
Order interface{} `json:"order"` Order interface{} `json:"order"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
} }
type SearchUsersRow struct { type SearchUsersRow struct {

View File

@@ -11,14 +11,30 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
const bulkDeleteVerificationByMediaId = `-- name: BulkDeleteVerificationByMediaId :exec const bulkDeleteVerificationMediaByMediaId = `-- name: BulkDeleteVerificationMediaByMediaId :many
DELETE FROM verification_medias DELETE FROM verification_medias
WHERE media_id = $1 WHERE media_id = $1
RETURNING verification_id
` `
func (q *Queries) BulkDeleteVerificationByMediaId(ctx context.Context, mediaID pgtype.UUID) error { func (q *Queries) BulkDeleteVerificationMediaByMediaId(ctx context.Context, mediaID pgtype.UUID) ([]pgtype.UUID, error) {
_, err := q.db.Exec(ctx, bulkDeleteVerificationByMediaId, mediaID) rows, err := q.db.Query(ctx, bulkDeleteVerificationMediaByMediaId, mediaID)
return err if err != nil {
return nil, err
}
defer rows.Close()
items := []pgtype.UUID{}
for rows.Next() {
var verification_id pgtype.UUID
if err := rows.Scan(&verification_id); err != nil {
return nil, err
}
items = append(items, verification_id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
} }
const countUserVerifications = `-- name: CountUserVerifications :one const countUserVerifications = `-- name: CountUserVerifications :one
@@ -27,8 +43,14 @@ FROM user_verifications uv
WHERE WHERE
uv.is_deleted = false uv.is_deleted = false
AND ($1::uuid[] IS NULL OR uv.user_id = ANY($1::uuid[])) 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 (
AND ($3::text[] IS NULL OR uv.status = ANY($3::text[])) $2::smallint[] IS NULL
OR uv.verify_type = ANY($2::smallint[])
)
AND (
$3::smallint[] IS NULL
OR uv.status = ANY($3::smallint[])
)
AND ($4::uuid IS NULL OR uv.reviewed_by = $4::uuid) AND ($4::uuid IS NULL OR uv.reviewed_by = $4::uuid)
AND ($5::timestamptz IS NULL OR uv.created_at >= $5::timestamptz) AND ($5::timestamptz IS NULL OR uv.created_at >= $5::timestamptz)
AND ($6::timestamptz IS NULL OR uv.created_at <= $6::timestamptz) AND ($6::timestamptz IS NULL OR uv.created_at <= $6::timestamptz)
@@ -40,13 +62,13 @@ WHERE
` `
type CountUserVerificationsParams struct { type CountUserVerificationsParams struct {
UserIds []pgtype.UUID `json:"user_ids"` UserIds []pgtype.UUID `json:"user_ids"`
VerifyTypes []string `json:"verify_types"` VerifyTypes []int16 `json:"verify_types"`
Statuses []string `json:"statuses"` Statuses []int16 `json:"statuses"`
ReviewedBy pgtype.UUID `json:"reviewed_by"` ReviewedBy pgtype.UUID `json:"reviewed_by"`
CreatedAfter pgtype.Timestamptz `json:"created_after"` CreatedFrom pgtype.Timestamptz `json:"created_from"`
CreatedBefore pgtype.Timestamptz `json:"created_before"` CreatedTo pgtype.Timestamptz `json:"created_to"`
SearchText pgtype.Text `json:"search_text"` SearchText pgtype.Text `json:"search_text"`
} }
func (q *Queries) CountUserVerifications(ctx context.Context, arg CountUserVerificationsParams) (int64, error) { func (q *Queries) CountUserVerifications(ctx context.Context, arg CountUserVerificationsParams) (int64, error) {
@@ -55,8 +77,8 @@ func (q *Queries) CountUserVerifications(ctx context.Context, arg CountUserVerif
arg.VerifyTypes, arg.VerifyTypes,
arg.Statuses, arg.Statuses,
arg.ReviewedBy, arg.ReviewedBy,
arg.CreatedAfter, arg.CreatedFrom,
arg.CreatedBefore, arg.CreatedTo,
arg.SearchText, arg.SearchText,
) )
var count int64 var count int64
@@ -70,7 +92,7 @@ INSERT INTO user_verifications (
) VALUES ( ) VALUES (
$1, $2, $3 $1, $2, $3
) )
RETURNING id, user_id, verify_type, content, is_deleted, status, reviewed_by, reviewed_at, created_at RETURNING id, user_id, verify_type, content, is_deleted, status, reviewed_by, review_note, reviewed_at, created_at
` `
type CreateUserVerificationParams struct { type CreateUserVerificationParams struct {
@@ -90,6 +112,7 @@ func (q *Queries) CreateUserVerification(ctx context.Context, arg CreateUserVeri
&i.IsDeleted, &i.IsDeleted,
&i.Status, &i.Status,
&i.ReviewedBy, &i.ReviewedBy,
&i.ReviewNote,
&i.ReviewedAt, &i.ReviewedAt,
&i.CreatedAt, &i.CreatedAt,
) )
@@ -140,21 +163,6 @@ 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,
@@ -164,6 +172,7 @@ SELECT
uv.is_deleted, uv.is_deleted,
uv.status, uv.status,
uv.reviewed_by, uv.reviewed_by,
uv.review_note,
uv.reviewed_at, uv.reviewed_at,
uv.created_at, uv.created_at,
( (
@@ -197,6 +206,7 @@ type GetUserVerificationByIDRow struct {
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"`
ReviewNote pgtype.Text `json:"review_note"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"` ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
Medias []byte `json:"medias"` Medias []byte `json:"medias"`
@@ -213,6 +223,7 @@ func (q *Queries) GetUserVerificationByID(ctx context.Context, id pgtype.UUID) (
&i.IsDeleted, &i.IsDeleted,
&i.Status, &i.Status,
&i.ReviewedBy, &i.ReviewedBy,
&i.ReviewNote,
&i.ReviewedAt, &i.ReviewedAt,
&i.CreatedAt, &i.CreatedAt,
&i.Medias, &i.Medias,
@@ -229,7 +240,8 @@ SELECT
uv.is_deleted, uv.is_deleted,
uv.status, uv.status,
uv.reviewed_by, uv.reviewed_by,
uv.reviewed_at, uv.reviewed_at,
uv.review_note,
uv.created_at, uv.created_at,
( (
SELECT COALESCE( SELECT COALESCE(
@@ -264,6 +276,7 @@ type GetUserVerificationsRow struct {
Status int16 `json:"status"` Status int16 `json:"status"`
ReviewedBy pgtype.UUID `json:"reviewed_by"` ReviewedBy pgtype.UUID `json:"reviewed_by"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"` ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
ReviewNote pgtype.Text `json:"review_note"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
Medias []byte `json:"medias"` Medias []byte `json:"medias"`
} }
@@ -286,6 +299,7 @@ func (q *Queries) GetUserVerifications(ctx context.Context, userID pgtype.UUID)
&i.Status, &i.Status,
&i.ReviewedBy, &i.ReviewedBy,
&i.ReviewedAt, &i.ReviewedAt,
&i.ReviewNote,
&i.CreatedAt, &i.CreatedAt,
&i.Medias, &i.Medias,
); err != nil { ); err != nil {
@@ -307,7 +321,8 @@ SELECT
uv.content, uv.content,
uv.is_deleted, uv.is_deleted,
uv.status, uv.status,
uv.reviewed_by, uv.reviewed_by,
uv.review_note,
uv.reviewed_at, uv.reviewed_at,
uv.created_at, uv.created_at,
( (
@@ -333,8 +348,14 @@ FROM user_verifications uv
WHERE WHERE
uv.is_deleted = false uv.is_deleted = false
AND ($1::uuid[] IS NULL OR uv.user_id = ANY($1::uuid[])) 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 (
AND ($3::text[] IS NULL OR uv.status = ANY($3::text[])) $2::smallint[] IS NULL
OR uv.verify_type = ANY($2::smallint[])
)
AND (
$3::smallint[] IS NULL
OR uv.status = ANY($3::smallint[])
)
AND ($4::uuid IS NULL OR uv.reviewed_by = $4::uuid) AND ($4::uuid IS NULL OR uv.reviewed_by = $4::uuid)
AND ($5::timestamptz IS NULL OR uv.created_at >= $5::timestamptz) AND ($5::timestamptz IS NULL OR uv.created_at >= $5::timestamptz)
AND ($6::timestamptz IS NULL OR uv.created_at <= $6::timestamptz) AND ($6::timestamptz IS NULL OR uv.created_at <= $6::timestamptz)
@@ -356,17 +377,17 @@ OFFSET $10
` `
type SearchUserVerificationsParams struct { type SearchUserVerificationsParams struct {
UserIds []pgtype.UUID `json:"user_ids"` UserIds []pgtype.UUID `json:"user_ids"`
VerifyTypes []string `json:"verify_types"` VerifyTypes []int16 `json:"verify_types"`
Statuses []string `json:"statuses"` Statuses []int16 `json:"statuses"`
ReviewedBy pgtype.UUID `json:"reviewed_by"` ReviewedBy pgtype.UUID `json:"reviewed_by"`
CreatedAfter pgtype.Timestamptz `json:"created_after"` CreatedFrom pgtype.Timestamptz `json:"created_from"`
CreatedBefore pgtype.Timestamptz `json:"created_before"` CreatedTo pgtype.Timestamptz `json:"created_to"`
SearchText pgtype.Text `json:"search_text"` SearchText pgtype.Text `json:"search_text"`
Sort interface{} `json:"sort"` Sort interface{} `json:"sort"`
Order interface{} `json:"order"` Order interface{} `json:"order"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
} }
type SearchUserVerificationsRow struct { type SearchUserVerificationsRow struct {
@@ -377,6 +398,7 @@ type SearchUserVerificationsRow struct {
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"`
ReviewNote pgtype.Text `json:"review_note"`
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"` ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
Medias []byte `json:"medias"` Medias []byte `json:"medias"`
@@ -388,8 +410,8 @@ func (q *Queries) SearchUserVerifications(ctx context.Context, arg SearchUserVer
arg.VerifyTypes, arg.VerifyTypes,
arg.Statuses, arg.Statuses,
arg.ReviewedBy, arg.ReviewedBy,
arg.CreatedAfter, arg.CreatedFrom,
arg.CreatedBefore, arg.CreatedTo,
arg.SearchText, arg.SearchText,
arg.Sort, arg.Sort,
arg.Order, arg.Order,
@@ -411,6 +433,7 @@ func (q *Queries) SearchUserVerifications(ctx context.Context, arg SearchUserVer
&i.IsDeleted, &i.IsDeleted,
&i.Status, &i.Status,
&i.ReviewedBy, &i.ReviewedBy,
&i.ReviewNote,
&i.ReviewedAt, &i.ReviewedAt,
&i.CreatedAt, &i.CreatedAt,
&i.Medias, &i.Medias,
@@ -429,7 +452,8 @@ const updateUserVerificationStatus = `-- name: UpdateUserVerificationStatus :exe
UPDATE user_verifications UPDATE user_verifications
SET SET
status = $2, status = $2,
reviewed_by = $3, review_note = $3,
reviewed_by = $4,
reviewed_at = now() reviewed_at = now()
WHERE id = $1 AND is_deleted = false WHERE id = $1 AND is_deleted = false
` `
@@ -437,10 +461,16 @@ WHERE id = $1 AND is_deleted = false
type UpdateUserVerificationStatusParams struct { type UpdateUserVerificationStatusParams struct {
ID pgtype.UUID `json:"id"` ID pgtype.UUID `json:"id"`
Status int16 `json:"status"` Status int16 `json:"status"`
ReviewNote pgtype.Text `json:"review_note"`
ReviewedBy pgtype.UUID `json:"reviewed_by"` ReviewedBy pgtype.UUID `json:"reviewed_by"`
} }
func (q *Queries) UpdateUserVerificationStatus(ctx context.Context, arg UpdateUserVerificationStatusParams) error { func (q *Queries) UpdateUserVerificationStatus(ctx context.Context, arg UpdateUserVerificationStatusParams) error {
_, err := q.db.Exec(ctx, updateUserVerificationStatus, arg.ID, arg.Status, arg.ReviewedBy) _, err := q.db.Exec(ctx, updateUserVerificationStatus,
arg.ID,
arg.Status,
arg.ReviewNote,
arg.ReviewedBy,
)
return err return err
} }

View File

@@ -60,3 +60,24 @@ func RequireAllRoles(required ...constants.Role) fiber.Handler {
return c.Next() return c.Next()
} }
} }
func ForbidRoles(forbidden ...constants.Role) fiber.Handler {
return func(c fiber.Ctx) error {
userRoles, err := getRoles(c)
if err != nil {
return err
}
if len(userRoles) == 0 {
return c.Next()
}
for _, ur := range userRoles {
if slices.Contains(forbidden, ur) {
return fiber.ErrForbidden
}
}
return c.Next()
}
}

View File

@@ -23,7 +23,7 @@ type MediaSimpleEntity struct {
MimeType string `json:"mime_type"` MimeType string `json:"mime_type"`
Size int64 `json:"size"` Size int64 `json:"size"`
FileMetadata []byte `json:"file_metadata"` FileMetadata []byte `json:"file_metadata"`
CreatedAt time.Time `json:"created_at"` CreatedAt *time.Time `json:"created_at"`
} }
@@ -53,6 +53,18 @@ func (e *MediaEntity) ToResponse() *response.MediaResponse {
} }
} }
func (e *MediaEntity) ToSimpleEntity() *MediaSimpleEntity {
return &MediaSimpleEntity{
ID: e.ID,
StorageKey: e.StorageKey,
OriginalName: e.OriginalName,
MimeType: e.MimeType,
Size: e.Size,
FileMetadata: e.FileMetadata,
CreatedAt: e.CreatedAt,
}
}
func MediaEntitiesToResponse(entities []*MediaEntity) []*response.MediaResponse { func MediaEntitiesToResponse(entities []*MediaEntity) []*response.MediaResponse {
responses := make([]*response.MediaResponse, len(entities)) responses := make([]*response.MediaResponse, len(entities))
for i, entity := range entities { for i, entity := range entities {
@@ -62,7 +74,7 @@ func MediaEntitiesToResponse(entities []*MediaEntity) []*response.MediaResponse
} }
func MediaEntitiesToStorageEntitye(entities []*MediaEntity) []*MediaStorageEntity { func MediaEntitiesToStorageEntity(entities []*MediaEntity) []*MediaStorageEntity {
responses := make([]*MediaStorageEntity, len(entities)) responses := make([]*MediaStorageEntity, len(entities))
for i, entity := range entities { for i, entity := range entities {
responses[i] = entity.ToStorageEntity() responses[i] = entity.ToStorageEntity()

View File

@@ -10,16 +10,23 @@ import (
type UserVerificationEntity struct { type UserVerificationEntity struct {
ID string `json:"id"` ID string `json:"id"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
VerifyType string `json:"verify_type"` VerifyType constants.VerifyType `json:"verify_type"`
Content string `json:"content"` Content string `json:"content"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
Status constants.VerifyType `json:"status"` Status constants.StatusType `json:"status"`
ReviewedBy *string `json:"reviewed_by"` ReviewedBy string `json:"reviewed_by"`
ReviewNote string `json:"review_note"`
ReviewedAt *time.Time `json:"reviewed_at"` ReviewedAt *time.Time `json:"reviewed_at"`
CreatedAt time.Time `json:"created_at"` CreatedAt *time.Time `json:"created_at"`
Media []*MediaSimpleEntity `json:"media"` Media []*MediaSimpleEntity `json:"media"`
} }
type UserVerificationStorageEntity struct {
Email string `json:"email"`
Status constants.StatusType `json:"status"`
ReviewNote string `json:"review_note"`
}
func (u *UserVerificationEntity) ParseMedia(data []byte) error { func (u *UserVerificationEntity) ParseMedia(data []byte) error {
if len(data) == 0 { if len(data) == 0 {
u.Media = []*MediaSimpleEntity{} u.Media = []*MediaSimpleEntity{}
@@ -47,20 +54,26 @@ func (u *UserVerificationEntity) ToResponse() *response.UserVerificationResponse
res := &response.UserVerificationResponse{ res := &response.UserVerificationResponse{
ID: u.ID, ID: u.ID,
UserID: u.UserID, UserID: u.UserID,
VerifyType: u.VerifyType, VerifyType: u.VerifyType.String(),
Content: u.Content, Content: u.Content,
Status: u.Status.String(), Status: u.Status.String(),
ReviewNote: u.ReviewNote,
CreatedAt: u.CreatedAt, CreatedAt: u.CreatedAt,
Medias: mediaResponses, Medias: mediaResponses,
} }
if u.ReviewedBy != nil {
res.ReviewedBy = u.ReviewedBy
}
if u.ReviewedAt != nil { if u.ReviewedAt != nil {
res.ReviewedAt = u.ReviewedAt res.ReviewedAt = u.ReviewedAt
} }
return res return res
} }
func UserVerificationsEntitiesToResponse(entities []*UserVerificationEntity) []*response.UserVerificationResponse {
responses := make([]*response.UserVerificationResponse, len(entities))
for i, entity := range entities {
responses[i] = entity.ToResponse()
}
return responses
}

View File

@@ -141,8 +141,7 @@ func (r *mediaRepository) Create(ctx context.Context, params sqlc.CreateMediaPar
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
} }
cacheId := fmt.Sprintf("media:id:%s", media.ID) _ = r.c.Set(ctx, fmt.Sprintf("media:id:%s", media.ID), media, constants.NormalCacheDuration)
_ = r.c.Set(ctx, cacheId, media, constants.NormalCacheDuration)
_ = r.c.Del(ctx, fmt.Sprintf("media:userId:%s", convert.UUIDToString(params.UserID))) _ = r.c.Del(ctx, fmt.Sprintf("media:userId:%s", convert.UUIDToString(params.UserID)))
return &media, nil return &media, nil
} }

View File

@@ -2,9 +2,14 @@ package repositories
import ( import (
"context" "context"
"crypto/md5"
"encoding/json"
"fmt"
"history-api/internal/gen/sqlc" "history-api/internal/gen/sqlc"
"history-api/internal/models" "history-api/internal/models"
"history-api/pkg/cache" "history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
@@ -13,10 +18,13 @@ type VerificationRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.UserVerificationEntity, error) GetByID(ctx context.Context, id pgtype.UUID) (*models.UserVerificationEntity, error)
GetByUserID(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) Count(ctx context.Context, params sqlc.CountUserVerificationsParams) (int64, error)
Create(ctx context.Context, params sqlc.CreateUserVerificationParams) (*models.UserVerificationEntity, error)
UpdateStatus(ctx context.Context, params sqlc.UpdateUserVerificationStatusParams) error
Search(ctx context.Context, params sqlc.SearchUserVerificationsParams) ([]*models.UserVerificationEntity, error) Search(ctx context.Context, params sqlc.SearchUserVerificationsParams) ([]*models.UserVerificationEntity, error)
Delete(ctx context.Context, id pgtype.UUID) error Delete(ctx context.Context, id pgtype.UUID) error
CreateVerificationMedia(ctx context.Context, params sqlc.CreateVerificationMediaParams) error CreateVerificationMedia(ctx context.Context, params sqlc.CreateVerificationMediaParams) error
DeleteVerificationMedia(ctx context.Context, params sqlc.DeleteVerificationMediasParams) error DeleteVerificationMedia(ctx context.Context, params sqlc.DeleteVerificationMediaParams) error
BulkVerificationMediaByMediaId(ctx context.Context, mediaId pgtype.UUID) error
} }
type verificationRepository struct { type verificationRepository struct {
@@ -24,9 +32,297 @@ type verificationRepository struct {
c cache.Cache c cache.Cache
} }
// func NewVerificationRepository(db sqlc.DBTX, c cache.Cache) VerificationRepository { func NewVerificationRepository(db sqlc.DBTX, c cache.Cache) VerificationRepository {
// return &verificationRepository{ return &verificationRepository{
// q: sqlc.New(db), q: sqlc.New(db),
// c: c, c: c,
// } }
// } }
func (v *verificationRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
return fmt.Sprintf("%s:%s", prefix, hash)
}
func (v *verificationRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.UserVerificationEntity, error) {
cacheId := fmt.Sprintf("verification:id:%s", convert.UUIDToString(id))
var verification models.UserVerificationEntity
err := v.c.Get(ctx, cacheId, &verification)
if err == nil {
return &verification, nil
}
row, err := v.q.GetUserVerificationByID(ctx, id)
if err != nil {
return nil, err
}
verification = models.UserVerificationEntity{
ID: convert.UUIDToString(row.ID),
UserID: convert.UUIDToString(row.UserID),
VerifyType: constants.ParseVerifyType(row.VerifyType),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
Status: constants.ParseStatusType(row.Status),
ReviewNote: convert.TextToString(row.ReviewNote),
ReviewedBy: convert.UUIDToString(row.ReviewedBy),
ReviewedAt: convert.TimeToPtr(row.ReviewedAt),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}
if err := verification.ParseMedia(row.Medias); err != nil {
return nil, err
}
_ = v.c.Set(ctx, cacheId, verification, constants.NormalCacheDuration)
return &verification, nil
}
func (v *verificationRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.UserVerificationEntity, error) {
if len(ids) == 0 {
return []*models.UserVerificationEntity{}, nil
}
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("verification:id:%s", id)
}
raws := v.c.MGet(ctx, keys...)
var verification []*models.UserVerificationEntity
missingVerificationToCache := make(map[string]any)
for i, b := range raws {
if len(b) > 0 {
var u models.UserVerificationEntity
if err := json.Unmarshal(b, &u); err == nil {
verification = append(verification, &u)
}
} else {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
}
dbUser, err := v.GetByID(ctx, pgId)
if err == nil && dbUser != nil {
verification = append(verification, dbUser)
missingVerificationToCache[keys[i]] = dbUser
}
}
}
if len(missingVerificationToCache) > 0 {
_ = v.c.MSet(ctx, missingVerificationToCache, constants.NormalCacheDuration)
}
return verification, nil
}
func (v *verificationRepository) Count(ctx context.Context, params sqlc.CountUserVerificationsParams) (int64, error) {
queryKey := v.generateQueryKey("verification:count", params)
var count int64
if err := v.c.Get(ctx, queryKey, &count); err == nil {
_ = v.c.Set(ctx, queryKey, count, constants.ListCacheDuration)
return count, nil
}
count, err := v.q.CountUserVerifications(ctx, params)
if err != nil {
return 0, err
}
_ = v.c.Set(ctx, queryKey, count, constants.ListCacheDuration)
return count, nil
}
func (v *verificationRepository) Create(ctx context.Context, params sqlc.CreateUserVerificationParams) (*models.UserVerificationEntity, error) {
row, err := v.q.CreateUserVerification(ctx, params)
if err != nil {
return nil, err
}
go func() {
bgCtx := context.Background()
_ = v.c.DelByPattern(bgCtx, "verification:search*")
_ = v.c.DelByPattern(bgCtx, "verification:count*")
}()
verification := models.UserVerificationEntity{
ID: convert.UUIDToString(row.ID),
UserID: convert.UUIDToString(row.UserID),
VerifyType: constants.ParseVerifyType(row.VerifyType),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
Status: constants.ParseStatusType(row.Status),
ReviewNote: convert.TextToString(row.ReviewNote),
ReviewedBy: convert.UUIDToString(row.ReviewedBy),
ReviewedAt: convert.TimeToPtr(row.ReviewedAt),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}
_ = v.c.Del(ctx, fmt.Sprintf("verification:userId:%s", convert.UUIDToString(params.UserID)))
return &verification, nil
}
func (v *verificationRepository) UpdateStatus(ctx context.Context, params sqlc.UpdateUserVerificationStatusParams) error {
err := v.q.UpdateUserVerificationStatus(ctx, params)
return err
}
func (v *verificationRepository) Delete(ctx context.Context, id pgtype.UUID) error {
err := v.q.DeleteUserVerification(ctx, id)
if err != nil {
return err
}
cacheId := fmt.Sprintf("verification:id:%s", convert.UUIDToString(id))
_ = v.c.Del(ctx, cacheId)
return nil
}
func (v *verificationRepository) BulkVerificationMediaByMediaId(ctx context.Context, mediaId pgtype.UUID) error {
ids, err := v.q.BulkDeleteVerificationMediaByMediaId(ctx, mediaId)
if err != nil {
return err
}
listCacheId := make([]string, 0)
for _, it := range ids {
id := convert.UUIDToString(it)
if id == "" {
continue
}
listCacheId = append(listCacheId, fmt.Sprintf("verification:id:%s", id))
}
go func() {
bgCtx := context.Background()
_ = v.c.Del(bgCtx, listCacheId...)
}()
return nil
}
func (v *verificationRepository) CreateVerificationMedia(ctx context.Context, params sqlc.CreateVerificationMediaParams) error {
err := v.q.CreateVerificationMedia(ctx, params)
return err
}
func (v *verificationRepository) DeleteVerificationMedia(ctx context.Context, params sqlc.DeleteVerificationMediaParams) error {
err := v.q.DeleteVerificationMedia(ctx, params)
return err
}
func (v *verificationRepository) GetByUserID(ctx context.Context, userId pgtype.UUID) ([]*models.UserVerificationEntity, error) {
queryKey := fmt.Sprintf("verification:userId:%s", convert.UUIDToString(userId))
var cachedIDs []string
if err := v.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
listItem, err := v.getByIDsWithFallback(ctx, cachedIDs)
if err != nil {
return nil, err
}
newCachedIDs := make([]string, len(listItem))
for i, media := range listItem {
newCachedIDs[i] = media.ID
}
_ = v.c.Set(ctx, queryKey, newCachedIDs, constants.ListCacheDuration)
return listItem, nil
}
rows, err := v.q.GetUserVerifications(ctx, userId)
if err != nil {
return nil, err
}
var items []*models.UserVerificationEntity
var ids []string
itemToCache := make(map[string]any)
for _, row := range rows {
verification := &models.UserVerificationEntity{
ID: convert.UUIDToString(row.ID),
UserID: convert.UUIDToString(row.UserID),
VerifyType: constants.ParseVerifyType(row.VerifyType),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
Status: constants.ParseStatusType(row.Status),
ReviewNote: convert.TextToString(row.ReviewNote),
ReviewedBy: convert.UUIDToString(row.ReviewedBy),
ReviewedAt: convert.TimeToPtr(row.ReviewedAt),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}
if err := verification.ParseMedia(row.Medias); err != nil {
return nil, err
}
ids = append(ids, verification.ID)
items = append(items, verification)
itemToCache[fmt.Sprintf("verification:id:%s", verification.ID)] = verification
}
if len(itemToCache) > 0 {
_ = v.c.MSet(ctx, itemToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = v.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
}
return items, nil
}
func (v *verificationRepository) Search(ctx context.Context, params sqlc.SearchUserVerificationsParams) ([]*models.UserVerificationEntity, error) {
queryKey := v.generateQueryKey("verification:search", params)
var cachedIDs []string
if err := v.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
listItem, err := v.getByIDsWithFallback(ctx, cachedIDs)
if err != nil {
return nil, err
}
newCachedIDs := make([]string, len(listItem))
for i, media := range listItem {
newCachedIDs[i] = media.ID
}
_ = v.c.Set(ctx, queryKey, newCachedIDs, constants.ListCacheDuration)
return listItem, err
}
rows, err := v.q.SearchUserVerifications(ctx, params)
if err != nil {
return nil, err
}
var items []*models.UserVerificationEntity
var ids []string
itemToCache := make(map[string]any)
for _, row := range rows {
verification := &models.UserVerificationEntity{
ID: convert.UUIDToString(row.ID),
UserID: convert.UUIDToString(row.UserID),
VerifyType: constants.ParseVerifyType(row.VerifyType),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
Status: constants.ParseStatusType(row.Status),
ReviewNote: convert.TextToString(row.ReviewNote),
ReviewedBy: convert.UUIDToString(row.ReviewedBy),
ReviewedAt: convert.TimeToPtr(row.ReviewedAt),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}
if err := verification.ParseMedia(row.Medias); err != nil {
return nil, err
}
ids = append(ids, verification.ID)
items = append(items, verification)
itemToCache[fmt.Sprintf("verification:id:%s", verification.ID)] = verification
}
if len(itemToCache) > 0 {
_ = v.c.MSet(ctx, itemToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = v.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
}
return items, nil
}

View File

@@ -58,6 +58,13 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
controller.GetMediaByUserID, controller.GetMediaByUserID,
) )
route.Get(
"/:id/application",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
controller.GetVerificationByUserID,
)
route.Patch( route.Patch(
"/:id/restore", "/:id/restore",
middlewares.JwtAccess(userRepo), middlewares.JwtAccess(userRepo),

View File

@@ -0,0 +1,48 @@
package routes
import (
"history-api/internal/controllers"
"history-api/internal/middlewares"
"history-api/internal/repositories"
"history-api/pkg/constants"
"github.com/gofiber/fiber/v3"
)
func VerificationRoutes(app *fiber.App, controller *controllers.VerificationController, userRepo repositories.UserRepository) {
route := app.Group("/historian/application")
route.Get(
"/",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
controller.SearchVerification,
)
route.Post(
"/",
middlewares.JwtAccess(userRepo),
middlewares.ForbidRoles(constants.HISTORIAN),
controller.CreateVerification,
)
route.Get(
"/:id",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
controller.GetVerificationByID,
)
route.Delete(
"/:id",
middlewares.JwtAccess(userRepo),
controller.DeleteVerification,
)
route.Put(
"/:id/status",
middlewares.JwtAccess(userRepo),
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
controller.UpdateVerificationStatus,
)
}

View File

@@ -229,6 +229,7 @@ func (m *mediaService) SearchMedia(ctx context.Context, dto *request.SearchMedia
return response.BuildPaginatedResponse(rows, totalRecords, dto.Page, dto.Limit), nil return response.BuildPaginatedResponse(rows, totalRecords, dto.Page, dto.Limit), nil
} }
func (m *mediaService) UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, error) { func (m *mediaService) UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, error) {
userIdUUID, err := convert.StringToUUID(userId) userIdUUID, err := convert.StringToUUID(userId)
if err != nil { if err != nil {

View File

@@ -307,11 +307,11 @@ func (m *userService) fillSearchArgs(arg *sqlc.SearchUsersParams, dto *request.S
} }
if dto.CreatedFrom != nil { if dto.CreatedFrom != nil {
arg.CreatedFrom = pgtype.Timestamp{Time: *dto.CreatedFrom, Valid: true} arg.CreatedFrom = pgtype.Timestamptz{Time: *dto.CreatedFrom, Valid: true}
} }
if dto.CreatedTo != nil { if dto.CreatedTo != nil {
arg.CreatedTo = pgtype.Timestamp{Time: *dto.CreatedTo, Valid: true} arg.CreatedTo = pgtype.Timestamptz{Time: *dto.CreatedTo, Valid: true}
} }
if dto.IsDeleted != nil { if dto.IsDeleted != nil {

View File

@@ -0,0 +1,380 @@
package services
import (
"context"
"fmt"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/internal/repositories"
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
"slices"
"github.com/gofiber/fiber/v3"
"github.com/jackc/pgx/v5/pgtype"
"golang.org/x/sync/errgroup"
)
type VerificationService interface {
GetVerificationByID(ctx context.Context, verificationId string) (*response.UserVerificationResponse, error)
GetVerificationByUserID(ctx context.Context, userId string) ([]*response.UserVerificationResponse, error)
SearchVerification(ctx context.Context, dto *request.SearchUserVerificationDto) (*response.PaginatedResponse, error)
DeleteVerification(ctx context.Context, claims *response.JWTClaims, verificationId string) error
CreateVerification(ctx context.Context, userId string, dto *request.CreateUserVerificationDto) (*response.UserVerificationResponse, error)
UpdateStatusVerification(ctx context.Context, userId string, verificationId string, dto *request.UpdateVerificationStatusDto) (*response.UserVerificationResponse, error)
}
type verificationService struct {
verificationRepo repositories.VerificationRepository
mediaRepo repositories.MediaRepository
userRepo repositories.UserRepository
roleRepo repositories.RoleRepository
c cache.Cache
}
func NewVerificationService(
verificationRepo repositories.VerificationRepository,
mediaRepo repositories.MediaRepository,
userRepo repositories.UserRepository,
roleRepo repositories.RoleRepository,
c cache.Cache,
) VerificationService {
return &verificationService{
verificationRepo: verificationRepo,
mediaRepo: mediaRepo,
userRepo: userRepo,
roleRepo: roleRepo,
c: c,
}
}
func (v *verificationService) CreateVerification(ctx context.Context, userId string, dto *request.CreateUserVerificationDto) (*response.UserVerificationResponse, error) {
verifyType := constants.ParseVerifyTypeText(dto.VerifyType)
if verifyType == constants.VerifyUnknown {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Unknown verify type!")
}
pgID, err := convert.StringToUUID(userId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
mediaList, err := v.mediaRepo.GetByIDs(ctx, dto.MediaIDs)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
if len(mediaList) != len(dto.MediaIDs) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Some media IDs are invalid!")
}
item, err := v.verificationRepo.Create(
ctx,
sqlc.CreateUserVerificationParams{
VerifyType: verifyType.Int16(),
Content: convert.PtrToText(&dto.Content),
UserID: pgID,
},
)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
itemId, err := convert.StringToUUID(item.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
mediaIdList := make([]pgtype.UUID, 0)
for _, it := range mediaList {
mediaId, err := convert.StringToUUID(it.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
mediaIdList = append(mediaIdList, mediaId)
item.Media = append(item.Media, it.ToSimpleEntity())
}
err = v.verificationRepo.CreateVerificationMedia(
ctx,
sqlc.CreateVerificationMediaParams{
VerificationID: itemId,
Column2: mediaIdList,
},
)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return item.ToResponse(), nil
}
func (v *verificationService) DeleteVerification(ctx context.Context, claims *response.JWTClaims, verificationId string) error {
verificationIdUUID, err := convert.StringToUUID(verificationId)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
verification, err := v.verificationRepo.GetByID(ctx, verificationIdUUID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
shoudDelete := false
if slices.Contains(claims.Roles, constants.ADMIN) || slices.Contains(claims.Roles, constants.MOD) || verification.UserID == claims.UId {
shoudDelete = true
}
if !shoudDelete {
return fiber.NewError(fiber.StatusForbidden, "You don't have permission to delete this verification")
}
err = v.mediaRepo.Delete(ctx, verificationIdUUID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return nil
}
func (v *verificationService) GetVerificationByID(ctx context.Context, verificationId string) (*response.UserVerificationResponse, error) {
verificationUUID, err := convert.StringToUUID(verificationId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
verification, err := v.verificationRepo.GetByID(ctx, verificationUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return verification.ToResponse(), nil
}
func (v *verificationService) GetVerificationByUserID(ctx context.Context, userId string) ([]*response.UserVerificationResponse, error) {
userUUID, err := convert.StringToUUID(userId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
verifications, err := v.verificationRepo.GetByUserID(ctx, userUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return models.UserVerificationsEntitiesToResponse(verifications), nil
}
func (m *verificationService) fillSearchArgs(arg *sqlc.SearchUserVerificationsParams, dto *request.SearchUserVerificationDto) {
if dto.Sort != "" {
arg.Sort = pgtype.Text{String: dto.Sort, Valid: true}
} else {
arg.Sort = pgtype.Text{String: "id", Valid: true}
}
arg.Order = pgtype.Text{String: "asc", Valid: true}
if dto.Order == "desc" {
arg.Order = pgtype.Text{String: "desc", Valid: true}
}
if len(dto.Statuses) > 0 {
for _, id := range dto.Statuses {
if u := constants.ParseStatusTypeText(id); u == constants.StatusUnknown {
arg.Statuses = append(arg.Statuses, u.Int16())
}
}
}
if len(dto.VerifyTypes) > 0 {
for _, id := range dto.VerifyTypes {
if u := constants.ParseVerifyTypeText(id); u == constants.VerifyUnknown {
arg.VerifyTypes = append(arg.VerifyTypes, u.Int16())
}
}
}
if len(dto.UserIDs) > 0 {
for _, id := range dto.UserIDs {
if u, err := convert.StringToUUID(id); err == nil {
arg.UserIds = append(arg.UserIds, u)
}
}
}
if dto.ReviewedBy != nil {
if rvID, err := convert.StringToUUID(*dto.ReviewedBy); err == nil {
arg.ReviewedBy = rvID
}
}
if dto.CreatedFrom != nil {
arg.CreatedFrom = pgtype.Timestamptz{Time: *dto.CreatedFrom, Valid: true}
}
if dto.CreatedTo != nil {
arg.CreatedTo = pgtype.Timestamptz{Time: *dto.CreatedTo, Valid: true}
}
if dto.Search != "" {
arg.SearchText = pgtype.Text{String: dto.Search, Valid: true}
}
}
func (v *verificationService) SearchVerification(ctx context.Context, dto *request.SearchUserVerificationDto) (*response.PaginatedResponse, error) {
if dto.Page < 1 {
dto.Page = 1
}
if dto.Limit == 0 {
dto.Limit = 20
}
offset := (dto.Page - 1) * dto.Limit
arg := sqlc.SearchUserVerificationsParams{
Limit: int32(dto.Limit),
Offset: int32(offset),
}
v.fillSearchArgs(&arg, dto)
var rows []*models.UserVerificationEntity
var totalRecords int64
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
var err error
rows, err = v.verificationRepo.Search(gCtx, arg)
return err
})
g.Go(func() error {
countArg := sqlc.CountUserVerificationsParams{
UserIds: arg.UserIds,
Statuses: arg.Statuses,
VerifyTypes: arg.VerifyTypes,
ReviewedBy: arg.ReviewedBy,
CreatedFrom: arg.CreatedFrom,
CreatedTo: arg.CreatedTo,
SearchText: arg.SearchText,
}
var err error
totalRecords, err = v.verificationRepo.Count(gCtx, countArg)
return err
})
if err := g.Wait(); err != nil {
return nil, err
}
return response.BuildPaginatedResponse(rows, totalRecords, dto.Page, dto.Limit), nil
}
func (v *verificationService) UpdateStatusVerification(ctx context.Context, userId string, verificationId string, dto *request.UpdateVerificationStatusDto) (*response.UserVerificationResponse, error) {
statusType := constants.ParseStatusTypeText(dto.Status)
if statusType == constants.StatusUnknown {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Unknown status type!")
}
verificationUUID, err := convert.StringToUUID(verificationId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
userAdminUUID, err := convert.StringToUUID(userId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
historianRole, err := v.roleRepo.GetByname(ctx, constants.HISTORIAN.String())
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
historianRoleID, err := convert.StringToUUID(historianRole.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
verification, err := v.verificationRepo.GetByID(ctx, verificationUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
if verification.Status != constants.StatusPending {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid status!")
}
userVerificationUUID, err := convert.StringToUUID(verification.UserID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
userVerification, err := v.userRepo.GetByID(ctx, userVerificationUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = v.verificationRepo.UpdateStatus(
ctx,
sqlc.UpdateUserVerificationStatusParams{
ID: verificationUUID,
Status: statusType.Int16(),
ReviewedBy: userAdminUUID,
ReviewNote: convert.PtrToText(&dto.ReviewNote),
},
)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
verification.Status = statusType
data := &models.UserVerificationStorageEntity{
Email: userVerification.Email,
ReviewNote: dto.ReviewNote,
Status: statusType,
}
roleIdList := make([]pgtype.UUID, 0)
userVerification.Roles = append(userVerification.Roles, historianRole.ToRoleSimple())
roleIdList = append(roleIdList, historianRoleID)
for _, role := range userVerification.Roles {
roleID, err := convert.StringToUUID(role.ID)
if err != nil {
continue
}
roleIdList = append(roleIdList, roleID)
}
err = v.roleRepo.BulkDeleteRolesFromUser(ctx, userVerificationUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = v.roleRepo.CreateUserRole(ctx, sqlc.CreateUserRoleParams{
UserID: userVerificationUUID,
Column2: roleIdList,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
err = v.userRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
ID: userVerificationUUID,
TokenVersion: userVerification.TokenVersion + 1,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
userVerification.TokenVersion += 1
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", userVerification.Email): userVerification,
fmt.Sprintf("user:id:%s", userVerification.ID): userVerification,
}
_ = v.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
v.c.PublishTask(ctx, constants.StreamEmailName, constants.TaskTypeNotifyHistorianReview, data)
return verification.ToResponse(), nil
}

View File

@@ -3,6 +3,7 @@ package constants
type StatusType int16 type StatusType int16
const ( const (
StatusUnknown StatusType = 0
StatusPending StatusType = 1 StatusPending StatusType = 1
StatusApproved StatusType = 2 StatusApproved StatusType = 2
StatusRejected StatusType = 3 StatusRejected StatusType = 3
@@ -21,6 +22,10 @@ func (t StatusType) String() string {
} }
} }
func (t StatusType) Int16() int16 {
return int16(t)
}
func ParseStatusType(v int16) StatusType { func ParseStatusType(v int16) StatusType {
switch v { switch v {
case 1: case 1:
@@ -30,6 +35,19 @@ func ParseStatusType(v int16) StatusType {
case 3: case 3:
return StatusRejected return StatusRejected
default: default:
return 0 return StatusUnknown
}
}
func ParseStatusTypeText(v string) StatusType {
switch v {
case "PENDING":
return StatusPending
case "APPROVED":
return StatusApproved
case "REJECT":
return StatusRejected
default:
return StatusUnknown
} }
} }

View File

@@ -3,9 +3,10 @@ package constants
type TaskType string type TaskType string
const ( const (
TaskTypeSendEmailOTP TaskType = "SEND_EMAIL_OTP" TaskTypeSendEmailOTP TaskType = "SEND_EMAIL_OTP"
TaskTypeDeleteMedia TaskType = "DELETE_MEDIA" TaskTypeNotifyHistorianReview TaskType = "NOTIFY_HISTORIAN_REVIEW"
TaskTypeBulkDeleteMedia TaskType = "BULK_DELETE_MEDIA" TaskTypeDeleteMedia TaskType = "DELETE_MEDIA"
TaskTypeBulkDeleteMedia TaskType = "BULK_DELETE_MEDIA"
) )
func (t TaskType) String() string { func (t TaskType) String() string {

View File

@@ -25,7 +25,26 @@ func (t VerifyType) String() string {
} }
} }
func ParseVerifyType(v string) VerifyType { func (t VerifyType) Int16() int16 {
return int16(t)
}
func ParseVerifyType(v int16) VerifyType {
switch v {
case 1:
return VerifyIdCard
case 2:
return VerifyEducation
case 3:
return VerifyExpert
case 4:
return VerifyOther
default:
return VerifyUnknown
}
}
func ParseVerifyTypeText(v string) VerifyType {
switch v { switch v {
case "ID_CARD": case "ID_CARD":
return VerifyIdCard return VerifyIdCard

View File

@@ -45,7 +45,7 @@ func TimeToPtr(v pgtype.Timestamptz) *time.Time {
} }
func PtrToText(s *string) pgtype.Text { func PtrToText(s *string) pgtype.Text {
if s == nil { if s == nil || *s == "" {
return pgtype.Text{Valid: false} return pgtype.Text{Valid: false}
} }
return pgtype.Text{ return pgtype.Text{

View File

@@ -3,6 +3,7 @@ package email
import ( import (
"fmt" "fmt"
"history-api/assets" "history-api/assets"
"history-api/internal/models"
"history-api/pkg/config" "history-api/pkg/config"
"history-api/pkg/constants" "history-api/pkg/constants"
"strings" "strings"
@@ -10,7 +11,7 @@ import (
"github.com/wneessen/go-mail" "github.com/wneessen/go-mail"
) )
func SendMailOTP(toEmail, otpCode string, tokenType constants.TokenType) error { func SendMail(toEmail, subject, templatePath string, data map[string]string) error {
userSmtp, err := config.GetConfig("SMTP_USER") userSmtp, err := config.GetConfig("SMTP_USER")
if err != nil { if err != nil {
return err return err
@@ -21,36 +22,27 @@ func SendMailOTP(toEmail, otpCode string, tokenType constants.TokenType) error {
return err return err
} }
var subject string
var templatePath string
switch tokenType {
case constants.TokenPasswordReset:
subject = "Your Password Reset Code"
templatePath = "resources/password_reset.html"
case constants.TokenEmailVerify:
subject = "Verify your email address"
templatePath = "resources/email_verify.html"
default:
return fmt.Errorf("invalid token type: %v", tokenType)
}
htmlTemplate, err := assets.GetFileContent(templatePath) htmlTemplate, err := assets.GetFileContent(templatePath)
if err != nil { if err != nil {
return fmt.Errorf("failed to read email template: %s", err) return fmt.Errorf("failed to read email template: %s", err)
} }
message := mail.NewMsg() finalHTML := htmlTemplate
if err := message.From(userSmtp); err != nil { for k, v := range data {
return fmt.Errorf("failed to set From email address: %s", err) finalHTML = strings.ReplaceAll(finalHTML, "{{"+k+"}}", v)
}
if err := message.To(toEmail); err != nil {
return fmt.Errorf("failed to set To email address: %s", err)
} }
finalHTML := strings.ReplaceAll(htmlTemplate, "{{OTP_CODE}}", otpCode) message := mail.NewMsg()
if err := message.From(userSmtp); err != nil {
return fmt.Errorf("failed to set From: %s", err)
}
if err := message.To(toEmail); err != nil {
return fmt.Errorf("failed to set To: %s", err)
}
message.Subject(subject) message.Subject(subject)
message.SetBodyString(mail.TypeTextHTML, finalHTML) message.SetBodyString(mail.TypeTextHTML, finalHTML)
client, err := mail.NewClient( client, err := mail.NewClient(
"smtp.gmail.com", "smtp.gmail.com",
mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover),
@@ -59,12 +51,56 @@ func SendMailOTP(toEmail, otpCode string, tokenType constants.TokenType) error {
mail.WithPassword(passSmtp), mail.WithPassword(passSmtp),
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to create mail client: %s", err) return fmt.Errorf("failed to create client: %s", err)
} }
err = client.DialAndSend(message) if err := client.DialAndSend(message); err != nil {
if err != nil {
return fmt.Errorf("failed to send mail: %s", err) return fmt.Errorf("failed to send mail: %s", err)
} }
return nil return nil
} }
func SendMailOTP(dto *models.TokenEntity) error {
var subject string
var templatePath string
switch dto.TokenType {
case constants.TokenPasswordReset:
subject = "Your Password Reset Code"
templatePath = "resources/password_reset.html"
case constants.TokenEmailVerify:
subject = "Verify your email address"
templatePath = "resources/email_verify.html"
default:
return fmt.Errorf("invalid token type: %v", dto.TokenType)
}
return SendMail(dto.Email, subject, templatePath, map[string]string{
"OTP_CODE": dto.Token,
})
}
func SendHistorianReviewMail(dto *models.UserVerificationStorageEntity) error {
var subject string
var templatePath string
feUrl := config.GetConfigWithDefault("FRONTEND_URL", "http://localhost:3000")
switch dto.Status {
case constants.StatusApproved:
subject = "Your Historian Application is Approved"
templatePath = "resources/historian_approved.html"
case constants.StatusRejected:
subject = "Your Historian Application is Rejected"
templatePath = "resources/historian_rejected.html"
default:
return fmt.Errorf("invalid status: %v", dto.Status)
}
return SendMail(dto.Email, subject, templatePath, map[string]string{
"NAME": dto.Email,
"REASON": dto.ReviewNote,
"APP_URL": feUrl,
})
}