user and otp schema modification, SMTP setup using resend, afro SMS changed to direct API integration instead of using afoSMS library, most authentications implemented using username instead of email or phone number

This commit is contained in:
Yared Yemane 2025-12-23 18:57:48 +03:00
parent 47d70b029f
commit 915185c317
39 changed files with 3403 additions and 3126 deletions

View File

@ -48,7 +48,7 @@ INSERT INTO users (
id, id,
first_name, first_name,
last_name, last_name,
nick_name, user_name,
email, email,
phone_number, phone_number,
password, password,
@ -128,7 +128,7 @@ VALUES
ON CONFLICT (id) DO UPDATE ON CONFLICT (id) DO UPDATE
SET first_name = EXCLUDED.first_name, SET first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name, last_name = EXCLUDED.last_name,
nick_name = EXCLUDED.nick_name, user_name = EXCLUDED.user_name,
email = EXCLUDED.email, email = EXCLUDED.email,
phone_number = EXCLUDED.phone_number, phone_number = EXCLUDED.phone_number,
password = EXCLUDED.password, password = EXCLUDED.password,

View File

@ -8,11 +8,6 @@ SELECT setval(
) )
FROM users; FROM users;
SELECT setval(
pg_get_serial_sequence('organizations', 'id'),
COALESCE(MAX(id), 1)
)
FROM organizations;
SELECT setval( SELECT setval(
pg_get_serial_sequence('courses', 'id'), pg_get_serial_sequence('courses', 'id'),

View File

@ -33,16 +33,6 @@ DROP TABLE IF EXISTS lessons;
DROP TABLE IF EXISTS course_modules; DROP TABLE IF EXISTS course_modules;
DROP TABLE IF EXISTS courses; DROP TABLE IF EXISTS courses;
-- =========================================
-- Organization Settings
-- =========================================
DROP TABLE IF EXISTS organization_settings;
DROP TABLE IF EXISTS global_settings;
-- =========================================
-- Organizations (Tenants)
-- =========================================
DROP TABLE IF EXISTS organizations;
-- ========================================= -- =========================================
-- Authentication & Security -- Authentication & Security

View File

@ -2,10 +2,11 @@ CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
first_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL,
nick_name VARCHAR(100), user_name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE, email VARCHAR(255) UNIQUE,
phone_number VARCHAR(20) UNIQUE, phone_number VARCHAR(20) UNIQUE,
role VARCHAR(50) NOT NULL, -- SUPER_ADMIN, ORG_ADMIN, INSTRUCTOR, STUDENT, SUPPORT
role VARCHAR(50) NOT NULL, -- SUPER_ADMIN, INSTRUCTOR, STUDENT, SUPPORT
password BYTEA NOT NULL, password BYTEA NOT NULL,
age INT, age INT,
education_level VARCHAR(100), education_level VARCHAR(100),
@ -13,16 +14,19 @@ CREATE TABLE IF NOT EXISTS users (
region VARCHAR(100), region VARCHAR(100),
email_verified BOOLEAN NOT NULL DEFAULT FALSE, email_verified BOOLEAN NOT NULL DEFAULT FALSE,
phone_verified BOOLEAN NOT NULL DEFAULT FALSE, phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
suspended BOOLEAN NOT NULL DEFAULT FALSE, status VARCHAR(50) NOT NULL, -- PENDING, ACTIVE, SUSPENDED, DEACTIVATED
suspended_at TIMESTAMPTZ, last_login TIMESTAMPTZ,
organization_id BIGINT, profile_completed BOOLEAN NOT NULL DEFAULT FALSE,
profile_picture_url TEXT,
preferred_language VARCHAR(50),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ, updated_at TIMESTAMPTZ,
CHECK (email IS NOT NULL OR phone_number IS NOT NULL),
UNIQUE (email, organization_id), CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
UNIQUE (phone_number, organization_id)
); );
CREATE TABLE refresh_tokens ( CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@ -34,9 +38,10 @@ CREATE TABLE refresh_tokens (
CREATE TABLE otps ( CREATE TABLE otps (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_name VARCHAR(100) NOT NULL,
sent_to VARCHAR(255) NOT NULL, sent_to VARCHAR(255) NOT NULL,
medium VARCHAR(50) NOT NULL, -- email, sms medium VARCHAR(50) NOT NULL, -- email, sms
otp_for VARCHAR(50) NOT NULL, -- login, reset_password, verify otp_for VARCHAR(50) NOT NULL, -- register, reset
otp VARCHAR(10) NOT NULL, otp VARCHAR(10) NOT NULL,
used BOOLEAN NOT NULL DEFAULT FALSE, used BOOLEAN NOT NULL DEFAULT FALSE,
used_at TIMESTAMPTZ, used_at TIMESTAMPTZ,
@ -44,19 +49,8 @@ CREATE TABLE otps (
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE TABLE organizations (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
owner_id BIGINT NOT NULL REFERENCES users(id),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE courses ( CREATE TABLE courses (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
instructor_id BIGINT NOT NULL REFERENCES users(id), instructor_id BIGINT NOT NULL REFERENCES users(id),
title TEXT NOT NULL, title TEXT NOT NULL,
description TEXT, description TEXT,
@ -170,15 +164,6 @@ CREATE TABLE global_settings (
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
); );
CREATE TABLE organization_settings (
organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
key TEXT NOT NULL,
value TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (organization_id, key)
);
CREATE TABLE IF NOT EXISTS reported_issues ( CREATE TABLE IF NOT EXISTS reported_issues (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id), user_id BIGINT NOT NULL REFERENCES users(id),

View File

@ -1,11 +1,22 @@
-- name: UpdateExpiredOtp :exec
UPDATE otps
SET
otp = $2,
used = FALSE,
used_at = NULL,
expires_at = $3
WHERE
user_name = $1
AND expires_at <= NOW();
-- name: CreateOtp :exec -- name: CreateOtp :exec
INSERT INTO otps (sent_to, medium, otp_for, otp, used, created_at, expires_at) INSERT INTO otps (user_name, sent_to, medium, otp_for, otp, used, created_at, expires_at)
VALUES ($1, $2, $3, $4, FALSE, $5, $6); VALUES ($1, $2, $3, $4, $5, FALSE, $6, $7);
-- name: GetOtp :one -- name: GetOtp :one
SELECT id, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at SELECT id, user_name, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
FROM otps FROM otps
WHERE sent_to = $1 AND otp_for = $2 AND medium = $3 WHERE user_name = $1
ORDER BY created_at DESC LIMIT 1; ORDER BY created_at DESC LIMIT 1;
-- name: MarkOtpAsUsed :exec -- name: MarkOtpAsUsed :exec

View File

@ -1,48 +1,61 @@
-- name: IsUserPending :one
SELECT
CASE WHEN status = 'PENDING' THEN true ELSE false END AS is_pending
FROM users
WHERE user_name = $1
LIMIT 1;
-- name: IsUserNameUnique :one
SELECT
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique
FROM users
WHERE user_name = $1;
-- name: CreateUser :one -- name: CreateUser :one
INSERT INTO users ( INSERT INTO users (
first_name,
last_name,
nick_name,
email,
phone_number,
role,
password,
age,
education_level,
country,
region,
email_verified,
phone_verified,
suspended,
suspended_at,
organization_id,
created_at,
updated_at
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14,
$15,
$16,
$17,
$18
)
RETURNING id,
first_name, first_name,
last_name, last_name,
nick_name, user_name,
email,
phone_number,
role,
password,
age,
education_level,
country,
region,
email_verified,
phone_verified,
status,
profile_completed,
preferred_language,
updated_at
)
VALUES (
$1, -- first_name
$2, -- last_name
$3, -- user_name
$4, -- email
$5, -- phone_number
$6, -- role
$7, -- password (BYTEA)
$8, -- age
$9, -- education_level
$10, -- country
$11, -- region
$12, -- email_verified
$13, -- phone_verified
$14, -- status (PENDING | ACTIVE)
$15, -- profile_completed
$16, -- preferred_language
CURRENT_TIMESTAMP
)
RETURNING
id,
first_name,
last_name,
user_name,
email, email,
phone_number, phone_number,
role, role,
@ -52,22 +65,24 @@ RETURNING id,
region, region,
email_verified, email_verified,
phone_verified, phone_verified,
status,
profile_completed,
preferred_language,
created_at, created_at,
updated_at, updated_at;
suspended,
suspended_at,
organization_id;
-- name: GetUserByID :one -- name: GetUserByID :one
SELECT * SELECT *
FROM users FROM users
WHERE id = $1; WHERE id = $1;
-- name: GetAllUsers :many -- name: GetAllUsers :many
SELECT SELECT
COUNT(*) OVER () AS total_count, COUNT(*) OVER () AS total_count,
id, id,
first_name, first_name,
last_name, last_name,
nick_name, user_name,
email, email,
phone_number, phone_number,
role, role,
@ -77,52 +92,44 @@ SELECT
region, region,
email_verified, email_verified,
phone_verified, phone_verified,
status,
profile_completed,
preferred_language,
created_at, created_at,
updated_at, updated_at
suspended,
suspended_at,
organization_id
FROM users FROM users
WHERE ( WHERE (
role = $1 role = $1 OR $1 IS NULL
OR $1 IS NULL
)
AND (
organization_id = $2
OR $2 IS NULL
) )
AND ( AND (
first_name ILIKE '%' || sqlc.narg('query') || '%' first_name ILIKE '%' || sqlc.narg('query') || '%'
OR last_name ILIKE '%' || sqlc.narg('query') || '%' OR last_name ILIKE '%' || sqlc.narg('query') || '%'
OR phone_number ILIKE '%' || sqlc.narg('query') || '%' OR phone_number ILIKE '%' || sqlc.narg('query') || '%'
OR sqlc.narg('query') IS NULL OR email ILIKE '%' || sqlc.narg('query') || '%'
OR sqlc.narg('query') IS NULL
) )
AND ( AND (
created_at > sqlc.narg('created_before') created_at >= sqlc.narg('created_after')
OR sqlc.narg('created_before') IS NULL
)
AND (
created_at < sqlc.narg('created_after')
OR sqlc.narg('created_after') IS NULL OR sqlc.narg('created_after') IS NULL
) )
AND (
created_at <= sqlc.narg('created_before')
OR sqlc.narg('created_before') IS NULL
)
LIMIT sqlc.narg('limit') LIMIT sqlc.narg('limit')
OFFSET sqlc.narg('offset'); OFFSET sqlc.narg('offset');
-- name: GetTotalUsers :one -- name: GetTotalUsers :one
SELECT COUNT(*) SELECT COUNT(*)
FROM users FROM users
wHERE ( WHERE (role = $1 OR $1 IS NULL);
role = $1
OR $1 IS NULL
)
AND (
organization_id = $2
OR $2 IS NULL
);
-- name: SearchUserByNameOrPhone :many -- name: SearchUserByNameOrPhone :many
SELECT id, SELECT
id,
first_name, first_name,
last_name, last_name,
nick_name, user_name,
email, email,
phone_number, phone_number,
role, role,
@ -132,98 +139,113 @@ SELECT id,
region, region,
email_verified, email_verified,
phone_verified, phone_verified,
status,
profile_completed,
created_at, created_at,
updated_at, updated_at
suspended,
suspended_at,
organization_id
FROM users FROM users
WHERE ( WHERE (
organization_id = sqlc.narg('organization_id')
OR sqlc.narg('organization_id') IS NULL
)
AND (
first_name ILIKE '%' || $1 || '%' first_name ILIKE '%' || $1 || '%'
OR last_name ILIKE '%' || $1 || '%' OR last_name ILIKE '%' || $1 || '%'
OR phone_number LIKE '%' || $1 || '%' OR phone_number ILIKE '%' || $1 || '%'
OR email ILIKE '%' || $1 || '%'
) )
AND ( AND (
role = sqlc.narg('role') role = sqlc.narg('role')
OR sqlc.narg('role') IS NULL OR sqlc.narg('role') IS NULL
); );
-- name: UpdateUser :exec -- name: UpdateUser :exec
UPDATE users UPDATE users
SET first_name = $1, SET
last_name = $2, first_name = $1,
suspended = $3, last_name = $2,
status = $3,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $4; WHERE id = $4;
-- name: UpdateUserOrganization :exec
UPDATE users
SET organization_id = $1
WHERE id = $2;
-- name: DeleteUser :exec -- name: DeleteUser :exec
DELETE FROM users DELETE FROM users
WHERE id = $1; WHERE id = $1;
-- name: CheckPhoneEmailExist :one -- name: CheckPhoneEmailExist :one
SELECT EXISTS ( SELECT
EXISTS (
SELECT 1 SELECT 1
FROM users FROM users u1
WHERE users.phone_number = $1 WHERE u1.phone_number = $1
AND users.phone_number IS NOT NULL
AND users.organization_id = $2
) AS phone_exists, ) AS phone_exists,
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM users FROM users u2
WHERE users.email = $3 WHERE u2.email = $2
AND users.email IS NOT NULL
AND users.organization_id = $2
) AS email_exists; ) AS email_exists;
-- name: GetUserByUserName :one
SELECT
id,
first_name,
last_name,
user_name,
email,
phone_number,
role,
password,
age,
education_level,
country,
region,
email_verified,
phone_verified,
status,
profile_completed,
last_login,
profile_picture_url,
preferred_language,
created_at,
updated_at
FROM users
WHERE user_name = $1 AND $1 IS NOT NULL
LIMIT 1;
-- name: GetUserByEmailPhone :one -- name: GetUserByEmailPhone :one
SELECT SELECT
id, id,
first_name, first_name,
last_name, last_name,
nick_name, user_name,
email, email,
phone_number, phone_number,
role, role,
password, -- added this line password,
age, age,
education_level, education_level,
country, country,
region, region,
email_verified, email_verified,
phone_verified, phone_verified,
status,
profile_completed,
last_login,
profile_picture_url,
preferred_language,
created_at, created_at,
updated_at, updated_at
suspended,
suspended_at,
organization_id
FROM users FROM users
WHERE organization_id = $3 WHERE (email = $1 AND $1 IS NOT NULL)
AND (
(email = $1 AND $1 IS NOT NULL)
OR (phone_number = $2 AND $2 IS NOT NULL) OR (phone_number = $2 AND $2 IS NOT NULL)
)
LIMIT 1; LIMIT 1;
-- name: UpdatePassword :exec -- name: UpdatePassword :exec
UPDATE users UPDATE users
SET password = $1, SET
updated_at = $4 password = $1,
WHERE (
(email = $2 OR phone_number = $3)
AND organization_id = $5
);
-- name: GetOwnerByOrganizationID :one
SELECT users.*
FROM organizations
JOIN users ON organizations.owner_id = users.id
WHERE organizations.id = $1;
-- name: SuspendUser :exec
UPDATE users
SET suspended = $1,
suspended_at = $2,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $3; WHERE email = $2 OR phone_number = $3;
-- name: UpdateUserStatus :exec
UPDATE users
SET
status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;

View File

@ -393,58 +393,6 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/managers/{id}": {
"put": {
"description": "Update Managers",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"manager"
],
"summary": "Update Managers",
"parameters": [
{
"description": "Update Managers",
"name": "Managers",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.updateManagerReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/super-login": { "/api/v1/super-login": {
"post": { "post": {
"description": "Login super-admin", "description": "Login super-admin",
@ -761,7 +709,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.UserProfileRes" "$ref": "#/definitions/domain.UserProfileResponse"
} }
}, },
"400": { "400": {
@ -851,7 +799,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.UserProfileRes" "$ref": "#/definitions/domain.UserProfileResponse"
} }
}, },
"400": { "400": {
@ -875,9 +823,9 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/user/suspend": { "/api/v1/user/{user_name}/is-unique": {
"post": { "get": {
"description": "Suspend or unsuspend a user", "description": "Returns whether the specified user_name is available (unique)",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -887,35 +835,33 @@ const docTemplate = `{
"tags": [ "tags": [
"user" "user"
], ],
"summary": "Suspend or unsuspend a user", "summary": "Check if user_name is unique",
"parameters": [ "parameters": [
{ {
"description": "Suspend or unsuspend a user", "type": "string",
"name": "updateUserSuspend", "description": "User Name",
"in": "body", "name": "user_name",
"required": true, "in": "path",
"schema": { "required": true
"$ref": "#/definitions/handlers.UpdateUserSuspendReq"
}
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.UpdateUserSuspendRes" "$ref": "#/definitions/domain.Response"
} }
}, },
"400": { "400": {
"description": "Bad Request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
@ -1171,7 +1117,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/handlers.RegisterUserReq" "$ref": "#/definitions/domain.RegisterUserReq"
} }
} }
], ],
@ -1243,52 +1189,6 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/{tenant_slug}/user/search": {
"post": {
"description": "Search for user using name or phone",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Search for user using name or phone",
"parameters": [
{
"description": "Search for using his name or phone",
"name": "searchUserByNameOrPhone",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.SearchUserByNameOrPhoneReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UserProfileRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/{tenant_slug}/user/sendRegisterCode": { "/api/v1/{tenant_slug}/user/sendRegisterCode": {
"post": { "post": {
"description": "Send register code", "description": "Send register code",
@ -1380,6 +1280,102 @@ const docTemplate = `{
} }
} }
} }
},
"/api/v1/{tenant_slug}/user/verify-otp": {
"post": {
"description": "Verify OTP for registration or other actions",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Verify OTP",
"parameters": [
{
"description": "Verify OTP",
"name": "verifyOtp",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.VerifyOtpReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/{tenant_slug}/user/{user_name}/is-pending": {
"get": {
"description": "Returns whether the specified user has a status of \"pending\"",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Check if user status is pending",
"parameters": [
{
"type": "string",
"description": "User Name",
"name": "user_name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@ -1441,6 +1437,28 @@ const docTemplate = `{
} }
} }
}, },
"domain.OtpFor": {
"type": "string",
"enum": [
"reset",
"register"
],
"x-enum-varnames": [
"OtpReset",
"OtpRegister"
]
},
"domain.OtpMedium": {
"type": "string",
"enum": [
"email",
"sms"
],
"x-enum-varnames": [
"OtpMediumEmail",
"OtpMediumSms"
]
},
"domain.Pagination": { "domain.Pagination": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1458,6 +1476,72 @@ const docTemplate = `{
} }
} }
}, },
"domain.RegisterUserReq": {
"type": "object",
"properties": {
"age": {
"type": "integer"
},
"country": {
"type": "string"
},
"educationLevel": {
"type": "string"
},
"email": {
"type": "string"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"organizationID": {
"$ref": "#/definitions/domain.ValidInt64"
},
"otp": {
"type": "string"
},
"otpMedium": {
"$ref": "#/definitions/domain.OtpMedium"
},
"password": {
"type": "string"
},
"phoneNumber": {
"type": "string"
},
"preferredLanguage": {
"type": "string"
},
"region": {
"type": "string"
},
"role": {
"type": "string"
},
"userName": {
"type": "string"
}
}
},
"domain.Response": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"metadata": {},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.Role": { "domain.Role": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -1475,6 +1559,135 @@ const docTemplate = `{
"RoleSupport" "RoleSupport"
] ]
}, },
"domain.UserProfileResponse": {
"type": "object",
"properties": {
"age": {
"type": "integer"
},
"country": {
"type": "string"
},
"created_at": {
"type": "string"
},
"education_level": {
"type": "string"
},
"email": {
"type": "string"
},
"email_verified": {
"type": "boolean"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"organization_id": {
"type": "integer"
},
"phone_number": {
"type": "string"
},
"phone_verified": {
"type": "boolean"
},
"preferred_language": {
"type": "string"
},
"profile_completed": {
"type": "boolean"
},
"profile_picture_url": {
"type": "string"
},
"region": {
"type": "string"
},
"role": {
"$ref": "#/definitions/domain.Role"
},
"status": {
"$ref": "#/definitions/domain.UserStatus"
},
"updated_at": {
"type": "string"
},
"user_name": {
"type": "string"
}
}
},
"domain.UserStatus": {
"type": "string",
"enum": [
"PENDING",
"ACTIVE",
"SUSPENDED",
"DEACTIVATED"
],
"x-enum-varnames": [
"UserStatusPending",
"UserStatusActive",
"UserStatusSuspended",
"UserStatusDeactivated"
]
},
"domain.ValidInt64": {
"type": "object",
"properties": {
"valid": {
"type": "boolean"
},
"value": {
"type": "integer"
}
}
},
"domain.VerifyOtpReq": {
"type": "object",
"required": [
"otp",
"otp_for",
"otp_medium"
],
"properties": {
"email": {
"description": "Required if medium is email",
"type": "string"
},
"otp": {
"type": "string"
},
"otp_for": {
"$ref": "#/definitions/domain.OtpFor"
},
"otp_medium": {
"enum": [
"email",
"sms"
],
"allOf": [
{
"$ref": "#/definitions/domain.OtpMedium"
}
]
},
"phone_number": {
"description": "Required if medium is SMS",
"type": "string"
}
}
},
"handlers.AdminProfileRes": { "handlers.AdminProfileRes": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1690,39 +1903,6 @@ const docTemplate = `{
} }
} }
}, },
"handlers.RegisterUserReq": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"otp": {
"type": "string",
"example": "123456"
},
"password": {
"type": "string",
"example": "password123"
},
"phone_number": {
"type": "string",
"example": "1234567890"
},
"referral_code": {
"type": "string",
"example": "ABC123"
}
}
},
"handlers.ResetCodeReq": { "handlers.ResetCodeReq": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1773,80 +1953,6 @@ const docTemplate = `{
} }
} }
}, },
"handlers.UpdateUserSuspendReq": {
"type": "object",
"required": [
"user_id"
],
"properties": {
"suspended": {
"type": "boolean",
"example": true
},
"user_id": {
"type": "integer",
"example": 123
}
}
},
"handlers.UpdateUserSuspendRes": {
"type": "object",
"properties": {
"suspended": {
"type": "boolean"
},
"user_id": {
"type": "integer"
}
}
},
"handlers.UserProfileRes": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"email": {
"type": "string"
},
"email_verified": {
"type": "boolean"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"phone_verified": {
"type": "boolean"
},
"referral_code": {
"type": "string"
},
"role": {
"$ref": "#/definitions/domain.Role"
},
"suspended": {
"type": "boolean"
},
"suspended_at": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"handlers.loginAdminReq": { "handlers.loginAdminReq": {
"type": "object", "type": "object",
"required": [ "required": [
@ -1870,20 +1976,17 @@ const docTemplate = `{
"handlers.loginCustomerReq": { "handlers.loginCustomerReq": {
"type": "object", "type": "object",
"required": [ "required": [
"password" "password",
"user_name"
], ],
"properties": { "properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"password": { "password": {
"type": "string", "type": "string",
"example": "password123" "example": "password123"
}, },
"phone_number": { "user_name": {
"type": "string", "type": "string",
"example": "1234567890" "example": "johndoe"
} }
} }
}, },
@ -1951,27 +2054,6 @@ const docTemplate = `{
} }
} }
}, },
"handlers.updateManagerReq": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"example": 1
},
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"suspended": {
"type": "boolean",
"example": false
}
}
},
"response.APIResponse": { "response.APIResponse": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -385,58 +385,6 @@
} }
} }
}, },
"/api/v1/managers/{id}": {
"put": {
"description": "Update Managers",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"manager"
],
"summary": "Update Managers",
"parameters": [
{
"description": "Update Managers",
"name": "Managers",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.updateManagerReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/super-login": { "/api/v1/super-login": {
"post": { "post": {
"description": "Login super-admin", "description": "Login super-admin",
@ -753,7 +701,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.UserProfileRes" "$ref": "#/definitions/domain.UserProfileResponse"
} }
}, },
"400": { "400": {
@ -843,7 +791,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.UserProfileRes" "$ref": "#/definitions/domain.UserProfileResponse"
} }
}, },
"400": { "400": {
@ -867,9 +815,9 @@
} }
} }
}, },
"/api/v1/user/suspend": { "/api/v1/user/{user_name}/is-unique": {
"post": { "get": {
"description": "Suspend or unsuspend a user", "description": "Returns whether the specified user_name is available (unique)",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -879,35 +827,33 @@
"tags": [ "tags": [
"user" "user"
], ],
"summary": "Suspend or unsuspend a user", "summary": "Check if user_name is unique",
"parameters": [ "parameters": [
{ {
"description": "Suspend or unsuspend a user", "type": "string",
"name": "updateUserSuspend", "description": "User Name",
"in": "body", "name": "user_name",
"required": true, "in": "path",
"schema": { "required": true
"$ref": "#/definitions/handlers.UpdateUserSuspendReq"
}
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.UpdateUserSuspendRes" "$ref": "#/definitions/domain.Response"
} }
}, },
"400": { "400": {
"description": "Bad Request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
@ -1163,7 +1109,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/handlers.RegisterUserReq" "$ref": "#/definitions/domain.RegisterUserReq"
} }
} }
], ],
@ -1235,52 +1181,6 @@
} }
} }
}, },
"/api/v1/{tenant_slug}/user/search": {
"post": {
"description": "Search for user using name or phone",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Search for user using name or phone",
"parameters": [
{
"description": "Search for using his name or phone",
"name": "searchUserByNameOrPhone",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.SearchUserByNameOrPhoneReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UserProfileRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/{tenant_slug}/user/sendRegisterCode": { "/api/v1/{tenant_slug}/user/sendRegisterCode": {
"post": { "post": {
"description": "Send register code", "description": "Send register code",
@ -1372,6 +1272,102 @@
} }
} }
} }
},
"/api/v1/{tenant_slug}/user/verify-otp": {
"post": {
"description": "Verify OTP for registration or other actions",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Verify OTP",
"parameters": [
{
"description": "Verify OTP",
"name": "verifyOtp",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.VerifyOtpReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/{tenant_slug}/user/{user_name}/is-pending": {
"get": {
"description": "Returns whether the specified user has a status of \"pending\"",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Check if user status is pending",
"parameters": [
{
"type": "string",
"description": "User Name",
"name": "user_name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@ -1433,6 +1429,28 @@
} }
} }
}, },
"domain.OtpFor": {
"type": "string",
"enum": [
"reset",
"register"
],
"x-enum-varnames": [
"OtpReset",
"OtpRegister"
]
},
"domain.OtpMedium": {
"type": "string",
"enum": [
"email",
"sms"
],
"x-enum-varnames": [
"OtpMediumEmail",
"OtpMediumSms"
]
},
"domain.Pagination": { "domain.Pagination": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1450,6 +1468,72 @@
} }
} }
}, },
"domain.RegisterUserReq": {
"type": "object",
"properties": {
"age": {
"type": "integer"
},
"country": {
"type": "string"
},
"educationLevel": {
"type": "string"
},
"email": {
"type": "string"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"organizationID": {
"$ref": "#/definitions/domain.ValidInt64"
},
"otp": {
"type": "string"
},
"otpMedium": {
"$ref": "#/definitions/domain.OtpMedium"
},
"password": {
"type": "string"
},
"phoneNumber": {
"type": "string"
},
"preferredLanguage": {
"type": "string"
},
"region": {
"type": "string"
},
"role": {
"type": "string"
},
"userName": {
"type": "string"
}
}
},
"domain.Response": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"metadata": {},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.Role": { "domain.Role": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -1467,6 +1551,135 @@
"RoleSupport" "RoleSupport"
] ]
}, },
"domain.UserProfileResponse": {
"type": "object",
"properties": {
"age": {
"type": "integer"
},
"country": {
"type": "string"
},
"created_at": {
"type": "string"
},
"education_level": {
"type": "string"
},
"email": {
"type": "string"
},
"email_verified": {
"type": "boolean"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"organization_id": {
"type": "integer"
},
"phone_number": {
"type": "string"
},
"phone_verified": {
"type": "boolean"
},
"preferred_language": {
"type": "string"
},
"profile_completed": {
"type": "boolean"
},
"profile_picture_url": {
"type": "string"
},
"region": {
"type": "string"
},
"role": {
"$ref": "#/definitions/domain.Role"
},
"status": {
"$ref": "#/definitions/domain.UserStatus"
},
"updated_at": {
"type": "string"
},
"user_name": {
"type": "string"
}
}
},
"domain.UserStatus": {
"type": "string",
"enum": [
"PENDING",
"ACTIVE",
"SUSPENDED",
"DEACTIVATED"
],
"x-enum-varnames": [
"UserStatusPending",
"UserStatusActive",
"UserStatusSuspended",
"UserStatusDeactivated"
]
},
"domain.ValidInt64": {
"type": "object",
"properties": {
"valid": {
"type": "boolean"
},
"value": {
"type": "integer"
}
}
},
"domain.VerifyOtpReq": {
"type": "object",
"required": [
"otp",
"otp_for",
"otp_medium"
],
"properties": {
"email": {
"description": "Required if medium is email",
"type": "string"
},
"otp": {
"type": "string"
},
"otp_for": {
"$ref": "#/definitions/domain.OtpFor"
},
"otp_medium": {
"enum": [
"email",
"sms"
],
"allOf": [
{
"$ref": "#/definitions/domain.OtpMedium"
}
]
},
"phone_number": {
"description": "Required if medium is SMS",
"type": "string"
}
}
},
"handlers.AdminProfileRes": { "handlers.AdminProfileRes": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1682,39 +1895,6 @@
} }
} }
}, },
"handlers.RegisterUserReq": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"otp": {
"type": "string",
"example": "123456"
},
"password": {
"type": "string",
"example": "password123"
},
"phone_number": {
"type": "string",
"example": "1234567890"
},
"referral_code": {
"type": "string",
"example": "ABC123"
}
}
},
"handlers.ResetCodeReq": { "handlers.ResetCodeReq": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1765,80 +1945,6 @@
} }
} }
}, },
"handlers.UpdateUserSuspendReq": {
"type": "object",
"required": [
"user_id"
],
"properties": {
"suspended": {
"type": "boolean",
"example": true
},
"user_id": {
"type": "integer",
"example": 123
}
}
},
"handlers.UpdateUserSuspendRes": {
"type": "object",
"properties": {
"suspended": {
"type": "boolean"
},
"user_id": {
"type": "integer"
}
}
},
"handlers.UserProfileRes": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"email": {
"type": "string"
},
"email_verified": {
"type": "boolean"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"phone_verified": {
"type": "boolean"
},
"referral_code": {
"type": "string"
},
"role": {
"$ref": "#/definitions/domain.Role"
},
"suspended": {
"type": "boolean"
},
"suspended_at": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"handlers.loginAdminReq": { "handlers.loginAdminReq": {
"type": "object", "type": "object",
"required": [ "required": [
@ -1862,20 +1968,17 @@
"handlers.loginCustomerReq": { "handlers.loginCustomerReq": {
"type": "object", "type": "object",
"required": [ "required": [
"password" "password",
"user_name"
], ],
"properties": { "properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"password": { "password": {
"type": "string", "type": "string",
"example": "password123" "example": "password123"
}, },
"phone_number": { "user_name": {
"type": "string", "type": "string",
"example": "1234567890" "example": "johndoe"
} }
} }
}, },
@ -1943,27 +2046,6 @@
} }
} }
}, },
"handlers.updateManagerReq": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"example": 1
},
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"suspended": {
"type": "boolean",
"example": false
}
}
},
"response.APIResponse": { "response.APIResponse": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -37,6 +37,22 @@ definitions:
pagination: pagination:
$ref: '#/definitions/domain.Pagination' $ref: '#/definitions/domain.Pagination'
type: object type: object
domain.OtpFor:
enum:
- reset
- register
type: string
x-enum-varnames:
- OtpReset
- OtpRegister
domain.OtpMedium:
enum:
- email
- sms
type: string
x-enum-varnames:
- OtpMediumEmail
- OtpMediumSms
domain.Pagination: domain.Pagination:
properties: properties:
current_page: current_page:
@ -48,6 +64,50 @@ definitions:
total_pages: total_pages:
type: integer type: integer
type: object type: object
domain.RegisterUserReq:
properties:
age:
type: integer
country:
type: string
educationLevel:
type: string
email:
type: string
firstName:
type: string
lastName:
type: string
organizationID:
$ref: '#/definitions/domain.ValidInt64'
otp:
type: string
otpMedium:
$ref: '#/definitions/domain.OtpMedium'
password:
type: string
phoneNumber:
type: string
preferredLanguage:
type: string
region:
type: string
role:
type: string
userName:
type: string
type: object
domain.Response:
properties:
data: {}
message:
type: string
metadata: {}
status_code:
type: integer
success:
type: boolean
type: object
domain.Role: domain.Role:
enum: enum:
- super_admin - super_admin
@ -62,6 +122,93 @@ definitions:
- RoleStudent - RoleStudent
- RoleInstructor - RoleInstructor
- RoleSupport - RoleSupport
domain.UserProfileResponse:
properties:
age:
type: integer
country:
type: string
created_at:
type: string
education_level:
type: string
email:
type: string
email_verified:
type: boolean
first_name:
type: string
id:
type: integer
last_login:
type: string
last_name:
type: string
organization_id:
type: integer
phone_number:
type: string
phone_verified:
type: boolean
preferred_language:
type: string
profile_completed:
type: boolean
profile_picture_url:
type: string
region:
type: string
role:
$ref: '#/definitions/domain.Role'
status:
$ref: '#/definitions/domain.UserStatus'
updated_at:
type: string
user_name:
type: string
type: object
domain.UserStatus:
enum:
- PENDING
- ACTIVE
- SUSPENDED
- DEACTIVATED
type: string
x-enum-varnames:
- UserStatusPending
- UserStatusActive
- UserStatusSuspended
- UserStatusDeactivated
domain.ValidInt64:
properties:
valid:
type: boolean
value:
type: integer
type: object
domain.VerifyOtpReq:
properties:
email:
description: Required if medium is email
type: string
otp:
type: string
otp_for:
$ref: '#/definitions/domain.OtpFor'
otp_medium:
allOf:
- $ref: '#/definitions/domain.OtpMedium'
enum:
- email
- sms
phone_number:
description: Required if medium is SMS
type: string
required:
- otp
- otp_for
- otp_medium
type: object
handlers.AdminProfileRes: handlers.AdminProfileRes:
properties: properties:
created_at: created_at:
@ -206,30 +353,6 @@ definitions:
example: "1234567890" example: "1234567890"
type: string type: string
type: object type: object
handlers.RegisterUserReq:
properties:
email:
example: john.doe@example.com
type: string
first_name:
example: John
type: string
last_name:
example: Doe
type: string
otp:
example: "123456"
type: string
password:
example: password123
type: string
phone_number:
example: "1234567890"
type: string
referral_code:
example: ABC123
type: string
type: object
handlers.ResetCodeReq: handlers.ResetCodeReq:
properties: properties:
email: email:
@ -265,55 +388,6 @@ definitions:
role: role:
$ref: '#/definitions/domain.Role' $ref: '#/definitions/domain.Role'
type: object type: object
handlers.UpdateUserSuspendReq:
properties:
suspended:
example: true
type: boolean
user_id:
example: 123
type: integer
required:
- user_id
type: object
handlers.UpdateUserSuspendRes:
properties:
suspended:
type: boolean
user_id:
type: integer
type: object
handlers.UserProfileRes:
properties:
created_at:
type: string
email:
type: string
email_verified:
type: boolean
first_name:
type: string
id:
type: integer
last_login:
type: string
last_name:
type: string
phone_number:
type: string
phone_verified:
type: boolean
referral_code:
type: string
role:
$ref: '#/definitions/domain.Role'
suspended:
type: boolean
suspended_at:
type: string
updated_at:
type: string
type: object
handlers.loginAdminReq: handlers.loginAdminReq:
properties: properties:
email: email:
@ -330,17 +404,15 @@ definitions:
type: object type: object
handlers.loginCustomerReq: handlers.loginCustomerReq:
properties: properties:
email:
example: john.doe@example.com
type: string
password: password:
example: password123 example: password123
type: string type: string
phone_number: user_name:
example: "1234567890" example: johndoe
type: string type: string
required: required:
- password - password
- user_name
type: object type: object
handlers.loginCustomerRes: handlers.loginCustomerRes:
properties: properties:
@ -386,21 +458,6 @@ definitions:
example: false example: false
type: boolean type: boolean
type: object type: object
handlers.updateManagerReq:
properties:
company_id:
example: 1
type: integer
first_name:
example: John
type: string
last_name:
example: Doe
type: string
suspended:
example: false
type: boolean
type: object
response.APIResponse: response.APIResponse:
properties: properties:
data: {} data: {}
@ -505,6 +562,39 @@ paths:
summary: Login customer summary: Login customer
tags: tags:
- auth - auth
/api/v1/{tenant_slug}/user/{user_name}/is-pending:
get:
consumes:
- application/json
description: Returns whether the specified user has a status of "pending"
parameters:
- description: User Name
in: path
name: user_name
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Check if user status is pending
tags:
- user
/api/v1/{tenant_slug}/user/admin-profile: /api/v1/{tenant_slug}/user/admin-profile:
get: get:
consumes: consumes:
@ -596,7 +686,7 @@ paths:
name: registerUser name: registerUser
required: true required: true
schema: schema:
$ref: '#/definitions/handlers.RegisterUserReq' $ref: '#/definitions/domain.RegisterUserReq'
produces: produces:
- application/json - application/json
responses: responses:
@ -645,36 +735,6 @@ paths:
summary: Reset tenant password summary: Reset tenant password
tags: tags:
- user - user
/api/v1/{tenant_slug}/user/search:
post:
consumes:
- application/json
description: Search for user using name or phone
parameters:
- description: Search for using his name or phone
in: body
name: searchUserByNameOrPhone
required: true
schema:
$ref: '#/definitions/handlers.SearchUserByNameOrPhoneReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.UserProfileRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Search for user using name or phone
tags:
- user
/api/v1/{tenant_slug}/user/sendRegisterCode: /api/v1/{tenant_slug}/user/sendRegisterCode:
post: post:
consumes: consumes:
@ -735,6 +795,36 @@ paths:
summary: Send reset code summary: Send reset code
tags: tags:
- user - user
/api/v1/{tenant_slug}/user/verify-otp:
post:
consumes:
- application/json
description: Verify OTP for registration or other actions
parameters:
- description: Verify OTP
in: body
name: verifyOtp
required: true
schema:
$ref: '#/definitions/domain.VerifyOtpReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Verify OTP
tags:
- user
/api/v1/admin: /api/v1/admin:
get: get:
consumes: consumes:
@ -980,40 +1070,6 @@ paths:
summary: Retrieve application logs with filtering and pagination summary: Retrieve application logs with filtering and pagination
tags: tags:
- Logs - Logs
/api/v1/managers/{id}:
put:
consumes:
- application/json
description: Update Managers
parameters:
- description: Update Managers
in: body
name: Managers
required: true
schema:
$ref: '#/definitions/handlers.updateManagerReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Update Managers
tags:
- manager
/api/v1/super-login: /api/v1/super-login:
post: post:
consumes: consumes:
@ -1144,6 +1200,35 @@ paths:
summary: Check if phone number or email exist summary: Check if phone number or email exist
tags: tags:
- user - user
/api/v1/user/{user_name}/is-unique:
get:
consumes:
- application/json
description: Returns whether the specified user_name is available (unique)
parameters:
- description: User Name
in: path
name: user_name
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Check if user_name is unique
tags:
- user
/api/v1/user/delete/{id}: /api/v1/user/delete/{id}:
delete: delete:
consumes: consumes:
@ -1221,7 +1306,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handlers.UserProfileRes' $ref: '#/definitions/domain.UserProfileResponse'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1280,7 +1365,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handlers.UserProfileRes' $ref: '#/definitions/domain.UserProfileResponse'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1296,36 +1381,6 @@ paths:
summary: Get user by id summary: Get user by id
tags: tags:
- user - user
/api/v1/user/suspend:
post:
consumes:
- application/json
description: Suspend or unsuspend a user
parameters:
- description: Suspend or unsuspend a user
in: body
name: updateUserSuspend
required: true
schema:
$ref: '#/definitions/handlers.UpdateUserSuspendReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.UpdateUserSuspendRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Suspend or unsuspend a user
tags:
- user
securityDefinitions: securityDefinitions:
Bearer: Bearer:
in: header in: header

View File

@ -30,7 +30,6 @@ type AssessmentSubmission struct {
type Course struct { type Course struct {
ID int64 `json:"id"` ID int64 `json:"id"`
OrganizationID int64 `json:"organization_id"`
InstructorID int64 `json:"instructor_id"` InstructorID int64 `json:"instructor_id"`
Title string `json:"title"` Title string `json:"title"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
@ -97,26 +96,9 @@ type Notification struct {
ReadAt pgtype.Timestamptz `json:"read_at"` ReadAt pgtype.Timestamptz `json:"read_at"`
} }
type Organization struct {
ID int64 `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
OwnerID int64 `json:"owner_id"`
IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type OrganizationSetting struct {
OrganizationID int64 `json:"organization_id"`
Key string `json:"key"`
Value string `json:"value"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type Otp struct { type Otp struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UserName string `json:"user_name"`
SentTo string `json:"sent_to"` SentTo string `json:"sent_to"`
Medium string `json:"medium"` Medium string `json:"medium"`
OtpFor string `json:"otp_for"` OtpFor string `json:"otp_for"`
@ -127,19 +109,6 @@ type Otp struct {
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
} }
type ReferralCode struct {
ID int64 `json:"id"`
Code string `json:"code"`
ReferrerID int64 `json:"referrer_id"`
IsActive bool `json:"is_active"`
MaxUses pgtype.Int4 `json:"max_uses"`
CurrentUses int32 `json:"current_uses"`
IncentiveType string `json:"incentive_type"`
IncentiveValue pgtype.Text `json:"incentive_value"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type RefreshToken struct { type RefreshToken struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
@ -163,31 +132,25 @@ type ReportedIssue struct {
} }
type User struct { type User struct {
ID int64 `json:"id"` ID int64 `json:"id"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
NickName pgtype.Text `json:"nick_name"` UserName string `json:"user_name"`
Email pgtype.Text `json:"email"` Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"` PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"` Role string `json:"role"`
Password []byte `json:"password"` Password []byte `json:"password"`
Age pgtype.Int4 `json:"age"` Age pgtype.Int4 `json:"age"`
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
Suspended bool `json:"suspended"` Status string `json:"status"`
SuspendedAt pgtype.Timestamptz `json:"suspended_at"` LastLogin pgtype.Timestamptz `json:"last_login"`
OrganizationID pgtype.Int8 `json:"organization_id"` ProfileCompleted bool `json:"profile_completed"`
CreatedAt pgtype.Timestamptz `json:"created_at"` ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` PreferredLanguage pgtype.Text `json:"preferred_language"`
} CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
type UserReferral struct {
ID int64 `json:"id"`
ReferrerID int64 `json:"referrer_id"`
ReferredUserID int64 `json:"referred_user_id"`
ReferralCodeID int64 `json:"referral_code_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
} }

View File

@ -12,11 +12,12 @@ import (
) )
const CreateOtp = `-- name: CreateOtp :exec const CreateOtp = `-- name: CreateOtp :exec
INSERT INTO otps (sent_to, medium, otp_for, otp, used, created_at, expires_at) INSERT INTO otps (user_name, sent_to, medium, otp_for, otp, used, created_at, expires_at)
VALUES ($1, $2, $3, $4, FALSE, $5, $6) VALUES ($1, $2, $3, $4, $5, FALSE, $6, $7)
` `
type CreateOtpParams struct { type CreateOtpParams struct {
UserName string `json:"user_name"`
SentTo string `json:"sent_to"` SentTo string `json:"sent_to"`
Medium string `json:"medium"` Medium string `json:"medium"`
OtpFor string `json:"otp_for"` OtpFor string `json:"otp_for"`
@ -27,6 +28,7 @@ type CreateOtpParams struct {
func (q *Queries) CreateOtp(ctx context.Context, arg CreateOtpParams) error { func (q *Queries) CreateOtp(ctx context.Context, arg CreateOtpParams) error {
_, err := q.db.Exec(ctx, CreateOtp, _, err := q.db.Exec(ctx, CreateOtp,
arg.UserName,
arg.SentTo, arg.SentTo,
arg.Medium, arg.Medium,
arg.OtpFor, arg.OtpFor,
@ -38,20 +40,15 @@ func (q *Queries) CreateOtp(ctx context.Context, arg CreateOtpParams) error {
} }
const GetOtp = `-- name: GetOtp :one const GetOtp = `-- name: GetOtp :one
SELECT id, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at SELECT id, user_name, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
FROM otps FROM otps
WHERE sent_to = $1 AND otp_for = $2 AND medium = $3 WHERE user_name = $1
ORDER BY created_at DESC LIMIT 1 ORDER BY created_at DESC LIMIT 1
` `
type GetOtpParams struct {
SentTo string `json:"sent_to"`
OtpFor string `json:"otp_for"`
Medium string `json:"medium"`
}
type GetOtpRow struct { type GetOtpRow struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UserName string `json:"user_name"`
SentTo string `json:"sent_to"` SentTo string `json:"sent_to"`
Medium string `json:"medium"` Medium string `json:"medium"`
OtpFor string `json:"otp_for"` OtpFor string `json:"otp_for"`
@ -62,11 +59,12 @@ type GetOtpRow struct {
ExpiresAt pgtype.Timestamptz `json:"expires_at"` ExpiresAt pgtype.Timestamptz `json:"expires_at"`
} }
func (q *Queries) GetOtp(ctx context.Context, arg GetOtpParams) (GetOtpRow, error) { func (q *Queries) GetOtp(ctx context.Context, userName string) (GetOtpRow, error) {
row := q.db.QueryRow(ctx, GetOtp, arg.SentTo, arg.OtpFor, arg.Medium) row := q.db.QueryRow(ctx, GetOtp, userName)
var i GetOtpRow var i GetOtpRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.UserName,
&i.SentTo, &i.SentTo,
&i.Medium, &i.Medium,
&i.OtpFor, &i.OtpFor,
@ -94,3 +92,26 @@ func (q *Queries) MarkOtpAsUsed(ctx context.Context, arg MarkOtpAsUsedParams) er
_, err := q.db.Exec(ctx, MarkOtpAsUsed, arg.ID, arg.UsedAt) _, err := q.db.Exec(ctx, MarkOtpAsUsed, arg.ID, arg.UsedAt)
return err return err
} }
const UpdateExpiredOtp = `-- name: UpdateExpiredOtp :exec
UPDATE otps
SET
otp = $2,
used = FALSE,
used_at = NULL,
expires_at = $3
WHERE
user_name = $1
AND expires_at <= NOW()
`
type UpdateExpiredOtpParams struct {
UserName string `json:"user_name"`
Otp string `json:"otp"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
}
func (q *Queries) UpdateExpiredOtp(ctx context.Context, arg UpdateExpiredOtpParams) error {
_, err := q.db.Exec(ctx, UpdateExpiredOtp, arg.UserName, arg.Otp, arg.ExpiresAt)
return err
}

View File

@ -12,26 +12,22 @@ import (
) )
const CheckPhoneEmailExist = `-- name: CheckPhoneEmailExist :one const CheckPhoneEmailExist = `-- name: CheckPhoneEmailExist :one
SELECT EXISTS ( SELECT
EXISTS (
SELECT 1 SELECT 1
FROM users FROM users u1
WHERE users.phone_number = $1 WHERE u1.phone_number = $1
AND users.phone_number IS NOT NULL
AND users.organization_id = $2
) AS phone_exists, ) AS phone_exists,
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM users FROM users u2
WHERE users.email = $3 WHERE u2.email = $2
AND users.email IS NOT NULL
AND users.organization_id = $2
) AS email_exists ) AS email_exists
` `
type CheckPhoneEmailExistParams struct { type CheckPhoneEmailExistParams struct {
PhoneNumber pgtype.Text `json:"phone_number"` PhoneNumber pgtype.Text `json:"phone_number"`
OrganizationID pgtype.Int8 `json:"organization_id"` Email pgtype.Text `json:"email"`
Email pgtype.Text `json:"email"`
} }
type CheckPhoneEmailExistRow struct { type CheckPhoneEmailExistRow struct {
@ -40,7 +36,7 @@ type CheckPhoneEmailExistRow struct {
} }
func (q *Queries) CheckPhoneEmailExist(ctx context.Context, arg CheckPhoneEmailExistParams) (CheckPhoneEmailExistRow, error) { func (q *Queries) CheckPhoneEmailExist(ctx context.Context, arg CheckPhoneEmailExistParams) (CheckPhoneEmailExistRow, error) {
row := q.db.QueryRow(ctx, CheckPhoneEmailExist, arg.PhoneNumber, arg.OrganizationID, arg.Email) row := q.db.QueryRow(ctx, CheckPhoneEmailExist, arg.PhoneNumber, arg.Email)
var i CheckPhoneEmailExistRow var i CheckPhoneEmailExistRow
err := row.Scan(&i.PhoneExists, &i.EmailExists) err := row.Scan(&i.PhoneExists, &i.EmailExists)
return i, err return i, err
@ -48,49 +44,48 @@ func (q *Queries) CheckPhoneEmailExist(ctx context.Context, arg CheckPhoneEmailE
const CreateUser = `-- name: CreateUser :one const CreateUser = `-- name: CreateUser :one
INSERT INTO users ( INSERT INTO users (
first_name,
last_name,
nick_name,
email,
phone_number,
role,
password,
age,
education_level,
country,
region,
email_verified,
phone_verified,
suspended,
suspended_at,
organization_id,
created_at,
updated_at
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14,
$15,
$16,
$17,
$18
)
RETURNING id,
first_name, first_name,
last_name, last_name,
nick_name, user_name,
email,
phone_number,
role,
password,
age,
education_level,
country,
region,
email_verified,
phone_verified,
status,
profile_completed,
preferred_language,
updated_at
)
VALUES (
$1, -- first_name
$2, -- last_name
$3, -- user_name
$4, -- email
$5, -- phone_number
$6, -- role
$7, -- password (BYTEA)
$8, -- age
$9, -- education_level
$10, -- country
$11, -- region
$12, -- email_verified
$13, -- phone_verified
$14, -- status (PENDING | ACTIVE)
$15, -- profile_completed
$16, -- preferred_language
CURRENT_TIMESTAMP
)
RETURNING
id,
first_name,
last_name,
user_name,
email, email,
phone_number, phone_number,
role, role,
@ -100,60 +95,58 @@ RETURNING id,
region, region,
email_verified, email_verified,
phone_verified, phone_verified,
status,
profile_completed,
preferred_language,
created_at, created_at,
updated_at, updated_at
suspended,
suspended_at,
organization_id
` `
type CreateUserParams struct { type CreateUserParams struct {
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
NickName pgtype.Text `json:"nick_name"` UserName string `json:"user_name"`
Email pgtype.Text `json:"email"` Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"` PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"` Role string `json:"role"`
Password []byte `json:"password"` Password []byte `json:"password"`
Age pgtype.Int4 `json:"age"` Age pgtype.Int4 `json:"age"`
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
Suspended bool `json:"suspended"` Status string `json:"status"`
SuspendedAt pgtype.Timestamptz `json:"suspended_at"` ProfileCompleted bool `json:"profile_completed"`
OrganizationID pgtype.Int8 `json:"organization_id"` PreferredLanguage pgtype.Text `json:"preferred_language"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
type CreateUserRow struct { type CreateUserRow struct {
ID int64 `json:"id"` ID int64 `json:"id"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
NickName pgtype.Text `json:"nick_name"` UserName string `json:"user_name"`
Email pgtype.Text `json:"email"` Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"` PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"` Role string `json:"role"`
Age pgtype.Int4 `json:"age"` Age pgtype.Int4 `json:"age"`
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
CreatedAt pgtype.Timestamptz `json:"created_at"` Status string `json:"status"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` ProfileCompleted bool `json:"profile_completed"`
Suspended bool `json:"suspended"` PreferredLanguage pgtype.Text `json:"preferred_language"`
SuspendedAt pgtype.Timestamptz `json:"suspended_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
OrganizationID pgtype.Int8 `json:"organization_id"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) { func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
row := q.db.QueryRow(ctx, CreateUser, row := q.db.QueryRow(ctx, CreateUser,
arg.FirstName, arg.FirstName,
arg.LastName, arg.LastName,
arg.NickName, arg.UserName,
arg.Email, arg.Email,
arg.PhoneNumber, arg.PhoneNumber,
arg.Role, arg.Role,
@ -164,18 +157,16 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
arg.Region, arg.Region,
arg.EmailVerified, arg.EmailVerified,
arg.PhoneVerified, arg.PhoneVerified,
arg.Suspended, arg.Status,
arg.SuspendedAt, arg.ProfileCompleted,
arg.OrganizationID, arg.PreferredLanguage,
arg.CreatedAt,
arg.UpdatedAt,
) )
var i CreateUserRow var i CreateUserRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.FirstName, &i.FirstName,
&i.LastName, &i.LastName,
&i.NickName, &i.UserName,
&i.Email, &i.Email,
&i.PhoneNumber, &i.PhoneNumber,
&i.Role, &i.Role,
@ -185,11 +176,11 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
&i.Region, &i.Region,
&i.EmailVerified, &i.EmailVerified,
&i.PhoneVerified, &i.PhoneVerified,
&i.Status,
&i.ProfileCompleted,
&i.PreferredLanguage,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.Suspended,
&i.SuspendedAt,
&i.OrganizationID,
) )
return i, err return i, err
} }
@ -210,7 +201,7 @@ SELECT
id, id,
first_name, first_name,
last_name, last_name,
nick_name, user_name,
email, email,
phone_number, phone_number,
role, role,
@ -220,77 +211,71 @@ SELECT
region, region,
email_verified, email_verified,
phone_verified, phone_verified,
status,
profile_completed,
preferred_language,
created_at, created_at,
updated_at, updated_at
suspended,
suspended_at,
organization_id
FROM users FROM users
WHERE ( WHERE (
role = $1 role = $1 OR $1 IS NULL
OR $1 IS NULL
) )
AND ( AND (
organization_id = $2 first_name ILIKE '%' || $2 || '%'
OR $2 IS NULL OR last_name ILIKE '%' || $2 || '%'
OR phone_number ILIKE '%' || $2 || '%'
OR email ILIKE '%' || $2 || '%'
OR $2 IS NULL
) )
AND ( AND (
first_name ILIKE '%' || $3 || '%' created_at >= $3
OR last_name ILIKE '%' || $3 || '%'
OR phone_number ILIKE '%' || $3 || '%'
OR $3 IS NULL OR $3 IS NULL
) )
AND ( AND (
created_at > $4 created_at <= $4
OR $4 IS NULL OR $4 IS NULL
) )
AND ( LIMIT $6
created_at < $5 OFFSET $5
OR $5 IS NULL
)
LIMIT $7
OFFSET $6
` `
type GetAllUsersParams struct { type GetAllUsersParams struct {
Role string `json:"role"` Role string `json:"role"`
OrganizationID pgtype.Int8 `json:"organization_id"` Query pgtype.Text `json:"query"`
Query pgtype.Text `json:"query"` CreatedAfter pgtype.Timestamptz `json:"created_after"`
CreatedBefore pgtype.Timestamptz `json:"created_before"` CreatedBefore pgtype.Timestamptz `json:"created_before"`
CreatedAfter pgtype.Timestamptz `json:"created_after"` Offset pgtype.Int4 `json:"offset"`
Offset pgtype.Int4 `json:"offset"` Limit pgtype.Int4 `json:"limit"`
Limit pgtype.Int4 `json:"limit"`
} }
type GetAllUsersRow struct { type GetAllUsersRow struct {
TotalCount int64 `json:"total_count"` TotalCount int64 `json:"total_count"`
ID int64 `json:"id"` ID int64 `json:"id"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
NickName pgtype.Text `json:"nick_name"` UserName string `json:"user_name"`
Email pgtype.Text `json:"email"` Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"` PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"` Role string `json:"role"`
Age pgtype.Int4 `json:"age"` Age pgtype.Int4 `json:"age"`
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
CreatedAt pgtype.Timestamptz `json:"created_at"` Status string `json:"status"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` ProfileCompleted bool `json:"profile_completed"`
Suspended bool `json:"suspended"` PreferredLanguage pgtype.Text `json:"preferred_language"`
SuspendedAt pgtype.Timestamptz `json:"suspended_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
OrganizationID pgtype.Int8 `json:"organization_id"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]GetAllUsersRow, error) { func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]GetAllUsersRow, error) {
rows, err := q.db.Query(ctx, GetAllUsers, rows, err := q.db.Query(ctx, GetAllUsers,
arg.Role, arg.Role,
arg.OrganizationID,
arg.Query, arg.Query,
arg.CreatedBefore,
arg.CreatedAfter, arg.CreatedAfter,
arg.CreatedBefore,
arg.Offset, arg.Offset,
arg.Limit, arg.Limit,
) )
@ -306,7 +291,7 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
&i.ID, &i.ID,
&i.FirstName, &i.FirstName,
&i.LastName, &i.LastName,
&i.NickName, &i.UserName,
&i.Email, &i.Email,
&i.PhoneNumber, &i.PhoneNumber,
&i.Role, &i.Role,
@ -316,11 +301,11 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
&i.Region, &i.Region,
&i.EmailVerified, &i.EmailVerified,
&i.PhoneVerified, &i.PhoneVerified,
&i.Status,
&i.ProfileCompleted,
&i.PreferredLanguage,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.Suspended,
&i.SuspendedAt,
&i.OrganizationID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -332,60 +317,14 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
return items, nil return items, nil
} }
const GetOwnerByOrganizationID = `-- name: GetOwnerByOrganizationID :one
SELECT users.id, users.first_name, users.last_name, users.nick_name, users.email, users.phone_number, users.role, users.password, users.age, users.education_level, users.country, users.region, users.email_verified, users.phone_verified, users.suspended, users.suspended_at, users.organization_id, users.created_at, users.updated_at
FROM organizations
JOIN users ON organizations.owner_id = users.id
WHERE organizations.id = $1
`
func (q *Queries) GetOwnerByOrganizationID(ctx context.Context, id int64) (User, error) {
row := q.db.QueryRow(ctx, GetOwnerByOrganizationID, id)
var i User
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.NickName,
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.Password,
&i.Age,
&i.EducationLevel,
&i.Country,
&i.Region,
&i.EmailVerified,
&i.PhoneVerified,
&i.Suspended,
&i.SuspendedAt,
&i.OrganizationID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetTotalUsers = `-- name: GetTotalUsers :one const GetTotalUsers = `-- name: GetTotalUsers :one
SELECT COUNT(*) SELECT COUNT(*)
FROM users FROM users
wHERE ( WHERE (role = $1 OR $1 IS NULL)
role = $1
OR $1 IS NULL
)
AND (
organization_id = $2
OR $2 IS NULL
)
` `
type GetTotalUsersParams struct { func (q *Queries) GetTotalUsers(ctx context.Context, role string) (int64, error) {
Role string `json:"role"` row := q.db.QueryRow(ctx, GetTotalUsers, role)
OrganizationID pgtype.Int8 `json:"organization_id"`
}
func (q *Queries) GetTotalUsers(ctx context.Context, arg GetTotalUsersParams) (int64, error) {
row := q.db.QueryRow(ctx, GetTotalUsers, arg.Role, arg.OrganizationID)
var count int64 var count int64
err := row.Scan(&count) err := row.Scan(&count)
return count, err return count, err
@ -396,67 +335,67 @@ SELECT
id, id,
first_name, first_name,
last_name, last_name,
nick_name, user_name,
email, email,
phone_number, phone_number,
role, role,
password, -- added this line password,
age, age,
education_level, education_level,
country, country,
region, region,
email_verified, email_verified,
phone_verified, phone_verified,
status,
profile_completed,
last_login,
profile_picture_url,
preferred_language,
created_at, created_at,
updated_at, updated_at
suspended,
suspended_at,
organization_id
FROM users FROM users
WHERE organization_id = $3 WHERE (email = $1 AND $1 IS NOT NULL)
AND (
(email = $1 AND $1 IS NOT NULL)
OR (phone_number = $2 AND $2 IS NOT NULL) OR (phone_number = $2 AND $2 IS NOT NULL)
)
LIMIT 1 LIMIT 1
` `
type GetUserByEmailPhoneParams struct { type GetUserByEmailPhoneParams struct {
Email pgtype.Text `json:"email"` Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"` PhoneNumber pgtype.Text `json:"phone_number"`
OrganizationID pgtype.Int8 `json:"organization_id"`
} }
type GetUserByEmailPhoneRow struct { type GetUserByEmailPhoneRow struct {
ID int64 `json:"id"` ID int64 `json:"id"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
NickName pgtype.Text `json:"nick_name"` UserName string `json:"user_name"`
Email pgtype.Text `json:"email"` Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"` PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"` Role string `json:"role"`
Password []byte `json:"password"` Password []byte `json:"password"`
Age pgtype.Int4 `json:"age"` Age pgtype.Int4 `json:"age"`
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
CreatedAt pgtype.Timestamptz `json:"created_at"` Status string `json:"status"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` ProfileCompleted bool `json:"profile_completed"`
Suspended bool `json:"suspended"` LastLogin pgtype.Timestamptz `json:"last_login"`
SuspendedAt pgtype.Timestamptz `json:"suspended_at"` ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
OrganizationID pgtype.Int8 `json:"organization_id"` PreferredLanguage pgtype.Text `json:"preferred_language"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPhoneParams) (GetUserByEmailPhoneRow, error) { func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPhoneParams) (GetUserByEmailPhoneRow, error) {
row := q.db.QueryRow(ctx, GetUserByEmailPhone, arg.Email, arg.PhoneNumber, arg.OrganizationID) row := q.db.QueryRow(ctx, GetUserByEmailPhone, arg.Email, arg.PhoneNumber)
var i GetUserByEmailPhoneRow var i GetUserByEmailPhoneRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.FirstName, &i.FirstName,
&i.LastName, &i.LastName,
&i.NickName, &i.UserName,
&i.Email, &i.Email,
&i.PhoneNumber, &i.PhoneNumber,
&i.Role, &i.Role,
@ -467,17 +406,19 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
&i.Region, &i.Region,
&i.EmailVerified, &i.EmailVerified,
&i.PhoneVerified, &i.PhoneVerified,
&i.Status,
&i.ProfileCompleted,
&i.LastLogin,
&i.ProfilePictureUrl,
&i.PreferredLanguage,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.Suspended,
&i.SuspendedAt,
&i.OrganizationID,
) )
return i, err return i, err
} }
const GetUserByID = `-- name: GetUserByID :one const GetUserByID = `-- name: GetUserByID :one
SELECT id, first_name, last_name, nick_name, email, phone_number, role, password, age, education_level, country, region, email_verified, phone_verified, suspended, suspended_at, organization_id, created_at, updated_at SELECT id, first_name, last_name, user_name, email, phone_number, role, password, age, education_level, country, region, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at
FROM users FROM users
WHERE id = $1 WHERE id = $1
` `
@ -489,7 +430,7 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
&i.ID, &i.ID,
&i.FirstName, &i.FirstName,
&i.LastName, &i.LastName,
&i.NickName, &i.UserName,
&i.Email, &i.Email,
&i.PhoneNumber, &i.PhoneNumber,
&i.Role, &i.Role,
@ -500,20 +441,133 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
&i.Region, &i.Region,
&i.EmailVerified, &i.EmailVerified,
&i.PhoneVerified, &i.PhoneVerified,
&i.Suspended, &i.Status,
&i.SuspendedAt, &i.LastLogin,
&i.OrganizationID, &i.ProfileCompleted,
&i.ProfilePictureUrl,
&i.PreferredLanguage,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
) )
return i, err return i, err
} }
const SearchUserByNameOrPhone = `-- name: SearchUserByNameOrPhone :many const GetUserByUserName = `-- name: GetUserByUserName :one
SELECT id, SELECT
id,
first_name, first_name,
last_name, last_name,
nick_name, user_name,
email,
phone_number,
role,
password,
age,
education_level,
country,
region,
email_verified,
phone_verified,
status,
profile_completed,
last_login,
profile_picture_url,
preferred_language,
created_at,
updated_at
FROM users
WHERE user_name = $1 AND $1 IS NOT NULL
LIMIT 1
`
type GetUserByUserNameRow struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
UserName string `json:"user_name"`
Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"`
Password []byte `json:"password"`
Age pgtype.Int4 `json:"age"`
EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"`
ProfileCompleted bool `json:"profile_completed"`
LastLogin pgtype.Timestamptz `json:"last_login"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
PreferredLanguage pgtype.Text `json:"preferred_language"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetUserByUserName(ctx context.Context, userName string) (GetUserByUserNameRow, error) {
row := q.db.QueryRow(ctx, GetUserByUserName, userName)
var i GetUserByUserNameRow
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.UserName,
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.Password,
&i.Age,
&i.EducationLevel,
&i.Country,
&i.Region,
&i.EmailVerified,
&i.PhoneVerified,
&i.Status,
&i.ProfileCompleted,
&i.LastLogin,
&i.ProfilePictureUrl,
&i.PreferredLanguage,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const IsUserNameUnique = `-- name: IsUserNameUnique :one
SELECT
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique
FROM users
WHERE user_name = $1
`
func (q *Queries) IsUserNameUnique(ctx context.Context, userName string) (bool, error) {
row := q.db.QueryRow(ctx, IsUserNameUnique, userName)
var is_unique bool
err := row.Scan(&is_unique)
return is_unique, err
}
const IsUserPending = `-- name: IsUserPending :one
SELECT
CASE WHEN status = 'PENDING' THEN true ELSE false END AS is_pending
FROM users
WHERE user_name = $1
LIMIT 1
`
func (q *Queries) IsUserPending(ctx context.Context, userName string) (bool, error) {
row := q.db.QueryRow(ctx, IsUserPending, userName)
var is_pending bool
err := row.Scan(&is_pending)
return is_pending, err
}
const SearchUserByNameOrPhone = `-- name: SearchUserByNameOrPhone :many
SELECT
id,
first_name,
last_name,
user_name,
email, email,
phone_number, phone_number,
role, role,
@ -523,56 +577,50 @@ SELECT id,
region, region,
email_verified, email_verified,
phone_verified, phone_verified,
status,
profile_completed,
created_at, created_at,
updated_at, updated_at
suspended,
suspended_at,
organization_id
FROM users FROM users
WHERE ( WHERE (
organization_id = $2
OR $2 IS NULL
)
AND (
first_name ILIKE '%' || $1 || '%' first_name ILIKE '%' || $1 || '%'
OR last_name ILIKE '%' || $1 || '%' OR last_name ILIKE '%' || $1 || '%'
OR phone_number LIKE '%' || $1 || '%' OR phone_number ILIKE '%' || $1 || '%'
OR email ILIKE '%' || $1 || '%'
) )
AND ( AND (
role = $3 role = $2
OR $3 IS NULL OR $2 IS NULL
) )
` `
type SearchUserByNameOrPhoneParams struct { type SearchUserByNameOrPhoneParams struct {
Column1 pgtype.Text `json:"column_1"` Column1 pgtype.Text `json:"column_1"`
OrganizationID pgtype.Int8 `json:"organization_id"` Role pgtype.Text `json:"role"`
Role pgtype.Text `json:"role"`
} }
type SearchUserByNameOrPhoneRow struct { type SearchUserByNameOrPhoneRow struct {
ID int64 `json:"id"` ID int64 `json:"id"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
NickName pgtype.Text `json:"nick_name"` UserName string `json:"user_name"`
Email pgtype.Text `json:"email"` Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"` PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"` Role string `json:"role"`
Age pgtype.Int4 `json:"age"` Age pgtype.Int4 `json:"age"`
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
CreatedAt pgtype.Timestamptz `json:"created_at"` Status string `json:"status"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` ProfileCompleted bool `json:"profile_completed"`
Suspended bool `json:"suspended"` CreatedAt pgtype.Timestamptz `json:"created_at"`
SuspendedAt pgtype.Timestamptz `json:"suspended_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
OrganizationID pgtype.Int8 `json:"organization_id"`
} }
func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByNameOrPhoneParams) ([]SearchUserByNameOrPhoneRow, error) { func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByNameOrPhoneParams) ([]SearchUserByNameOrPhoneRow, error) {
rows, err := q.db.Query(ctx, SearchUserByNameOrPhone, arg.Column1, arg.OrganizationID, arg.Role) rows, err := q.db.Query(ctx, SearchUserByNameOrPhone, arg.Column1, arg.Role)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -584,7 +632,7 @@ func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByN
&i.ID, &i.ID,
&i.FirstName, &i.FirstName,
&i.LastName, &i.LastName,
&i.NickName, &i.UserName,
&i.Email, &i.Email,
&i.PhoneNumber, &i.PhoneNumber,
&i.Role, &i.Role,
@ -594,11 +642,10 @@ func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByN
&i.Region, &i.Region,
&i.EmailVerified, &i.EmailVerified,
&i.PhoneVerified, &i.PhoneVerified,
&i.Status,
&i.ProfileCompleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.Suspended,
&i.SuspendedAt,
&i.OrganizationID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -610,59 +657,31 @@ func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByN
return items, nil return items, nil
} }
const SuspendUser = `-- name: SuspendUser :exec
UPDATE users
SET suspended = $1,
suspended_at = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $3
`
type SuspendUserParams struct {
Suspended bool `json:"suspended"`
SuspendedAt pgtype.Timestamptz `json:"suspended_at"`
ID int64 `json:"id"`
}
func (q *Queries) SuspendUser(ctx context.Context, arg SuspendUserParams) error {
_, err := q.db.Exec(ctx, SuspendUser, arg.Suspended, arg.SuspendedAt, arg.ID)
return err
}
const UpdatePassword = `-- name: UpdatePassword :exec const UpdatePassword = `-- name: UpdatePassword :exec
UPDATE users UPDATE users
SET password = $1, SET
updated_at = $4 password = $1,
WHERE ( updated_at = CURRENT_TIMESTAMP
(email = $2 OR phone_number = $3) WHERE email = $2 OR phone_number = $3
AND organization_id = $5
)
` `
type UpdatePasswordParams struct { type UpdatePasswordParams struct {
Password []byte `json:"password"` Password []byte `json:"password"`
Email pgtype.Text `json:"email"` Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"` PhoneNumber pgtype.Text `json:"phone_number"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
OrganizationID pgtype.Int8 `json:"organization_id"`
} }
func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error { func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error {
_, err := q.db.Exec(ctx, UpdatePassword, _, err := q.db.Exec(ctx, UpdatePassword, arg.Password, arg.Email, arg.PhoneNumber)
arg.Password,
arg.Email,
arg.PhoneNumber,
arg.UpdatedAt,
arg.OrganizationID,
)
return err return err
} }
const UpdateUser = `-- name: UpdateUser :exec const UpdateUser = `-- name: UpdateUser :exec
UPDATE users UPDATE users
SET first_name = $1, SET
last_name = $2, first_name = $1,
suspended = $3, last_name = $2,
status = $3,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $4 WHERE id = $4
` `
@ -670,7 +689,7 @@ WHERE id = $4
type UpdateUserParams struct { type UpdateUserParams struct {
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
Suspended bool `json:"suspended"` Status string `json:"status"`
ID int64 `json:"id"` ID int64 `json:"id"`
} }
@ -678,24 +697,26 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
_, err := q.db.Exec(ctx, UpdateUser, _, err := q.db.Exec(ctx, UpdateUser,
arg.FirstName, arg.FirstName,
arg.LastName, arg.LastName,
arg.Suspended, arg.Status,
arg.ID, arg.ID,
) )
return err return err
} }
const UpdateUserOrganization = `-- name: UpdateUserOrganization :exec const UpdateUserStatus = `-- name: UpdateUserStatus :exec
UPDATE users UPDATE users
SET organization_id = $1 SET
status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2 WHERE id = $2
` `
type UpdateUserOrganizationParams struct { type UpdateUserStatusParams struct {
OrganizationID pgtype.Int8 `json:"organization_id"` Status string `json:"status"`
ID int64 `json:"id"` ID int64 `json:"id"`
} }
func (q *Queries) UpdateUserOrganization(ctx context.Context, arg UpdateUserOrganizationParams) error { func (q *Queries) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) error {
_, err := q.db.Exec(ctx, UpdateUserOrganization, arg.OrganizationID, arg.ID) _, err := q.db.Exec(ctx, UpdateUserStatus, arg.Status, arg.ID)
return err return err
} }

View File

@ -72,6 +72,12 @@ var (
// Enabled bool `mapstructure:"Enabled"` // Enabled bool `mapstructure:"Enabled"`
// } // }
type AFROSMSConfig struct {
AfroSMSIdentifierID string `mapstructure:"afro_sms_identifier_id"`
AfroSMSAPIKey string `mapstructure:"afro_sms_api_key"`
AfroSMSBaseURL string `mapstructure:"afro_sms_base_url"`
}
// type AtlasConfig struct { // type AtlasConfig struct {
// BaseURL string `mapstructure:"ATLAS_BASE_URL"` // BaseURL string `mapstructure:"ATLAS_BASE_URL"`
// SecretKey string `mapstructure:"ATLAS_SECRET_KEY"` // SecretKey string `mapstructure:"ATLAS_SECRET_KEY"`
@ -120,6 +126,7 @@ type TELEBIRRConfig struct {
} }
type Config struct { type Config struct {
AFROSMSConfig AFROSMSConfig `mapstructure:"afro_sms_config"`
APP_VERSION string APP_VERSION string
FIXER_API_KEY string FIXER_API_KEY string
FIXER_BASE_URL string FIXER_BASE_URL string
@ -248,6 +255,11 @@ func (c *Config) loadEnv() error {
return ErrInvalidLevel return ErrInvalidLevel
} }
//Afro SMS
c.AFROSMSConfig.AfroSMSAPIKey = os.Getenv("AFRO_SMS_API_KEY")
c.AFROSMSConfig.AfroSMSIdentifierID = os.Getenv("AFRO_SMS_IDENTIFIER_ID")
c.AFROSMSConfig.AfroSMSBaseURL = os.Getenv("AFRO_SMS_BASE_URL")
//Telebirr //Telebirr
c.TELEBIRR.TelebirrBaseURL = os.Getenv("TELEBIRR_BASE_URL") c.TELEBIRR.TelebirrBaseURL = os.Getenv("TELEBIRR_BASE_URL")
c.TELEBIRR.TelebirrAppSecret = os.Getenv("TELEBIRR_APP_SECRET") c.TELEBIRR.TelebirrAppSecret = os.Getenv("TELEBIRR_APP_SECRET")
@ -388,7 +400,7 @@ func (c *Config) loadEnv() error {
c.ADRO_SMS_HOST_URL = os.Getenv("ADRO_SMS_HOST_URL") c.ADRO_SMS_HOST_URL = os.Getenv("ADRO_SMS_HOST_URL")
if c.ADRO_SMS_HOST_URL == "" { if c.ADRO_SMS_HOST_URL == "" {
c.ADRO_SMS_HOST_URL = "https://api.afrosms.com" c.ADRO_SMS_HOST_URL = "https://api.afromessage.com"
} }
//Atlas //Atlas

View File

@ -26,10 +26,9 @@ const (
OtpMediumSms OtpMedium = "sms" OtpMediumSms OtpMedium = "sms"
) )
type Otp struct { type Otp struct {
ID int64 ID int64
UserName string
SentTo string SentTo string
Medium OtpMedium Medium OtpMedium
For OtpFor For OtpFor
@ -39,3 +38,12 @@ type Otp struct {
CreatedAt time.Time CreatedAt time.Time
ExpiresAt time.Time ExpiresAt time.Time
} }
type VerifyOtpReq struct {
UserName string `json:"user_name" validate:"required"`
Otp string `json:"otp" validate:"required"`
}
type ResendOtpReq struct {
UserName string `json:"user_name" validate:"required"`
}

View File

@ -6,34 +6,79 @@ import (
) )
var ( var (
ErrUserNotFound = errors.New("user not found") ErrUserNotVerified = errors.New("user not verified")
ErrUserNotFound = errors.New("user not found")
ErrEmailAlreadyRegistered = errors.New("email is already registered")
ErrPhoneAlreadyRegistered = errors.New("phone number is already registered")
)
/*
UserStatus reflects the lifecycle state of a user account.
Matches DB column: users.status
*/
type UserStatus string
const (
UserStatusPending UserStatus = "PENDING"
UserStatusActive UserStatus = "ACTIVE"
UserStatusSuspended UserStatus = "SUSPENDED"
UserStatusDeactivated UserStatus = "DEACTIVATED"
) )
type User struct { type User struct {
ID int64 ID int64
FirstName string FirstName string
LastName string LastName string
NickName string UserName string
Email string `json:"email"` Email string
PhoneNumber string `json:"phone_number"` PhoneNumber string
Password []byte Password []byte
Role Role Role Role
Age int Age int
EducationLevel string EducationLevel string
Country string Country string
Region string Region string
EmailVerified bool
PhoneVerified bool EmailVerified bool
Suspended bool PhoneVerified bool
SuspendedAt time.Time Status UserStatus
OrganizationID ValidInt64
CreatedAt time.Time LastLogin *time.Time
UpdatedAt time.Time ProfileCompleted bool
ProfilePictureURL string
PreferredLanguage string
CreatedAt time.Time
UpdatedAt *time.Time
}
type UserProfileResponse struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
UserName string `json:"user_name,omitempty"`
Email string `json:"email,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"`
Role Role `json:"role"`
Age int `json:"age,omitempty"`
EducationLevel string `json:"education_level,omitempty"`
Country string `json:"country,omitempty"`
Region string `json:"region,omitempty"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
Status UserStatus `json:"status"`
LastLogin *time.Time `json:"last_login,omitempty"`
ProfileCompleted bool `json:"profile_completed"`
ProfilePictureURL string `json:"profile_picture_url,omitempty"`
PreferredLanguage string `json:"preferred_language,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
} }
type UserFilter struct { type UserFilter struct {
Role string Role string
OrganizationID ValidInt64
Page ValidInt Page ValidInt
PageSize ValidInt PageSize ValidInt
Query ValidString Query ValidString
@ -42,92 +87,62 @@ type UserFilter struct {
} }
type RegisterUserReq struct { type RegisterUserReq struct {
FirstName string FirstName string
LastName string LastName string
NickName string UserName string
Email string Email string
PhoneNumber string PhoneNumber string
Password string Password string
Role string Role string
Otp string
ReferralCode string `json:"referral_code"` OtpMedium OtpMedium
OtpMedium OtpMedium
OrganizationID ValidInt64 Age int
Age int EducationLevel string
EducationLevel string Country string
Country string Region string
Region string PreferredLanguage string
} }
type CreateUserReq struct { type CreateUserReq struct {
FirstName string FirstName string
LastName string LastName string
NickName string UserName string
Email string Email string
PhoneNumber string PhoneNumber string
Password string Password string
Role string Role string
Suspended bool
OrganizationID ValidInt64 Status UserStatus
Age int
EducationLevel string Age int
Country string EducationLevel string
Region string Country string
Region string
PreferredLanguage string
} }
type ResetPasswordReq struct { type ResetPasswordReq struct {
Email string UserName string
PhoneNumber string Password string
Password string OtpCode string
Otp string
OtpMedium OtpMedium
OrganizationID int64
} }
type UpdateUserReq struct { type UpdateUserReq struct {
UserID int64 UserID int64
FirstName ValidString
LastName ValidString FirstName ValidString
NickName ValidString LastName ValidString
Suspended ValidBool UserName ValidString
OrganizationID ValidInt64
Status ValidString
Age ValidInt Age ValidInt
EducationLevel ValidString EducationLevel ValidString
Country ValidString Country ValidString
Region ValidString Region ValidString
ProfileCompleted ValidBool
ProfilePictureURL ValidString
PreferredLanguage ValidString
} }
type UpdateUserReferralCode struct {
UserID int64
Code string
}
// ValidInt64 wraps int64 for optional values
// type ValidInt64 struct {
// Value int64
// Valid bool
// }
// // ValidInt wraps int for optional values
// type ValidInt struct {
// Value int
// Valid bool
// }
// // ValidString wraps string for optional values
// type ValidString struct {
// Value string
// Valid bool
// }
// // ValidBool wraps bool for optional values
// type ValidBool struct {
// Value bool
// Valid bool
// }
// // ValidTime wraps time.Time for optional values
// type ValidTime struct {
// Value time.Time
// Valid bool
// }

View File

@ -8,33 +8,50 @@ import (
) )
type UserStore interface { type UserStore interface {
CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error) IsUserNameUnique(ctx context.Context, userName string) (bool, error)
CreateUserWithoutOtp(ctx context.Context, user domain.User) (domain.User, error) IsUserPending(ctx context.Context, UserName string) (bool, error)
GetUserByID(ctx context.Context, id int64) (domain.User, error) GetUserByUserName(
ctx context.Context,
userName string,
) (domain.User, error)
CreateUser(
ctx context.Context,
user domain.User,
usedOtpId int64,
) (domain.User, error)
CreateUserWithoutOtp(
ctx context.Context,
user domain.User,
) (domain.User, error)
GetUserByID(
ctx context.Context,
id int64,
) (domain.User, error)
GetAllUsers( GetAllUsers(
ctx context.Context, ctx context.Context,
role *string, role *string,
organizationID *int64,
query *string, query *string,
createdBefore, createdAfter *time.Time, createdBefore, createdAfter *time.Time,
limit, offset int32, limit, offset int32,
) ([]domain.User, int64, error) ) ([]domain.User, int64, error)
GetTotalUsers(ctx context.Context, role *string, organizationID *int64) (int64, error) GetTotalUsers(ctx context.Context, role *string) (int64, error)
SearchUserByNameOrPhone(ctx context.Context, search string, organizationID *int64, role *string) ([]domain.User, error) SearchUserByNameOrPhone(
ctx context.Context,
search string,
role *string,
) ([]domain.User, error)
UpdateUser(ctx context.Context, user domain.User) error UpdateUser(ctx context.Context, user domain.User) error
UpdateUserOrganization(ctx context.Context, userID, organizationID int64) error
SuspendUser(ctx context.Context, userID int64, suspended bool, suspendedAt time.Time) error
DeleteUser(ctx context.Context, userID int64) error DeleteUser(ctx context.Context, userID int64) error
CheckPhoneEmailExist(ctx context.Context, phone, email string, organizationID domain.ValidInt64) (phoneExists, emailExists bool, err error) CheckPhoneEmailExist(ctx context.Context, phone, email string) (phoneExists, emailExists bool, err error)
GetUserByEmailPhone( GetUserByEmailPhone(
ctx context.Context, ctx context.Context,
email string, email string,
phone string, phone string,
organizationID domain.ValidInt64,
) (domain.User, error) ) (domain.User, error)
UpdatePassword(ctx context.Context, password, email, phone string, organizationID int64, updatedAt time.Time) error UpdatePassword(ctx context.Context, password, email, phone string, updatedAt time.Time) error
GetOwnerByOrganizationID(ctx context.Context, organizationID int64) (domain.User, error) // GetOwnerByOrganizationID(ctx context.Context, organizationID int64) (domain.User, error)
UpdateUserSuspend(ctx context.Context, id int64, status bool) error // GetOwnerByOrganizationID(ctx context.Context, organizationID int64) (domain.User, error)
// UpdateUserSuspend(ctx context.Context, id int64, status bool) error
// UpdateUser(ctx context.Context, user domain.UpdateUserReq) error // UpdateUser(ctx context.Context, user domain.UpdateUserReq) error
// UpdateUserSuspend(ctx context.Context, id int64, status bool) error // UpdateUserSuspend(ctx context.Context, id int64, status bool) error
@ -57,6 +74,8 @@ type EmailGateway interface {
SendEmailOTP(ctx context.Context, email string, otp string) error SendEmailOTP(ctx context.Context, email string, otp string) error
} }
type OtpStore interface { type OtpStore interface {
UpdateExpiredOtp(ctx context.Context, otp, userName string) error
MarkOtpAsUsed(ctx context.Context, otp domain.Otp) error
CreateOtp(ctx context.Context, otp domain.Otp) error CreateOtp(ctx context.Context, otp domain.Otp) error
GetOtp(ctx context.Context, sentTo string, sentfor domain.OtpFor, medium domain.OtpMedium) (domain.Otp, error) GetOtp(ctx context.Context, userName string) (domain.Otp, error)
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"time"
dbgen "Yimaru-Backend/gen/db" dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
@ -73,19 +74,22 @@ func (s *Store) RevokeRefreshToken(ctx context.Context, token string) error {
} }
// GetUserByEmailOrPhone retrieves a user by email or phone number and optional organization ID // GetUserByEmailOrPhone retrieves a user by email or phone number and optional organization ID
func (s *Store) GetUserByEmailOrPhone(ctx context.Context, email, phone string, organizationID *int64) (domain.User, error) { func (s *Store) GetUserByEmailOrPhone(
// prepare organizationID param for the query ctx context.Context,
// var orgParam pgtype.Int8 email string,
// if organizationID != nil { phone string,
// orgParam = pgtype.Int8{Int64: *organizationID} ) (domain.User, error) {
// } else {
// orgParam = pgtype.Int8{Status: pgtype.Null}
// }
u, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{ u, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{
Email: pgtype.Text{String: email, Valid: email != ""}, Email: pgtype.Text{
PhoneNumber: pgtype.Text{String: phone, Valid: phone != ""}, String: email,
OrganizationID: pgtype.Int8{Int64: *organizationID}, Valid: email != "",
},
PhoneNumber: pgtype.Text{
String: phone,
Valid: phone != "",
},
// OrganizationID: pgtype.Int8{Int64: organizationID},
}) })
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
@ -94,20 +98,45 @@ func (s *Store) GetUserByEmailOrPhone(ctx context.Context, email, phone string,
return domain.User{}, err return domain.User{}, err
} }
var lastLogin *time.Time
if u.LastLogin.Valid {
lastLogin = &u.LastLogin.Time
}
var updatedAt *time.Time
if u.UpdatedAt.Valid {
updatedAt = &u.UpdatedAt.Time
}
return domain.User{ return domain.User{
ID: u.ID, ID: u.ID,
FirstName: u.FirstName, FirstName: u.FirstName,
LastName: u.LastName, LastName: u.LastName,
Email: u.Email.String, UserName: u.UserName,
PhoneNumber: u.PhoneNumber.String, Email: u.Email.String,
Role: domain.Role(u.Role), PhoneNumber: u.PhoneNumber.String,
Password: u.Password, Password: u.Password,
Role: domain.Role(u.Role),
Age: int(u.Age.Int32),
EducationLevel: u.EducationLevel.String,
Country: u.Country.String,
Region: u.Region.String,
EmailVerified: u.EmailVerified, EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified, PhoneVerified: u.PhoneVerified,
Suspended: u.Suspended, Status: domain.UserStatus(u.Status),
SuspendedAt: u.SuspendedAt.Time,
OrganizationID: domain.ValidInt64{Value: u.OrganizationID.Int64, Valid: u.OrganizationID.Valid}, LastLogin: lastLogin,
CreatedAt: u.CreatedAt.Time, ProfileCompleted: u.ProfileCompleted,
UpdatedAt: u.UpdatedAt.Time, ProfilePictureURL: u.ProfilePictureUrl.String,
PreferredLanguage: u.PreferredLanguage.String,
// OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt,
}, nil }, nil
} }

View File

@ -3,6 +3,7 @@ package repository
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"strconv"
dbgen "Yimaru-Backend/gen/db" dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
@ -155,7 +156,7 @@ func mapDBToDomain(db *dbgen.Notification) *domain.Notification {
} }
return &domain.Notification{ return &domain.Notification{
ID: string(db.ID), ID: strconv.FormatInt(db.ID, 10),
RecipientID: db.UserID, RecipientID: db.UserID,
Type: domain.NotificationType(db.Type), Type: domain.NotificationType(db.Type),
Level: domain.NotificationLevel(db.Level), Level: domain.NotificationLevel(db.Level),

View File

@ -4,16 +4,29 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"time"
dbgen "Yimaru-Backend/gen/db" dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
"Yimaru-Backend/internal/ports" "Yimaru-Backend/internal/ports"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
// Interface for creating new otp store // Interface for creating new otp store
func NewOTPStore(s *Store) ports.OtpStore { return s } func NewOTPStore(s *Store) ports.OtpStore { return s }
func (s *Store) UpdateExpiredOtp(ctx context.Context, otp, userName string) error {
return s.queries.UpdateExpiredOtp(ctx, dbgen.UpdateExpiredOtpParams{
UserName: userName,
Otp: otp,
ExpiresAt: pgtype.Timestamptz{
Time: time.Now().Add(5 * time.Minute),
Valid: true,
},
})
}
func (s *Store) CreateOtp(ctx context.Context, otp domain.Otp) error { func (s *Store) CreateOtp(ctx context.Context, otp domain.Otp) error {
return s.queries.CreateOtp(ctx, dbgen.CreateOtpParams{ return s.queries.CreateOtp(ctx, dbgen.CreateOtpParams{
SentTo: otp.SentTo, SentTo: otp.SentTo,
@ -30,14 +43,10 @@ func (s *Store) CreateOtp(ctx context.Context, otp domain.Otp) error {
}, },
}) })
} }
func (s *Store) GetOtp(ctx context.Context, sentTo string, sentfor domain.OtpFor, medium domain.OtpMedium) (domain.Otp, error) { func (s *Store) GetOtp(ctx context.Context, userName string) (domain.Otp, error) {
row, err := s.queries.GetOtp(ctx, dbgen.GetOtpParams{ row, err := s.queries.GetOtp(ctx, userName)
SentTo: sentTo,
Medium: string(medium),
OtpFor: string(sentfor),
})
if err != nil { if err != nil {
fmt.Printf("OTP REPO error: %v sentTo: %v, medium: %v, otpFor: %v\n", err, sentTo, medium, sentfor) fmt.Printf("OTP REPO error: %v userName: %v\n", err, userName)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return domain.Otp{}, domain.ErrOtpNotFound return domain.Otp{}, domain.ErrOtpNotFound
} }

View File

@ -14,129 +14,184 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
// type Store struct {
// db *pgxpool.Pool
// queries *dbgen.Queries
// }
// func NewStore(db *pgxpool.Pool) *Store {
// return &Store{
// db: db,
// queries: dbgen.New(db),
// }
// }
func NewUserStore(s *Store) ports.UserStore { return s } func NewUserStore(s *Store) ports.UserStore { return s }
func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User) (domain.User, error) { func (s *Store) IsUserPending(ctx context.Context, UserName string) (bool, error) {
isPending, err := s.queries.IsUserPending(ctx, UserName)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, authentication.ErrUserNotFound
}
return false, err
}
return isPending, nil
}
func (s *Store) IsUserNameUnique(ctx context.Context, userName string) (bool, error) {
isUnique, err := s.queries.IsUserNameUnique(ctx, userName)
if err != nil {
return false, err
}
return isUnique, nil
}
func (s *Store) CreateUserWithoutOtp(
ctx context.Context,
user domain.User,
) (domain.User, error) {
userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{ userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
NickName: pgtype.Text{String: user.NickName}, UserName: user.UserName,
Email: pgtype.Text{String: user.Email, Valid: user.Email != ""},
PhoneNumber: pgtype.Text{String: user.PhoneNumber, Valid: user.PhoneNumber != ""}, Email: pgtype.Text{String: user.Email, Valid: user.Email != ""},
Role: string(user.Role), PhoneNumber: pgtype.Text{String: user.PhoneNumber, Valid: user.PhoneNumber != ""},
Password: user.Password,
Role: string(user.Role),
Password: user.Password,
Age: pgtype.Int4{Int32: int32(user.Age), Valid: user.Age > 0}, Age: pgtype.Int4{Int32: int32(user.Age), Valid: user.Age > 0},
EducationLevel: pgtype.Text{String: user.EducationLevel, Valid: user.EducationLevel != ""}, EducationLevel: pgtype.Text{String: user.EducationLevel, Valid: user.EducationLevel != ""},
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""}, Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""}, Region: pgtype.Text{String: user.Region, Valid: user.Region != ""},
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified, EmailVerified: user.EmailVerified,
Suspended: user.Suspended, PhoneVerified: user.PhoneVerified,
SuspendedAt: pgtype.Timestamptz{Time: user.SuspendedAt, Valid: !user.SuspendedAt.IsZero()},
OrganizationID: pgtype.Int8{Int64: user.OrganizationID.Value, Valid: user.OrganizationID.Valid}, Status: string(user.Status),
CreatedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, ProfileCompleted: user.ProfileCompleted,
UpdatedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, PreferredLanguage: pgtype.Text{
String: user.PreferredLanguage,
Valid: user.PreferredLanguage != "",
},
// OrganizationID: user.OrganizationID.ToPG(),
}) })
if err != nil { if err != nil {
return domain.User{}, err return domain.User{}, err
} }
var updatedAt *time.Time
if userRes.UpdatedAt.Valid {
updatedAt = &userRes.UpdatedAt.Time
}
return domain.User{ return domain.User{
ID: userRes.ID, ID: userRes.ID,
FirstName: userRes.FirstName, FirstName: userRes.FirstName,
LastName: userRes.LastName, LastName: userRes.LastName,
NickName: userRes.NickName.String, UserName: userRes.UserName,
Email: userRes.Email.String, Email: userRes.Email.String,
PhoneNumber: userRes.PhoneNumber.String, PhoneNumber: userRes.PhoneNumber.String,
Role: domain.Role(userRes.Role), Role: domain.Role(userRes.Role),
Password: user.Password,
Age: int(userRes.Age.Int32), Age: int(userRes.Age.Int32),
EducationLevel: userRes.EducationLevel.String, EducationLevel: userRes.EducationLevel.String,
Country: userRes.Country.String, Country: userRes.Country.String,
Region: userRes.Region.String, Region: userRes.Region.String,
EmailVerified: userRes.EmailVerified,
PhoneVerified: userRes.PhoneVerified, EmailVerified: userRes.EmailVerified,
Suspended: userRes.Suspended, PhoneVerified: userRes.PhoneVerified,
SuspendedAt: userRes.SuspendedAt.Time, Status: domain.UserStatus(userRes.Status),
OrganizationID: domain.ValidInt64{Value: userRes.OrganizationID.Int64, Valid: userRes.OrganizationID.Valid},
CreatedAt: userRes.CreatedAt.Time, ProfileCompleted: userRes.ProfileCompleted,
UpdatedAt: userRes.UpdatedAt.Time, PreferredLanguage: userRes.PreferredLanguage.String,
CreatedAt: userRes.CreatedAt.Time,
UpdatedAt: updatedAt,
}, nil }, nil
} }
// CreateUser inserts a new user into the database // CreateUser inserts a new user into the database
func (s *Store) CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error) { func (s *Store) CreateUser(
ctx context.Context,
user domain.User,
usedOtpId int64,
) (domain.User, error) {
// Optional: mark OTP as used // Optional: mark OTP as used
if usedOtpId > 0 { if usedOtpId > 0 {
err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{ if err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{
ID: usedOtpId, ID: usedOtpId,
UsedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, UsedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
}) }); err != nil {
if err != nil {
return domain.User{}, err return domain.User{}, err
} }
} }
userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{ userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
NickName: pgtype.Text{String: user.NickName}, UserName: user.UserName,
Email: pgtype.Text{String: user.Email, Valid: user.Email != ""},
PhoneNumber: pgtype.Text{String: user.PhoneNumber, Valid: user.PhoneNumber != ""}, Email: pgtype.Text{String: user.Email, Valid: user.Email != ""},
Role: string(user.Role), PhoneNumber: pgtype.Text{String: user.PhoneNumber, Valid: user.PhoneNumber != ""},
Password: user.Password,
Role: string(user.Role),
Password: user.Password,
Age: pgtype.Int4{Int32: int32(user.Age), Valid: user.Age > 0}, Age: pgtype.Int4{Int32: int32(user.Age), Valid: user.Age > 0},
EducationLevel: pgtype.Text{String: user.EducationLevel, Valid: user.EducationLevel != ""}, EducationLevel: pgtype.Text{String: user.EducationLevel, Valid: user.EducationLevel != ""},
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""}, Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""}, Region: pgtype.Text{String: user.Region, Valid: user.Region != ""},
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified, EmailVerified: user.EmailVerified,
Suspended: user.Suspended, PhoneVerified: user.PhoneVerified,
SuspendedAt: pgtype.Timestamptz{Time: user.SuspendedAt, Valid: !user.SuspendedAt.IsZero()},
OrganizationID: pgtype.Int8{Int64: user.OrganizationID.Value, Valid: user.OrganizationID.Valid}, Status: string(user.Status),
CreatedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, ProfileCompleted: user.ProfileCompleted,
UpdatedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, PreferredLanguage: pgtype.Text{
String: user.PreferredLanguage,
Valid: user.PreferredLanguage != "",
},
// OrganizationID: user.OrganizationID.ToPG(),
}) })
if err != nil { if err != nil {
return domain.User{}, err return domain.User{}, err
} }
var updatedAt *time.Time
if userRes.UpdatedAt.Valid {
updatedAt = &userRes.UpdatedAt.Time
}
return domain.User{ return domain.User{
ID: userRes.ID, ID: userRes.ID,
FirstName: userRes.FirstName, FirstName: userRes.FirstName,
LastName: userRes.LastName, LastName: userRes.LastName,
NickName: userRes.NickName.String, UserName: userRes.UserName,
Email: userRes.Email.String, Email: userRes.Email.String,
PhoneNumber: userRes.PhoneNumber.String, PhoneNumber: userRes.PhoneNumber.String,
Role: domain.Role(userRes.Role), Role: domain.Role(userRes.Role),
Password: user.Password,
Age: int(userRes.Age.Int32), Age: int(userRes.Age.Int32),
EducationLevel: userRes.EducationLevel.String, EducationLevel: userRes.EducationLevel.String,
Country: userRes.Country.String, Country: userRes.Country.String,
Region: userRes.Region.String, Region: userRes.Region.String,
EmailVerified: userRes.EmailVerified,
PhoneVerified: userRes.PhoneVerified,
Suspended: userRes.Suspended,
SuspendedAt: userRes.SuspendedAt.Time,
OrganizationID: domain.ValidInt64{Value: userRes.OrganizationID.Int64, Valid: userRes.OrganizationID.Valid},
CreatedAt: userRes.CreatedAt.Time,
UpdatedAt: userRes.UpdatedAt.Time,
}, nil
EmailVerified: userRes.EmailVerified,
PhoneVerified: userRes.PhoneVerified,
Status: domain.UserStatus(userRes.Status),
ProfileCompleted: userRes.ProfileCompleted,
PreferredLanguage: userRes.PreferredLanguage.String,
CreatedAt: userRes.CreatedAt.Time,
UpdatedAt: updatedAt,
}, nil
} }
// GetUserByID retrieves a user by ID // GetUserByID retrieves a user by ID
func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) { func (s *Store) GetUserByID(
userRes, err := s.queries.GetUserByID(ctx, id) ctx context.Context,
id int64,
) (domain.User, error) {
u, err := s.queries.GetUserByID(ctx, id)
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return domain.User{}, domain.ErrUserNotFound return domain.User{}, domain.ErrUserNotFound
@ -144,34 +199,52 @@ func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error)
return domain.User{}, err return domain.User{}, err
} }
return domain.User{ var lastLogin *time.Time
ID: userRes.ID, if u.LastLogin.Valid {
FirstName: userRes.FirstName, lastLogin = &u.LastLogin.Time
LastName: userRes.LastName, }
NickName: userRes.NickName.String,
Email: userRes.Email.String,
PhoneNumber: userRes.PhoneNumber.String,
Role: domain.Role(userRes.Role),
Age: int(userRes.Age.Int32),
EducationLevel: userRes.EducationLevel.String,
Country: userRes.Country.String,
Region: userRes.Region.String,
EmailVerified: userRes.EmailVerified,
PhoneVerified: userRes.PhoneVerified,
Suspended: userRes.Suspended,
SuspendedAt: userRes.SuspendedAt.Time,
OrganizationID: domain.ValidInt64{Value: userRes.OrganizationID.Int64, Valid: userRes.OrganizationID.Valid},
CreatedAt: userRes.CreatedAt.Time,
UpdatedAt: userRes.UpdatedAt.Time,
}, nil
var updatedAt *time.Time
if u.UpdatedAt.Valid {
updatedAt = &u.UpdatedAt.Time
}
return domain.User{
ID: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
UserName: u.UserName,
Email: u.Email.String,
PhoneNumber: u.PhoneNumber.String,
Role: domain.Role(u.Role),
Age: int(u.Age.Int32),
EducationLevel: u.EducationLevel.String,
Country: u.Country.String,
Region: u.Region.String,
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status),
LastLogin: lastLogin,
ProfileCompleted: u.ProfileCompleted,
ProfilePictureURL: u.ProfilePictureUrl.String,
PreferredLanguage: u.PreferredLanguage.String,
// OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt,
}, nil
} }
// GetAllUsers retrieves users with optional filters // GetAllUsers retrieves users with optional filters
func (s *Store) GetAllUsers( func (s *Store) GetAllUsers(
ctx context.Context, ctx context.Context,
role *string, role *string,
organizationID *int64,
query *string, query *string,
createdBefore, createdAfter *time.Time, createdBefore, createdAfter *time.Time,
limit, offset int32, limit, offset int32,
@ -186,9 +259,9 @@ func (s *Store) GetAllUsers(
params.Role = *role params.Role = *role
} }
if organizationID != nil { // if organizationID != nil {
params.OrganizationID = pgtype.Int8{Int64: *organizationID, Valid: true} // params.OrganizationID = pgtype.Int8{Int64: *organizationID, Valid: true}
} // }
if query != nil { if query != nil {
params.Query = pgtype.Text{String: *query, Valid: true} params.Query = pgtype.Text{String: *query, Valid: true}
@ -212,31 +285,42 @@ func (s *Store) GetAllUsers(
} }
totalCount := rows[0].TotalCount totalCount := rows[0].TotalCount
users := make([]domain.User, 0, len(rows)) users := make([]domain.User, 0, len(rows))
for _, u := range rows { for _, u := range rows {
var updatedAt *time.Time
if u.UpdatedAt.Valid {
updatedAt = &u.UpdatedAt.Time
}
users = append(users, domain.User{ users = append(users, domain.User{
ID: u.ID, ID: u.ID,
FirstName: u.FirstName, FirstName: u.FirstName,
LastName: u.LastName, LastName: u.LastName,
NickName: u.NickName.String, UserName: u.UserName,
Email: u.Email.String, Email: u.Email.String,
PhoneNumber: u.PhoneNumber.String, PhoneNumber: u.PhoneNumber.String,
Role: domain.Role(u.Role), Role: domain.Role(u.Role),
Age: int(u.Age.Int32), Age: int(u.Age.Int32),
EducationLevel: u.EducationLevel.String, EducationLevel: u.EducationLevel.String,
Country: u.Country.String, Country: u.Country.String,
Region: u.Region.String, Region: u.Region.String,
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified, EmailVerified: u.EmailVerified,
Suspended: u.Suspended, PhoneVerified: u.PhoneVerified,
SuspendedAt: u.SuspendedAt.Time, Status: domain.UserStatus(u.Status),
OrganizationID: domain.ValidInt64{
Value: u.OrganizationID.Int64, ProfileCompleted: u.ProfileCompleted,
Valid: u.OrganizationID.Valid, PreferredLanguage: u.PreferredLanguage.String,
},
// OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time, CreatedAt: u.CreatedAt.Time,
UpdatedAt: u.UpdatedAt.Time, UpdatedAt: updatedAt,
}) })
} }
@ -244,11 +328,8 @@ func (s *Store) GetAllUsers(
} }
// GetTotalUsers counts users with optional filters // GetTotalUsers counts users with optional filters
func (s *Store) GetTotalUsers(ctx context.Context, role *string, organizationID *int64) (int64, error) { func (s *Store) GetTotalUsers(ctx context.Context, role *string) (int64, error) {
count, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ count, err := s.queries.GetTotalUsers(ctx, *role)
Role: *role,
OrganizationID: pgtype.Int8{Int64: *organizationID},
})
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -256,38 +337,73 @@ func (s *Store) GetTotalUsers(ctx context.Context, role *string, organizationID
} }
// SearchUserByNameOrPhone searches users by name or phone // SearchUserByNameOrPhone searches users by name or phone
func (s *Store) SearchUserByNameOrPhone(ctx context.Context, search string, organizationID *int64, role *string) ([]domain.User, error) { func (s *Store) SearchUserByNameOrPhone(
rows, err := s.queries.SearchUserByNameOrPhone(ctx, dbgen.SearchUserByNameOrPhoneParams{ ctx context.Context,
Column1: pgtype.Text{String: search}, search string,
OrganizationID: pgtype.Int8{Int64: *organizationID}, role *string,
Role: pgtype.Text{String: *role}, ) ([]domain.User, error) {
})
params := dbgen.SearchUserByNameOrPhoneParams{
Column1: pgtype.Text{
String: search,
Valid: search != "",
},
}
// if organizationID != nil {
// params.OrganizationID = pgtype.Int8{
// Int64: *organizationID,
// Valid: true,
// }
// }
if role != nil {
params.Role = pgtype.Text{
String: *role,
Valid: true,
}
}
rows, err := s.queries.SearchUserByNameOrPhone(ctx, params)
if err != nil { if err != nil {
return nil, err return nil, err
} }
users := make([]domain.User, len(rows)) users := make([]domain.User, 0, len(rows))
for i, u := range rows { for _, u := range rows {
users[i] = domain.User{
ID: u.ID, var updatedAt *time.Time
FirstName: u.FirstName, if u.UpdatedAt.Valid {
LastName: u.LastName, updatedAt = &u.UpdatedAt.Time
NickName: u.NickName.String, }
Email: u.Email.String,
PhoneNumber: u.PhoneNumber.String, users = append(users, domain.User{
Role: domain.Role(u.Role), ID: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
UserName: u.UserName,
Email: u.Email.String,
PhoneNumber: u.PhoneNumber.String,
Role: domain.Role(u.Role),
Age: int(u.Age.Int32), Age: int(u.Age.Int32),
EducationLevel: u.EducationLevel.String, EducationLevel: u.EducationLevel.String,
Country: u.Country.String, Country: u.Country.String,
Region: u.Region.String, Region: u.Region.String,
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified, EmailVerified: u.EmailVerified,
Suspended: u.Suspended, PhoneVerified: u.PhoneVerified,
SuspendedAt: u.SuspendedAt.Time, Status: domain.UserStatus(u.Status),
OrganizationID: domain.ValidInt64{Value: u.OrganizationID.Int64, Valid: u.OrganizationID.Valid},
CreatedAt: u.CreatedAt.Time, ProfileCompleted: u.ProfileCompleted,
UpdatedAt: u.UpdatedAt.Time,
} // OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt,
})
} }
return users, nil return users, nil
@ -296,29 +412,29 @@ func (s *Store) SearchUserByNameOrPhone(ctx context.Context, search string, orga
// UpdateUser updates basic user info // UpdateUser updates basic user info
func (s *Store) UpdateUser(ctx context.Context, user domain.User) error { func (s *Store) UpdateUser(ctx context.Context, user domain.User) error {
return s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{ return s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
ID: user.ID,
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
Suspended: user.Suspended, Status: string(user.Status),
ID: user.ID,
}) })
} }
// UpdateUserOrganization updates a user's organization // UpdateUserOrganization updates a user's organization
func (s *Store) UpdateUserOrganization(ctx context.Context, userID, organizationID int64) error { // func (s *Store) UpdateUserOrganization(ctx context.Context, userID, organizationID int64) error {
return s.queries.UpdateUserOrganization(ctx, dbgen.UpdateUserOrganizationParams{ // return s.queries.UpdateUserOrganization(ctx, dbgen.UpdateUserOrganizationParams{
OrganizationID: pgtype.Int8{Int64: organizationID, Valid: true}, // OrganizationID: pgtype.Int8{Int64: organizationID, Valid: true},
ID: userID, // ID: userID,
}) // })
} // }
// SuspendUser suspends a user // SuspendUser suspends a user
func (s *Store) SuspendUser(ctx context.Context, userID int64, suspended bool, suspendedAt time.Time) error { // func (s *Store) SuspendUser(ctx context.Context, userID int64, suspended bool, suspendedAt time.Time) error {
return s.queries.SuspendUser(ctx, dbgen.SuspendUserParams{ // return s.queries.SuspendUser(ctx, dbgen.SuspendUserParams{
Suspended: suspended, // Suspended: suspended,
SuspendedAt: pgtype.Timestamptz{Time: suspendedAt, Valid: true}, // SuspendedAt: pgtype.Timestamptz{Time: suspendedAt, Valid: true},
ID: userID, // ID: userID,
}) // })
} // }
// DeleteUser removes a user // DeleteUser removes a user
func (s *Store) DeleteUser(ctx context.Context, userID int64) error { func (s *Store) DeleteUser(ctx context.Context, userID int64) error {
@ -326,11 +442,10 @@ func (s *Store) DeleteUser(ctx context.Context, userID int64) error {
} }
// CheckPhoneEmailExist checks if phone or email exists in an organization // CheckPhoneEmailExist checks if phone or email exists in an organization
func (s *Store) CheckPhoneEmailExist(ctx context.Context, phone, email string, organizationID domain.ValidInt64) (phoneExists, emailExists bool, err error) { func (s *Store) CheckPhoneEmailExist(ctx context.Context, phone, email string) (phoneExists, emailExists bool, err error) {
res, err := s.queries.CheckPhoneEmailExist(ctx, dbgen.CheckPhoneEmailExistParams{ res, err := s.queries.CheckPhoneEmailExist(ctx, dbgen.CheckPhoneEmailExistParams{
PhoneNumber: pgtype.Text{String: phone}, PhoneNumber: pgtype.Text{String: phone},
Email: pgtype.Text{String: email}, Email: pgtype.Text{String: email},
OrganizationID: pgtype.Int8{Int64: organizationID.Value},
}) })
if err != nil { if err != nil {
return false, false, err return false, false, err
@ -339,15 +454,69 @@ func (s *Store) CheckPhoneEmailExist(ctx context.Context, phone, email string, o
return res.PhoneExists, res.EmailExists, nil return res.PhoneExists, res.EmailExists, nil
} }
func (s *Store) GetUserByUserName(
ctx context.Context,
userName string,
) (domain.User, error) {
u, err := s.queries.GetUserByUserName(ctx, userName)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.User{}, authentication.ErrUserNotFound
}
return domain.User{}, err
}
var lastLogin *time.Time
if u.LastLogin.Valid {
lastLogin = &u.LastLogin.Time
}
var updatedAt *time.Time
if u.UpdatedAt.Valid {
updatedAt = &u.UpdatedAt.Time
}
return domain.User{
ID: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
UserName: u.UserName,
Email: u.Email.String,
PhoneNumber: u.PhoneNumber.String,
Password: u.Password,
Role: domain.Role(u.Role),
Age: int(u.Age.Int32),
EducationLevel: u.EducationLevel.String,
Country: u.Country.String,
Region: u.Region.String,
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status),
LastLogin: lastLogin,
ProfileCompleted: u.ProfileCompleted,
PreferredLanguage: u.PreferredLanguage.String,
// OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt,
}, nil
}
// GetUserByEmail retrieves a user by email and organization // GetUserByEmail retrieves a user by email and organization
func (s *Store) GetUserByEmailPhone( func (s *Store) GetUserByEmailPhone(
ctx context.Context, ctx context.Context,
email string, email string,
phone string, phone string,
organizationID domain.ValidInt64,
) (domain.User, error) { ) (domain.User, error) {
user, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{ u, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{
Email: pgtype.Text{ Email: pgtype.Text{
String: email, String: email,
Valid: email != "", Valid: email != "",
@ -356,7 +525,6 @@ func (s *Store) GetUserByEmailPhone(
String: phone, String: phone,
Valid: phone != "", Valid: phone != "",
}, },
OrganizationID: organizationID.ToPG(),
}) })
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
@ -365,87 +533,113 @@ func (s *Store) GetUserByEmailPhone(
return domain.User{}, err return domain.User{}, err
} }
return domain.User{ var lastLogin *time.Time
ID: user.ID, if u.LastLogin.Valid {
FirstName: user.FirstName, lastLogin = &u.LastLogin.Time
LastName: user.LastName,
NickName: user.NickName.String,
Email: user.Email.String,
PhoneNumber: user.PhoneNumber.String,
Password: user.Password,
Role: domain.Role(user.Role),
Age: int(user.Age.Int32),
EducationLevel: user.EducationLevel.String,
Country: user.Country.String,
Region: user.Region.String,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
Suspended: user.Suspended,
SuspendedAt: user.SuspendedAt.Time,
OrganizationID: domain.ValidInt64{
Value: user.OrganizationID.Int64,
Valid: user.OrganizationID.Valid,
},
CreatedAt: user.CreatedAt.Time,
UpdatedAt: user.UpdatedAt.Time,
}, nil
}
// UpdatePassword updates a user's password
func (s *Store) UpdatePassword(ctx context.Context, password, email, phone string, organizationID int64, updatedAt time.Time) error {
return s.queries.UpdatePassword(ctx, dbgen.UpdatePasswordParams{
Password: []byte(password),
Email: pgtype.Text{String: email},
PhoneNumber: pgtype.Text{String: phone},
UpdatedAt: pgtype.Timestamptz{Time: updatedAt},
OrganizationID: pgtype.Int8{Int64: organizationID},
})
}
// GetOwnerByOrganizationID retrieves the owner user of an organization
func (s *Store) GetOwnerByOrganizationID(ctx context.Context, organizationID int64) (domain.User, error) {
userRes, err := s.queries.GetOwnerByOrganizationID(ctx, organizationID)
if err != nil {
return domain.User{}, err
} }
return mapUser(userRes), nil
}
func (s *Store) UpdateUserSuspend(ctx context.Context, id int64, status bool) error { var updatedAt *time.Time
err := s.queries.SuspendUser(ctx, dbgen.SuspendUserParams{ if u.UpdatedAt.Valid {
ID: id, updatedAt = &u.UpdatedAt.Time
Suspended: status,
SuspendedAt: pgtype.Timestamptz{
Time: time.Now(),
Valid: true,
},
})
if err != nil {
return err
} }
return nil
}
// mapUser converts dbgen.User to domain.User
func mapUser(u dbgen.User) domain.User {
return domain.User{ return domain.User{
ID: u.ID, ID: u.ID,
FirstName: u.FirstName, FirstName: u.FirstName,
LastName: u.LastName, LastName: u.LastName,
NickName: u.NickName.String, UserName: u.UserName,
Email: u.Email.String, Email: u.Email.String,
PhoneNumber: u.PhoneNumber.String, PhoneNumber: u.PhoneNumber.String,
Role: domain.Role(u.Role), Password: u.Password,
Role: domain.Role(u.Role),
Age: int(u.Age.Int32), Age: int(u.Age.Int32),
EducationLevel: u.EducationLevel.String, EducationLevel: u.EducationLevel.String,
Country: u.Country.String, Country: u.Country.String,
Region: u.Region.String, Region: u.Region.String,
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified, EmailVerified: u.EmailVerified,
Suspended: u.Suspended, PhoneVerified: u.PhoneVerified,
SuspendedAt: u.SuspendedAt.Time, Status: domain.UserStatus(u.Status),
OrganizationID: domain.ValidInt64{Value: u.OrganizationID.Int64, Valid: u.OrganizationID.Valid},
CreatedAt: u.CreatedAt.Time, LastLogin: lastLogin,
UpdatedAt: u.UpdatedAt.Time, ProfileCompleted: u.ProfileCompleted,
PreferredLanguage: u.PreferredLanguage.String,
// OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt,
}, nil
}
// UpdatePassword updates a user's password
func (s *Store) UpdatePassword(ctx context.Context, password, email, phone string, updatedAt time.Time) error {
return s.queries.UpdatePassword(ctx, dbgen.UpdatePasswordParams{
Password: []byte(password),
Email: pgtype.Text{String: email},
PhoneNumber: pgtype.Text{String: phone},
// OrganizationID: pgtype.Int8{Int64: organizationID},
})
}
// GetOwnerByOrganizationID retrieves the owner user of an organization
// func (s *Store) GetOwnerByOrganizationID(ctx context.Context, organizationID int64) (domain.User, error) {
// userRes, err := s.queries.GetOwnerByOrganizationID(ctx, organizationID)
// if err != nil {
// return domain.User{}, err
// }
// return mapUser(userRes), nil
// }
// func (s *Store) UpdateUserSuspend(ctx context.Context, id int64, status bool) error {
// err := s.queries.SuspendUser(ctx, dbgen.SuspendUserParams{
// ID: id,
// Suspended: status,
// SuspendedAt: pgtype.Timestamptz{
// Time: time.Now(),
// Valid: true,
// },
// })
// if err != nil {
// return err
// }
// return nil
// }
// mapUser converts dbgen.User to domain.User
func MapUser(u dbgen.User) domain.User {
return domain.User{
ID: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
UserName: u.UserName,
Email: u.Email.String,
PhoneNumber: u.PhoneNumber.String,
Role: domain.Role(u.Role),
Age: int(u.Age.Int32),
EducationLevel: u.EducationLevel.String,
Country: u.Country.String,
Region: u.Region.String,
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status),
LastLogin: &u.LastLogin.Time,
ProfileCompleted: u.ProfileCompleted,
PreferredLanguage: u.PreferredLanguage.String,
// OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time,
} }
} }

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@ -23,55 +24,65 @@ type LoginSuccess struct {
UserId int64 UserId int64
Role domain.Role Role domain.Role
RfToken string RfToken string
CompanyID domain.ValidInt64
} }
func (s *Service) Login(ctx context.Context, email, phone string, password string, companyID domain.ValidInt64) (LoginSuccess, error) { func (s *Service) Login(
user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone, companyID) ctx context.Context,
userName, password string,
) (LoginSuccess, error) {
user, err := s.userStore.GetUserByUserName(ctx, userName)
if err != nil { if err != nil {
return LoginSuccess{}, err return LoginSuccess{}, err
} }
err = matchPassword(password, user.Password)
if err != nil { if user.Status == domain.UserStatusPending{
return LoginSuccess{}, domain.ErrUserNotVerified
}
// Verify password
if err := matchPassword(password, user.Password); err != nil {
return LoginSuccess{}, err return LoginSuccess{}, err
} }
if user.Suspended {
// Status check instead of Suspended
if user.Status == domain.UserStatusSuspended {
return LoginSuccess{}, ErrUserSuspended return LoginSuccess{}, ErrUserSuspended
} }
// Handle existing refresh token
oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID) oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
if err != nil && !errors.Is(err, ErrRefreshTokenNotFound) {
if err != nil && err != ErrRefreshTokenNotFound {
return LoginSuccess{}, err return LoginSuccess{}, err
} }
// If old refresh token is not revoked, revoke it // Revoke if exists and not revoked
if err == nil && !oldRefreshToken.Revoked { if err == nil && !oldRefreshToken.Revoked {
err = s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token) if err := s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token); err != nil {
if err != nil {
return LoginSuccess{}, err return LoginSuccess{}, err
} }
} }
// Generate new refresh token
refreshToken, err := generateRefreshToken() refreshToken, err := generateRefreshToken()
if err != nil { if err != nil {
return LoginSuccess{}, err return LoginSuccess{}, err
} }
err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
if err := s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
Token: refreshToken, Token: refreshToken,
UserID: user.ID, UserID: user.ID,
CreatedAt: time.Now(), CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second), ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
}) }); err != nil {
if err != nil {
return LoginSuccess{}, err return LoginSuccess{}, err
} }
// Return login success payload
return LoginSuccess{ return LoginSuccess{
UserId: user.ID, UserId: user.ID,
Role: user.Role, Role: user.Role,
RfToken: refreshToken, RfToken: refreshToken,
CompanyID: user.OrganizationID,
}, nil }, nil
} }

View File

@ -3,8 +3,13 @@ package messenger
import ( import (
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http"
"net/url"
"time"
afro "github.com/amanuelabay/afrosms-go" afro "github.com/amanuelabay/afrosms-go"
"github.com/twilio/twilio-go" "github.com/twilio/twilio-go"
@ -16,28 +21,13 @@ var (
) )
// If the company id is valid, it is a company based notification else its a global notification (by the super admin) // If the company id is valid, it is a company based notification else its a global notification (by the super admin)
func (s *Service) SendSMS(ctx context.Context, receiverPhone, message string, companyID domain.ValidInt64) error { func (s *Service) SendSMS(ctx context.Context, receiverPhone, message string) error {
var settingsList domain.SettingList var settingsList domain.SettingList
// var err error
// if companyID.Valid {
// settingsList, err = s.settingSvc.GetOverrideSettingsList(ctx, companyID.Value)
// if err != nil {
// // TODO: Send a log about the error
// return err
// }
// } else {
// settingsList, err = s.settingSvc.GetGlobalSettingList(ctx)
// if err != nil {
// // TODO: Send a log about the error
// return err
// }
// }
switch settingsList.SMSProvider { switch settingsList.SMSProvider {
case domain.AfroMessage: case domain.AfroMessage:
return s.SendAfroMessageSMS(ctx, receiverPhone, message) return s.SendAfroMessageSMSLatest(ctx, receiverPhone, message, nil)
case domain.TwilioSms: case domain.TwilioSms:
return s.SendTwilioSMS(ctx, receiverPhone, message) return s.SendTwilioSMS(ctx, receiverPhone, message)
default: default:
@ -73,6 +63,79 @@ func (s *Service) SendAfroMessageSMS(ctx context.Context, receiverPhone, message
} }
} }
func (s *Service) SendAfroMessageSMSLatest(
ctx context.Context,
receiverPhone string,
message string,
callbackURL *string, // optional
) error {
baseURL := s.config.AFROSMSConfig.AfroSMSBaseURL
// Build query parameters explicitly
params := url.Values{}
params.Set("to", receiverPhone)
params.Set("message", message)
params.Set("sender", s.config.AFRO_SMS_SENDER_NAME)
// Optional parameters
if s.config.AFROSMSConfig.AfroSMSIdentifierID != "" {
params.Set("from", s.config.AFROSMSConfig.AfroSMSIdentifierID)
}
if callbackURL != nil {
params.Set("callback", *callbackURL)
}
// Construct full URL
reqURL := fmt.Sprintf("%s?%s", baseURL, params.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return err
}
// AfroMessage authentication (API key)
req.Header.Set("Authorization", "Bearer "+s.config.AFRO_SMS_API_KEY)
req.Header.Set("Accept", "application/json")
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf(
"afromessage sms failed: status=%d response=%s",
resp.StatusCode,
string(body),
)
}
// Parse response
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return err
}
ack, ok := result["acknowledge"].(string)
if !ok || ack != "success" {
return fmt.Errorf("sms delivery failed: %v", result)
}
return nil
}
func (s *Service) SendTwilioSMS(ctx context.Context, receiverPhone, message string) error { func (s *Service) SendTwilioSMS(ctx context.Context, receiverPhone, message string) error {
accountSid := s.config.TwilioAccountSid accountSid := s.config.TwilioAccountSid
authToken := s.config.TwilioAuthToken authToken := s.config.TwilioAuthToken

View File

@ -9,12 +9,17 @@ import (
"Yimaru-Backend/internal/services/user" "Yimaru-Backend/internal/services/user"
"Yimaru-Backend/internal/web_server/ws" "Yimaru-Backend/internal/web_server/ws"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io"
"net/http"
"net/url"
// "errors" // "errors"
"log/slog" "log/slog"
"sync" "sync"
"time" "time"
// "github.com/segmentio/kafka-go" // "github.com/segmentio/kafka-go"
"go.uber.org/zap" "go.uber.org/zap"
// afro "github.com/amanuelabay/afrosms-go" // afro "github.com/amanuelabay/afrosms-go"
@ -67,6 +72,79 @@ func New(
return svc return svc
} }
func (s *Service) SendAfroMessageSMSTemp(
ctx context.Context,
receiverPhone string,
message string,
callbackURL *string, // optional
) error {
baseURL := s.config.AFROSMSConfig.AfroSMSBaseURL
// Build query parameters explicitly
params := url.Values{}
params.Set("to", receiverPhone)
params.Set("message", message)
params.Set("sender", s.config.AFRO_SMS_SENDER_NAME)
// Optional parameters
if s.config.AFROSMSConfig.AfroSMSIdentifierID != "" {
params.Set("from", s.config.AFROSMSConfig.AfroSMSIdentifierID)
}
if callbackURL != nil {
params.Set("callback", *callbackURL)
}
// Construct full URL
reqURL := fmt.Sprintf("%s?%s", baseURL, params.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return err
}
// AfroMessage authentication (API key)
req.Header.Set("Authorization", "Bearer "+s.config.AFRO_SMS_API_KEY)
req.Header.Set("Accept", "application/json")
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf(
"afromessage sms failed: status=%d response=%s",
resp.StatusCode,
string(body),
)
}
// Parse response
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return err
}
ack, ok := result["acknowledge"].(string)
if !ok || ack != "success" {
return fmt.Errorf("sms delivery failed: %v", result)
}
return nil
}
func (s *Service) addConnection(recipientID int64, c *websocket.Conn) error { func (s *Service) addConnection(recipientID int64, c *websocket.Conn) error {
if c == nil { if c == nil {
s.mongoLogger.Warn("[NotificationSvc.AddConnection] Attempted to add nil WebSocket connection", s.mongoLogger.Warn("[NotificationSvc.AddConnection] Attempted to add nil WebSocket connection",
@ -334,13 +412,12 @@ func (s *Service) SendNotificationSMS(ctx context.Context, recipientID int64, me
if user.PhoneNumber == "" { if user.PhoneNumber == "" {
return fmt.Errorf("phone Number is invalid") return fmt.Errorf("phone Number is invalid")
} }
err = s.messengerSvc.SendSMS(ctx, user.PhoneNumber, message, user.OrganizationID) err = s.messengerSvc.SendSMS(ctx, user.PhoneNumber, message)
if err != nil { if err != nil {
s.mongoLogger.Error("[NotificationSvc.HandleNotification] Failed to send notification SMS", s.mongoLogger.Error("[NotificationSvc.HandleNotification] Failed to send notification SMS",
zap.Int64("recipient_id", recipientID), zap.Int64("recipient_id", recipientID),
zap.String("user_phone_number", user.PhoneNumber), zap.String("user_phone_number", user.PhoneNumber),
zap.String("message", message), zap.String("message", message),
zap.Int64("company_id", user.OrganizationID.Value),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
@ -371,7 +448,6 @@ func (s *Service) SendNotificationEmail(ctx context.Context, recipientID int64,
zap.Int64("recipient_id", recipientID), zap.Int64("recipient_id", recipientID),
zap.String("user_email", user.Email), zap.String("user_email", user.Email),
zap.String("message", message), zap.String("message", message),
zap.Int64("company_id", user.OrganizationID.Value),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )

View File

@ -10,6 +10,51 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
func (s *Service) ResendOtp(
ctx context.Context,
userName string,
) error {
otpCode := helpers.GenerateOTP()
message := fmt.Sprintf(
"Welcome to Yimaru Online Learning Platform, your OTP is %s please don't share with anyone.",
otpCode,
)
otp, err := s.otpStore.GetOtp(ctx, userName)
if err != nil {
return err
}
// Broadcast OTP (same logic as SendOtp)
switch otp.Medium {
case domain.OtpMediumSms:
if err := s.messengerSvc.SendAfroMessageSMS(ctx, otp.SentTo, message); err != nil {
return err
}
case domain.OtpMediumEmail:
if err := s.messengerSvc.SendEmail(
ctx,
otp.SentTo,
message,
message,
"Yimaru - One Time Password",
); err != nil {
return err
}
default:
return fmt.Errorf("invalid otp medium: %s", otp.Medium)
}
if err := s.otpStore.UpdateExpiredOtp(ctx, otp.Otp, userName); err != nil {
return err
}
return nil
}
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.SMSProvider) error { func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.SMSProvider) error {
otpCode := helpers.GenerateOTP() otpCode := helpers.GenerateOTP()
@ -49,6 +94,11 @@ func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpF
return s.otpStore.CreateOtp(ctx, otp) return s.otpStore.CreateOtp(ctx, otp)
} }
// helper function to get a pointer to time.Time
func timePtr(t time.Time) time.Time {
return t
}
func hashPassword(plaintextPassword string) ([]byte, error) { func hashPassword(plaintextPassword string) ([]byte, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12) hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
if err != nil { if err != nil {

View File

@ -5,36 +5,35 @@ import (
"context" "context"
) )
func (s *Service) CreateUser(ctx context.Context, User domain.CreateUserReq, is_company bool) (domain.User, error) { func (s *Service) CreateUser(
// Create User ctx context.Context,
// creator, err := s.userStore.GetUserByID(ctx, createrUserId) req domain.CreateUserReq,
// if err != nil { isCompany bool,
// return domain.User{}, err ) (domain.User, error) {
// }
// if creator.Role != domain.RoleAdmin {
// User.BranchID = creator.BranchID
// User.Role = string(domain.RoleCashier)
// } else {
// User.BranchID = branchId
// User.Role = string(domain.RoleBranchManager)
// }
hashedPassword, err := hashPassword(User.Password) // Hash the password
hashedPassword, err := hashPassword(req.Password)
if err != nil { if err != nil {
return domain.User{}, err return domain.User{}, err
} }
// Create the user
return s.userStore.CreateUserWithoutOtp(ctx, domain.User{ return s.userStore.CreateUserWithoutOtp(ctx, domain.User{
FirstName: User.FirstName, FirstName: req.FirstName,
LastName: User.LastName, LastName: req.LastName,
Email: User.Email, UserName: req.UserName,
PhoneNumber: User.PhoneNumber, Email: req.Email,
Password: hashedPassword, PhoneNumber: req.PhoneNumber,
Role: domain.Role(User.Role), Password: hashedPassword,
EmailVerified: true, Role: domain.Role(req.Role),
PhoneVerified: true, EmailVerified: true, // assuming auto-verified on creation
Suspended: User.Suspended, PhoneVerified: true,
OrganizationID: User.OrganizationID, Status: domain.UserStatusActive,
Age: req.Age,
EducationLevel: req.EducationLevel,
Country: req.Country,
Region: req.Region,
ProfileCompleted: false,
}) })
} }
@ -45,7 +44,7 @@ func (s *Service) DeleteUser(ctx context.Context, id int64) error {
func (s *Service) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) { func (s *Service) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) {
// Get all Users // Get all Users
return s.userStore.GetAllUsers(ctx, &filter.Role, &filter.OrganizationID.Value, &filter.Query.Value, &filter.CreatedBefore.Value, &filter.CreatedAfter.Value, int32(filter.PageSize.Value), int32(filter.Page.Value)) return s.userStore.GetAllUsers(ctx, &filter.Role, &filter.Query.Value, &filter.CreatedBefore.Value, &filter.CreatedAfter.Value, int32(filter.PageSize.Value), int32(filter.Page.Value))
} }
func (s *Service) GetUserById(ctx context.Context, id int64) (domain.User, error) { func (s *Service) GetUserById(ctx context.Context, id int64) (domain.User, error) {

View File

@ -1,41 +1,48 @@
package user package user
// import ( import (
// "context" "Yimaru-Backend/internal/domain"
"context"
)
// ) type UserStore interface {
IsUserPending(ctx context.Context, userName string) (bool, error)
GetUserByUserName(
ctx context.Context,
userName string,
) (domain.User, error)
VerifyOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, otpCode string) error
CreateUser(ctx context.Context, user domain.User, usedOtpId int64, is_company bool) (domain.User, error)
CreateUserWithoutOtp(ctx context.Context, user domain.User, is_company bool) (domain.User, error)
GetUserByID(ctx context.Context, id int64) (domain.User, error)
GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error)
GetAdminByCompanyID(ctx context.Context, companyID int64) (domain.User, error)
UpdateUser(ctx context.Context, user domain.UpdateUserReq) error
UpdateUserCompany(ctx context.Context, id int64, companyID int64) error
UpdateUserSuspend(ctx context.Context, id int64, status bool) error
DeleteUser(ctx context.Context, id int64) error
CheckPhoneEmailExist(ctx context.Context, phoneNum, email string, companyID domain.ValidInt64) (bool, bool, error)
GetUserByEmail(ctx context.Context, email string, companyID domain.ValidInt64) (domain.User, error)
GetUserByPhone(ctx context.Context, phoneNum string, companyID domain.ValidInt64) (domain.User, error)
SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error)
UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64, companyId int64) error
// type UserStore interface { GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
// CreateUser(ctx context.Context, user domain.User, usedOtpId int64, is_company bool) (domain.User, error) GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error)
// CreateUserWithoutOtp(ctx context.Context, user domain.User, is_company bool) (domain.User, error) GetBranchCustomerCounts(ctx context.Context, filter domain.ReportFilter) (map[int64]int64, error)
// GetUserByID(ctx context.Context, id int64) (domain.User, error) GetRoleCounts(ctx context.Context, role string, filter domain.ReportFilter) (total, active, inactive int64, err error)
// GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) }
// GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([]domain.GetCashier, int64, error) type SmsGateway interface {
// GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error) SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
// GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error) }
// GetAdminByCompanyID(ctx context.Context, companyID int64) (domain.User, error) type EmailGateway interface {
// UpdateUser(ctx context.Context, user domain.UpdateUserReq) error SendEmailOTP(ctx context.Context, email string, otp string) error
// UpdateUserCompany(ctx context.Context, id int64, companyID int64) error }
// UpdateUserSuspend(ctx context.Context, id int64, status bool) error type OtpStore interface {
// DeleteUser(ctx context.Context, id int64) error ResendOtp(
// CheckPhoneEmailExist(ctx context.Context, phoneNum, email string, companyID domain.ValidInt64) (bool, bool, error) ctx context.Context,
// GetUserByEmail(ctx context.Context, email string, companyID domain.ValidInt64) (domain.User, error) userName string,
// GetUserByPhone(ctx context.Context, phoneNum string, companyID domain.ValidInt64) (domain.User, error) ) error
// SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error) CreateOtp(ctx context.Context, otp domain.Otp) error
// UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64, companyId int64) error GetOtp(ctx context.Context, userName string) (domain.Otp, error)
}
// GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
// GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error)
// GetBranchCustomerCounts(ctx context.Context, filter domain.ReportFilter) (map[int64]int64, error)
// GetRoleCounts(ctx context.Context, role string, filter domain.ReportFilter) (total, active, inactive int64, err error)
// }
// type SmsGateway interface {
// SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
// }
// type EmailGateway interface {
// SendEmailOTP(ctx context.Context, email string, otp string) error
// }
// type OtpStore interface {
// CreateOtp(ctx context.Context, otp domain.Otp) error
// GetOtp(ctx context.Context, sentTo string, sentfor domain.OtpFor, medium domain.OtpMedium) (domain.Otp, error)
// }

View File

@ -6,18 +6,51 @@ import (
"time" "time"
) )
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string, companyID domain.ValidInt64) (bool, bool, error) { // email,phone,error func (s *Service) VerifyOtp(ctx context.Context, userName string, otpCode string) error {
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email, companyID) // 1. Retrieve the OTP from the store
storedOtp, err := s.otpStore.GetOtp(ctx, userName)
if err != nil {
return err // could be ErrOtpNotFound or other DB errors
}
// 2. Check if OTP was already used
if storedOtp.Used {
return domain.ErrOtpAlreadyUsed
}
// 3. Check if OTP has expired
if time.Now().After(storedOtp.ExpiresAt) {
return domain.ErrOtpExpired
}
// 4. Check if the provided OTP matches
if storedOtp.Otp != otpCode {
return domain.ErrInvalidOtp
}
// 5. Mark OTP as used
storedOtp.Used = true
storedOtp.UsedAt = timePtr(time.Now())
if err := s.otpStore.MarkOtpAsUsed(ctx, storedOtp); err != nil {
return err
}
return nil
} }
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider, companyID domain.ValidInt64) error { func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
}
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error {
var err error var err error
// check if user exists // check if user exists
switch medium { switch medium {
case domain.OtpMediumEmail: case domain.OtpMediumEmail:
_, err = s.userStore.GetUserByEmailPhone(ctx, sentTo, "", companyID) _, err = s.userStore.GetUserByEmailPhone(ctx, sentTo, "")
case domain.OtpMediumSms: case domain.OtpMediumSms:
_, err = s.userStore.GetUserByEmailPhone(ctx, "", sentTo, companyID) _, err = s.userStore.GetUserByEmailPhone(ctx, "", sentTo)
} }
if err != nil && err != domain.ErrUserNotFound { if err != nil && err != domain.ErrUserNotFound {
@ -29,52 +62,68 @@ func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium,
return s.SendOtp(ctx, sentTo, domain.OtpRegister, medium, provider) return s.SendOtp(ctx, sentTo, domain.OtpRegister, medium, provider)
} }
func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) { // normal func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) {
// get otp // Check if the email or phone is already registered based on OTP medium
phoneExists, emailExists, err := s.userStore.CheckPhoneEmailExist(ctx, registerReq.PhoneNumber, registerReq.Email)
if err != nil {
return domain.User{}, err
}
if registerReq.OtpMedium == domain.OtpMediumEmail {
if emailExists {
return domain.User{}, domain.ErrEmailAlreadyRegistered
}
} else {
if phoneExists {
return domain.User{}, domain.ErrPhoneAlreadyRegistered
}
}
// Hash the password
hashedPassword, err := hashPassword(registerReq.Password)
if err != nil {
return domain.User{}, err
}
// Prepare the user
userR := domain.User{
FirstName: registerReq.FirstName,
LastName: registerReq.LastName,
UserName: registerReq.UserName,
Email: registerReq.Email,
PhoneNumber: registerReq.PhoneNumber,
Password: hashedPassword,
Role: domain.RoleStudent,
EmailVerified: false, // verification pending via OTP
PhoneVerified: false,
EducationLevel: registerReq.EducationLevel,
Age: registerReq.Age,
Country: registerReq.Country,
Region: registerReq.Region,
Status: domain.UserStatusPending,
ProfileCompleted: false,
PreferredLanguage: registerReq.PreferredLanguage,
CreatedAt: time.Now(),
}
var sentTo string var sentTo string
// var provider domain.Provid
if registerReq.OtpMedium == domain.OtpMediumEmail { if registerReq.OtpMedium == domain.OtpMediumEmail {
sentTo = registerReq.Email sentTo = registerReq.Email
} else { } else {
sentTo = registerReq.PhoneNumber sentTo = registerReq.PhoneNumber
} }
//
otp, err := s.otpStore.GetOtp( // Send OTP to the user (email/SMS)
ctx, sentTo, if err := s.SendOtp(ctx, sentTo, domain.OtpRegister, registerReq.OtpMedium, domain.TwilioSms); err != nil {
domain.OtpRegister, registerReq.OtpMedium)
if err != nil {
return domain.User{}, err return domain.User{}, err
} }
// verify otp
if otp.Used {
return domain.User{}, domain.ErrOtpAlreadyUsed
}
if time.Now().After(otp.ExpiresAt) {
return domain.User{}, domain.ErrOtpExpired
}
if otp.Otp != registerReq.Otp {
return domain.User{}, domain.ErrInvalidOtp
}
hashedPassword, err := hashPassword(registerReq.Password) // Create the user (no OTP validation yet)
if err != nil { user, err := s.userStore.CreateUserWithoutOtp(ctx, userR)
return domain.User{}, err
}
userR := domain.User{
FirstName: registerReq.FirstName,
LastName: registerReq.LastName,
Email: registerReq.Email,
PhoneNumber: registerReq.PhoneNumber,
Password: hashedPassword,
Role: domain.RoleStudent,
EmailVerified: registerReq.OtpMedium == domain.OtpMediumEmail,
PhoneVerified: registerReq.OtpMedium == domain.OtpMediumSms,
OrganizationID: registerReq.OrganizationID,
}
// create the user and mark otp as used
user, err := s.userStore.CreateUser(ctx, userR, otp.ID)
if err != nil { if err != nil {
return domain.User{}, err return domain.User{}, err
} }
return user, nil return user, nil
} }

View File

@ -7,15 +7,15 @@ import (
"time" "time"
) )
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider, companyID domain.ValidInt64) error { func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error {
var err error var err error
// check if user exists // check if user exists
switch medium { switch medium {
case domain.OtpMediumEmail: case domain.OtpMediumEmail:
_, err = s.userStore.GetUserByEmailPhone(ctx, sentTo, "", companyID) _, err = s.userStore.GetUserByEmailPhone(ctx, sentTo, "")
case domain.OtpMediumSms: case domain.OtpMediumSms:
_, err = s.userStore.GetUserByEmailPhone(ctx, "", sentTo, companyID) _, err = s.userStore.GetUserByEmailPhone(ctx, "", sentTo)
} }
if err != nil { if err != nil {
@ -27,37 +27,28 @@ func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, se
} }
func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswordReq) error { func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswordReq) error {
var sentTo string
if resetReq.OtpMedium == domain.OtpMediumEmail {
sentTo = resetReq.Email
} else {
sentTo = resetReq.PhoneNumber
}
otp, err := s.otpStore.GetOtp( otp, err := s.otpStore.GetOtp(ctx, resetReq.UserName)
ctx, sentTo,
domain.OtpReset, resetReq.OtpMedium)
if err != nil { if err != nil {
return err return err
} }
//
user, err := s.userStore.GetUserByUserName(ctx, resetReq.UserName)
if err != nil {
return err
}
if otp.Used { if otp.Used {
return domain.ErrOtpAlreadyUsed return domain.ErrOtpAlreadyUsed
} }
if time.Now().After(otp.ExpiresAt) { if time.Now().After(otp.ExpiresAt) {
return domain.ErrOtpExpired return domain.ErrOtpExpired
} }
if otp.Otp != resetReq.Otp { if otp.Otp != resetReq.OtpCode {
return domain.ErrInvalidOtp return domain.ErrInvalidOtp
} }
// hash password
// hashedPassword, err := hashPassword(resetReq.Password)
// if err != nil {
// return err
// }
// reset pass and mark otp as used
err = s.userStore.UpdatePassword(ctx, resetReq.Password, resetReq.Email, resetReq.PhoneNumber, resetReq.OrganizationID, time.Now()) err = s.userStore.UpdatePassword(ctx, resetReq.Password, user.Email, user.PhoneNumber, time.Now())
if err != nil { if err != nil {
return err return err
} }

View File

@ -3,41 +3,54 @@ package user
import ( import (
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
"context" "context"
"strconv"
) )
func (s *Service) SearchUserByNameOrPhone(ctx context.Context, searchString string, role *int64, companyID *string) ([]domain.User, error) { func (s *Service) IsUserPending(ctx context.Context, userName string) (bool, error) {
return s.userStore.IsUserPending(ctx, userName)
}
func (s *Service) IsUserNameUnique(ctx context.Context, userName string) (bool, error) {
return s.userStore.IsUserNameUnique(ctx, userName)
}
func (s *Service) GetUserByUserName(
ctx context.Context,
userName string,
) (domain.User, error) {
return s.userStore.GetUserByUserName(ctx, userName)
}
func (s *Service) SearchUserByNameOrPhone(ctx context.Context, searchString string, role *int64) ([]domain.User, error) {
// Search user // Search user
return s.userStore.SearchUserByNameOrPhone(ctx, searchString, role, companyID) var roleStr *string
if role != nil {
} tmp := strconv.FormatInt(*role, 10)
func (s *Service) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error { roleStr = &tmp
// update user
newUser := domain.User{
ID: user.UserID,
FirstName: user.FirstName.Value,
LastName: user.LastName.Value,
NickName: user.NickName.Value,
Age: user.Age.Value,
EducationLevel: user.EducationLevel.Value,
Country: user.Country.Value,
Region: user.Region.Value,
Suspended: user.Suspended.Value,
OrganizationID: user.OrganizationID,
} }
return s.userStore.SearchUserByNameOrPhone(ctx, searchString, roleStr)
}
func (s *Service) UpdateUser(ctx context.Context, req domain.UpdateUserReq) error {
newUser := domain.User{
ID: req.UserID,
FirstName: req.FirstName.Value,
LastName: req.LastName.Value,
UserName: req.UserName.Value,
Age: req.Age.Value,
EducationLevel: req.EducationLevel.Value,
Country: req.Country.Value,
Region: req.Region.Value,
}
// Update user in the store
return s.userStore.UpdateUser(ctx, newUser) return s.userStore.UpdateUser(ctx, newUser)
} }
// func (s *Service) UpdateUserCompany(ctx context.Context, id int64, companyID int64) error { // func (s *Service) UpdateUserSuspend(ctx context.Context, id int64, status bool) error {
// // update user // // update user
// return s.userStore.UpdateUserCompany(ctx, id, companyID) // return s.userStore.UpdateUserSuspend(ctx, id, status)
// } // }
func (s *Service) UpdateUserSuspend(ctx context.Context, id int64, status bool) error {
// update user
return s.userStore.UpdateUserSuspend(ctx, id, status)
}
func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) { func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
return s.userStore.GetUserByID(ctx, id) return s.userStore.GetUserByID(ctx, id)
} }

View File

@ -18,7 +18,6 @@ type CreateAdminReq struct {
Email string `json:"email" example:"john.doe@example.com"` Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"` PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"` Password string `json:"password" example:"password123"`
OrganizationID *int64 `json:"company_id,omitempty" example:"1"`
} }
// CreateAdmin godoc // CreateAdmin godoc
@ -34,7 +33,7 @@ type CreateAdminReq struct {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/admin [post] // @Router /api/v1/admin [post]
func (h *Handler) CreateAdmin(c *fiber.Ctx) error { func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
var OrganizationID domain.ValidInt64 // var OrganizationID domain.ValidInt64
var req CreateAdminReq var req CreateAdminReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
@ -60,36 +59,35 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, errMsg) return fiber.NewError(fiber.StatusBadRequest, errMsg)
} }
if req.OrganizationID == nil { // if req.OrganizationID == nil {
OrganizationID = domain.ValidInt64{ // OrganizationID = domain.ValidInt64{
Value: 0, // Value: 0,
Valid: false, // Valid: false,
} // }
} else { // } else {
// _, err := h.companySvc.GetCompanyByID(c.Context(), *req.OrganizationID) // // _, err := h.companySvc.GetCompanyByID(c.Context(), *req.OrganizationID)
// if err != nil { // // if err != nil {
// h.mongoLoggerSvc.Error("invalid company ID for CreateAdmin", // // h.mongoLoggerSvc.Error("invalid company ID for CreateAdmin",
// zap.Int64("status_code", fiber.StatusInternalServerError), // // zap.Int64("status_code", fiber.StatusInternalServerError),
// zap.Int64("company_id", *req.OrganizationID), // // zap.Int64("company_id", *req.OrganizationID),
// zap.Error(err), // // zap.Error(err),
// zap.Time("timestamp", time.Now()), // // zap.Time("timestamp", time.Now()),
// ) // // )
// return fiber.NewError(fiber.StatusInternalServerError, "Company ID is invalid:"+err.Error()) // // return fiber.NewError(fiber.StatusInternalServerError, "Company ID is invalid:"+err.Error())
// } // // }
OrganizationID = domain.ValidInt64{ // OrganizationID = domain.ValidInt64{
Value: *req.OrganizationID, // Value: *req.OrganizationID,
Valid: true, // Valid: true,
} // }
} // }
user := domain.CreateUserReq{ user := domain.CreateUserReq{
FirstName: req.FirstName, FirstName: req.FirstName,
LastName: req.LastName, LastName: req.LastName,
Email: req.Email, Email: req.Email,
PhoneNumber: req.PhoneNumber, PhoneNumber: req.PhoneNumber,
Password: req.Password, Password: req.Password,
Role: string(domain.RoleAdmin), Role: string(domain.RoleAdmin),
OrganizationID: OrganizationID,
} }
newUser, err := h.userSvc.CreateUser(c.Context(), user, true) newUser, err := h.userSvc.CreateUser(c.Context(), user, true)
@ -162,7 +160,6 @@ type AdminRes struct {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/admin [get] // @Router /api/v1/admin [get]
func (h *Handler) GetAllAdmins(c *fiber.Ctx) error { func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
searchQuery := c.Query("query") searchQuery := c.Query("query")
searchString := domain.ValidString{ searchString := domain.ValidString{
Value: searchQuery, Value: searchQuery,
@ -172,38 +169,32 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
createdBeforeQuery := c.Query("created_before") createdBeforeQuery := c.Query("created_before")
var createdBefore domain.ValidTime var createdBefore domain.ValidTime
if createdBeforeQuery != "" { if createdBeforeQuery != "" {
createdBeforeParsed, err := time.Parse(time.RFC3339, createdBeforeQuery) parsed, err := time.Parse(time.RFC3339, createdBeforeQuery)
if err != nil { if err != nil {
h.logger.Info("invalid start_time format", "error", err) h.logger.Info("invalid created_before format", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") return fiber.NewError(fiber.StatusBadRequest, "Invalid created_before format")
}
createdBefore = domain.ValidTime{
Value: createdBeforeParsed,
Valid: true,
} }
createdBefore = domain.ValidTime{Value: parsed, Valid: true}
} }
createdAfterQuery := c.Query("created_after") createdAfterQuery := c.Query("created_after")
var createdAfter domain.ValidTime var createdAfter domain.ValidTime
if createdAfterQuery != "" { if createdAfterQuery != "" {
createdAfterParsed, err := time.Parse(time.RFC3339, createdAfterQuery) parsed, err := time.Parse(time.RFC3339, createdAfterQuery)
if err != nil { if err != nil {
h.logger.Info("invalid start_time format", "error", err) h.logger.Info("invalid created_after format", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") return fiber.NewError(fiber.StatusBadRequest, "Invalid created_after format")
}
createdAfter = domain.ValidTime{
Value: createdAfterParsed,
Valid: true,
} }
createdAfter = domain.ValidTime{Value: parsed, Valid: true}
} }
companyFilter := int64(c.QueryInt("company_id")) // companyID := int64(c.QueryInt("company_id"))
filter := domain.UserFilter{ filter := domain.UserFilter{
Role: string(domain.RoleAdmin), Role: string(domain.RoleAdmin),
OrganizationID: domain.ValidInt64{ // OrganizationID: domain.ValidInt64{
Value: companyFilter, // Value: companyID,
Valid: companyFilter != 0, // Valid: companyID != 0,
}, // },
Page: domain.ValidInt{ Page: domain.ValidInt{
Value: c.QueryInt("page", 1) - 1, Value: c.QueryInt("page", 1) - 1,
Valid: true, Valid: true,
@ -217,49 +208,44 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
CreatedAfter: createdAfter, CreatedAfter: createdAfter,
} }
valErrs, ok := h.validator.Validate(c, filter) if valErrs, ok := h.validator.Validate(c, filter); !ok {
if !ok {
var errMsg string var errMsg string
for field, msg := range valErrs { for f, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg) errMsg += fmt.Sprintf("%s: %s; ", f, msg)
} }
h.mongoLoggerSvc.Info("invalid filter values in GetAllAdmins request", h.mongoLoggerSvc.Info("invalid filter values in GetAllAdmins request",
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.Any("validation_errors", valErrs), zap.Any("validation_errors", valErrs),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()))
)
return fiber.NewError(fiber.StatusBadRequest, errMsg) return fiber.NewError(fiber.StatusBadRequest, errMsg)
} }
admins, total, err := h.userSvc.GetAllUsers(c.Context(), filter) admins, total, err := h.userSvc.GetAllUsers(c.Context(), filter)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("failed to get admins from user service", h.mongoLoggerSvc.Error("failed to get admins",
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
zap.Any("filter", filter), zap.Any("filter", filter),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()))
) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get admins: "+err.Error())
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get Admins"+err.Error())
} }
result := make([]AdminRes, len(admins)) result := make([]AdminRes, len(admins))
for index, admin := range admins { for i, admin := range admins {
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), admin.ID) lastLogin, err := h.authSvc.GetLastLogin(c.Context(), admin.ID)
if err != nil { if err != nil && err != authentication.ErrRefreshTokenNotFound {
if err == authentication.ErrRefreshTokenNotFound { h.mongoLoggerSvc.Error("failed to get last login",
lastLogin = &admin.CreatedAt zap.Int("status_code", fiber.StatusInternalServerError),
} else { zap.Int64("admin_id", admin.ID),
h.mongoLoggerSvc.Error("failed to get last login for admin", zap.Error(err),
zap.Int("status_code", fiber.StatusInternalServerError), zap.Time("timestamp", time.Now()))
zap.Int64("admin_id", admin.ID), return fiber.NewError(fiber.StatusInternalServerError, "Failed to get last login: "+err.Error())
zap.Error(err), }
zap.Time("timestamp", time.Now()), if err == authentication.ErrRefreshTokenNotFound {
) lastLogin = &admin.CreatedAt
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login"+err.Error())
}
} }
result[index] = AdminRes{ result[i] = AdminRes{
ID: admin.ID, ID: admin.ID,
FirstName: admin.FirstName, FirstName: admin.FirstName,
LastName: admin.LastName, LastName: admin.LastName,
@ -269,9 +255,6 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
EmailVerified: admin.EmailVerified, EmailVerified: admin.EmailVerified,
PhoneVerified: admin.PhoneVerified, PhoneVerified: admin.PhoneVerified,
CreatedAt: admin.CreatedAt, CreatedAt: admin.CreatedAt,
UpdatedAt: admin.UpdatedAt,
SuspendedAt: admin.SuspendedAt,
Suspended: admin.Suspended,
LastLogin: *lastLogin, LastLogin: *lastLogin,
} }
} }
@ -299,38 +282,20 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/admin/{id} [get] // @Router /api/v1/admin/{id} [get]
func (h *Handler) GetAdminByID(c *fiber.Ctx) error { func (h *Handler) GetAdminByID(c *fiber.Ctx) error {
userIDstr := c.Params("id") idStr := c.Params("id")
userID, err := strconv.ParseInt(userIDstr, 10, 64) id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("invalid admin ID param",
zap.Int("status_code", fiber.StatusBadRequest),
zap.String("param", userIDstr),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid admin ID") return fiber.NewError(fiber.StatusBadRequest, "Invalid admin ID")
} }
user, err := h.userSvc.GetUserByID(c.Context(), userID) user, err := h.userSvc.GetUserByID(c.Context(), id)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("failed to fetch admin by ID", return fiber.NewError(fiber.StatusInternalServerError, "Failed to get admin: "+err.Error())
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Int64("admin_id", userID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get admin"+err.Error())
} }
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil && err != authentication.ErrRefreshTokenNotFound { if err != nil && err != authentication.ErrRefreshTokenNotFound {
h.mongoLoggerSvc.Error("failed to get admin last login", return fiber.NewError(fiber.StatusInternalServerError, "Failed to get last login: "+err.Error())
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Int64("admin_id", user.ID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login:"+err.Error())
} }
if err == authentication.ErrRefreshTokenNotFound { if err == authentication.ErrRefreshTokenNotFound {
lastLogin = &user.CreatedAt lastLogin = &user.CreatedAt
@ -346,18 +311,9 @@ func (h *Handler) GetAdminByID(c *fiber.Ctx) error {
EmailVerified: user.EmailVerified, EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified, PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt, CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
SuspendedAt: user.SuspendedAt,
Suspended: user.Suspended,
LastLogin: *lastLogin, LastLogin: *lastLogin,
} }
h.mongoLoggerSvc.Info("admin retrieved successfully",
zap.Int("status_code", fiber.StatusOK),
zap.Int64("admin_id", user.ID),
zap.Time("timestamp", time.Now()),
)
return response.WriteJSON(c, fiber.StatusOK, "Admin retrieved successfully", res, nil) return response.WriteJSON(c, fiber.StatusOK, "Admin retrieved successfully", res, nil)
} }
@ -365,7 +321,6 @@ type updateAdminReq struct {
FirstName string `json:"first_name" example:"John"` FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"` LastName string `json:"last_name" example:"Doe"`
Suspended bool `json:"suspended" example:"false"` Suspended bool `json:"suspended" example:"false"`
OrganizationID *int64 `json:"company_id,omitempty" example:"1"`
} }
// UpdateAdmin godoc // UpdateAdmin godoc
@ -383,50 +338,25 @@ type updateAdminReq struct {
func (h *Handler) UpdateAdmin(c *fiber.Ctx) error { func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
var req updateAdminReq var req updateAdminReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Error("UpdateAdmin failed - invalid request body", return fiber.NewError(fiber.StatusBadRequest, "Invalid request body: "+err.Error())
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error())
} }
valErrs, ok := h.validator.Validate(c, req) adminIDStr := c.Params("id")
if !ok { adminID, err := strconv.ParseInt(adminIDStr, 10, 64)
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
h.mongoLoggerSvc.Error("UpdateAdmin failed - validation errors",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Any("validation_errors", valErrs),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
AdminIDStr := c.Params("id")
AdminID, err := strconv.ParseInt(AdminIDStr, 10, 64)
if err != nil { if err != nil {
h.mongoLoggerSvc.Info("UpdateAdmin failed - invalid Admin ID param", return fiber.NewError(fiber.StatusBadRequest, "Invalid admin ID")
zap.Int("status_code", fiber.StatusBadRequest),
zap.String("admin_id_param", AdminIDStr),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid Admin ID")
} }
var OrganizationID domain.ValidInt64 // var orgID domain.ValidInt64
if req.OrganizationID != nil { // if req.OrganizationID != nil {
OrganizationID = domain.ValidInt64{ // orgID = domain.ValidInt64{
Value: *req.OrganizationID, // Value: *req.OrganizationID,
Valid: true, // Valid: true,
} // }
} // }
err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{ err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{
UserID: AdminID, UserID: adminID,
FirstName: domain.ValidString{ FirstName: domain.ValidString{
Value: req.FirstName, Value: req.FirstName,
Valid: req.FirstName != "", Valid: req.FirstName != "",
@ -435,46 +365,11 @@ func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
Value: req.LastName, Value: req.LastName,
Valid: req.LastName != "", Valid: req.LastName != "",
}, },
Suspended: domain.ValidBool{ // OrganizationID: orgID,
Value: req.Suspended,
Valid: true,
},
OrganizationID: OrganizationID,
}) })
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("UpdateAdmin failed - user service error", return fiber.NewError(fiber.StatusInternalServerError, "Failed to update admin: "+err.Error())
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Int64("admin_id", AdminID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update admin:"+err.Error())
} }
// if req.OrganizationID != nil { return response.WriteJSON(c, fiber.StatusOK, "Admin updated successfully", nil, nil)
// _, err := h.companySvc.UpdateCompany(c.Context(), domain.UpdateCompany{
// ID: *req.OrganizationID,
// AdminID: domain.ValidInt64{
// Value: AdminID,
// Valid: true,
// },
// })
// if err != nil {
// h.mongoLoggerSvc.Error("UpdateAdmin failed to update company",
// zap.Int("status_code", fiber.StatusInternalServerError),
// zap.Int64("admin_id", AdminID),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusInternalServerError, "Failed to update company:"+err.Error())
// }
// }
h.mongoLoggerSvc.Info("UpdateAdmin succeeded",
zap.Int("status_code", fiber.StatusOK),
zap.Int64("admin_id", AdminID),
zap.Time("timestamp", time.Now()),
)
return response.WriteJSON(c, fiber.StatusOK, "Managers updated successfully", nil, nil)
} }

View File

@ -13,41 +13,40 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
// loginCustomerReq represents the request body for the LoginCustomer endpoint. // loginUserReq represents the request body for the Loginuser endpoint.
type loginCustomerReq struct { type loginUserReq struct {
Email string `json:"email" validate:"required_without=PhoneNumber" example:"john.doe@example.com"` UserName string `json:"user_name" validate:"required" example:"johndoe"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` Password string `json:"password" validate:"required" example:"password123"`
Password string `json:"password" validate:"required" example:"password123"`
} }
// loginCustomerRes represents the response body for the LoginCustomer endpoint. // loginUserRes represents the response body for the Loginuser endpoint.
type loginCustomerRes struct { type loginUserRes struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
Role string `json:"role"` Role string `json:"role"`
} }
// LoginCustomer godoc // Loginuser godoc
// @Summary Login customer // @Summary Login user
// @Description Login customer // @Description Login user
// @Tags auth // @Tags auth
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param login body loginCustomerReq true "Login customer" // @Param login body loginUserReq true "Login user"
// @Success 200 {object} loginCustomerRes // @Success 200 {object} loginUserRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/customer-login [post] // @Router /api/v1/{tenant_slug}/user-login [post]
func (h *Handler) LoginCustomer(c *fiber.Ctx) error { func (h *Handler) LoginUser(c *fiber.Ctx) error {
OrganizationID := c.Locals("company_id").(domain.ValidInt64) // OrganizationID := c.Locals("company_id").(domain.ValidInt64)
if !OrganizationID.Valid { // if !OrganizationID.Valid {
h.BadRequestLogger().Error("invalid company id") // h.BadRequestLogger().Error("invalid company id")
return fiber.NewError(fiber.StatusBadRequest, "invalid company id") // return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
} // }
var req loginCustomerReq var req loginUserReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse LoginCustomer request", h.mongoLoggerSvc.Info("Failed to parse LoginUser request",
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
@ -63,15 +62,14 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, errMsg) return fiber.NewError(fiber.StatusBadRequest, errMsg)
} }
successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password, OrganizationID) successRes, err := h.authSvc.Login(c.Context(), req.UserName, req.Password)
if err != nil { if err != nil {
switch { switch {
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials", h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
zap.Int("status_code", fiber.StatusUnauthorized), zap.Int("status_code", fiber.StatusUnauthorized),
zap.String("email", req.Email), zap.String("user_name", req.UserName),
zap.String("phone", req.PhoneNumber),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
@ -79,8 +77,7 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
case errors.Is(err, authentication.ErrUserSuspended): case errors.Is(err, authentication.ErrUserSuspended):
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked", h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
zap.Int("status_code", fiber.StatusUnauthorized), zap.Int("status_code", fiber.StatusUnauthorized),
zap.String("email", req.Email), zap.String("user_name", req.UserName),
zap.String("phone", req.PhoneNumber),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
@ -96,21 +93,19 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
} }
if successRes.Role != domain.RoleStudent { if successRes.Role != domain.RoleStudent {
h.mongoLoggerSvc.Info("Login attempt: customer login of other role", h.mongoLoggerSvc.Info("Login attempt: user login of other role",
zap.Int("status_code", fiber.StatusForbidden), zap.Int("status_code", fiber.StatusForbidden),
zap.String("role", string(successRes.Role)), zap.String("role", string(successRes.Role)),
zap.String("email", req.Email), zap.String("user_name", req.UserName),
zap.String("phone", req.PhoneNumber),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusForbidden, "Only customers are allowed to login ") return fiber.NewError(fiber.StatusForbidden, "Only users are allowed to login ")
} }
accessToken, err := jwtutil.CreateJwt( accessToken, err := jwtutil.CreateJwt(
successRes.UserId, successRes.UserId,
successRes.Role, successRes.Role,
successRes.CompanyID,
h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessKey,
h.jwtConfig.JwtAccessExpiry, h.jwtConfig.JwtAccessExpiry,
) )
@ -124,7 +119,7 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token")
} }
res := loginCustomerRes{ res := loginUserRes{
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: successRes.RfToken, RefreshToken: successRes.RfToken,
Role: string(successRes.Role), Role: string(successRes.Role),
@ -142,9 +137,8 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
// loginAdminReq represents the request body for the LoginAdmin endpoint. // loginAdminReq represents the request body for the LoginAdmin endpoint.
type loginAdminReq struct { type loginAdminReq struct {
Email string `json:"email" validate:"email" example:"john.doe@example.com"` UserName string `json:"user_name" validate:"required" example:"adminuser"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` Password string `json:"password" validate:"required" example:"password123"`
Password string `json:"password" validate:"required" example:"password123"`
} }
// loginAdminRes represents the response body for the LoginAdmin endpoint. // loginAdminRes represents the response body for the LoginAdmin endpoint.
@ -155,8 +149,8 @@ type LoginAdminRes struct {
} }
// LoginAdmin godoc // LoginAdmin godoc
// @Summary Login customer // @Summary Login user
// @Description Login customer // @Description Login user
// @Tags auth // @Tags auth
// @Accept json // @Accept json
// @Produce json // @Produce json
@ -167,11 +161,6 @@ type LoginAdminRes struct {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/admin-login [post] // @Router /api/v1/{tenant_slug}/admin-login [post]
func (h *Handler) LoginAdmin(c *fiber.Ctx) error { func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
OrganizationID := c.Locals("company_id").(domain.ValidInt64)
if !OrganizationID.Valid {
h.BadRequestLogger().Error("invalid company id")
return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
}
var req loginAdminReq var req loginAdminReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request", h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
@ -190,14 +179,13 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, errMsg) return fiber.NewError(fiber.StatusBadRequest, errMsg)
} }
successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password, OrganizationID) successRes, err := h.authSvc.Login(c.Context(), req.UserName, req.Password)
if err != nil { if err != nil {
switch { switch {
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials", h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.String("email", req.Email), zap.String("user_name", req.UserName),
zap.String("phone", req.PhoneNumber),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
@ -205,8 +193,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
case errors.Is(err, authentication.ErrUserSuspended): case errors.Is(err, authentication.ErrUserSuspended):
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked", h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
zap.Int("status_code", fiber.StatusForbidden), zap.Int("status_code", fiber.StatusForbidden),
zap.String("email", req.Email), zap.String("user_name", req.UserName),
zap.String("phone", req.PhoneNumber),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
@ -222,18 +209,17 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
} }
if successRes.Role == domain.RoleStudent || successRes.Role == domain.RoleInstructor { if successRes.Role == domain.RoleStudent || successRes.Role == domain.RoleInstructor {
h.mongoLoggerSvc.Warn("Login attempt: admin login of customer", h.mongoLoggerSvc.Warn("Login attempt: admin login of user",
zap.Int("status_code", fiber.StatusForbidden), zap.Int("status_code", fiber.StatusForbidden),
zap.String("role", string(successRes.Role)), zap.String("role", string(successRes.Role)),
zap.String("email", req.Email), zap.String("user_name", req.UserName),
zap.String("phone", req.PhoneNumber),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusForbidden, "Only admin roles are allowed") return fiber.NewError(fiber.StatusForbidden, "Only admin roles are allowed")
} }
accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Failed to create access token", h.mongoLoggerSvc.Error("Failed to create access token",
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
@ -244,7 +230,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token")
} }
res := loginCustomerRes{ res := loginUserRes{
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: successRes.RfToken, RefreshToken: successRes.RfToken,
Role: string(successRes.Role), Role: string(successRes.Role),
@ -291,14 +277,13 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, errMsg) return fiber.NewError(fiber.StatusBadRequest, errMsg)
} }
successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password, domain.ValidInt64{}) successRes, err := h.authSvc.Login(c.Context(), req.UserName, req.Password)
if err != nil { if err != nil {
switch { switch {
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials", h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.String("email", req.Email), zap.String("user_name", req.UserName),
zap.String("phone", req.PhoneNumber),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
@ -306,8 +291,7 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
case errors.Is(err, authentication.ErrUserSuspended): case errors.Is(err, authentication.ErrUserSuspended):
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked", h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
zap.Int("status_code", fiber.StatusForbidden), zap.Int("status_code", fiber.StatusForbidden),
zap.String("email", req.Email), zap.String("user_name", req.UserName),
zap.String("phone", req.PhoneNumber),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
@ -326,15 +310,14 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
h.mongoLoggerSvc.Warn("Login attempt: super-admin login of non-super-admin", h.mongoLoggerSvc.Warn("Login attempt: super-admin login of non-super-admin",
zap.Int("status_code", fiber.StatusForbidden), zap.Int("status_code", fiber.StatusForbidden),
zap.String("role", string(successRes.Role)), zap.String("role", string(successRes.Role)),
zap.String("email", req.Email), zap.String("user_name", req.UserName),
zap.String("phone", req.PhoneNumber),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusForbidden, "Only admin roles are allowed") return fiber.NewError(fiber.StatusForbidden, "Only admin roles are allowed")
} }
accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Failed to create access token", h.mongoLoggerSvc.Error("Failed to create access token",
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
@ -345,7 +328,7 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token")
} }
res := loginCustomerRes{ res := loginUserRes{
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: successRes.RfToken, RefreshToken: successRes.RfToken,
Role: string(successRes.Role), Role: string(successRes.Role),
@ -373,14 +356,14 @@ type refreshToken struct {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param refresh body refreshToken true "tokens" // @Param refresh body refreshToken true "tokens"
// @Success 200 {object} loginCustomerRes // @Success 200 {object} loginUserRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/auth/refresh [post] // @Router /api/v1/auth/refresh [post]
func (h *Handler) RefreshToken(c *fiber.Ctx) error { func (h *Handler) RefreshToken(c *fiber.Ctx) error {
type loginCustomerRes struct { type loginUserRes struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
Role string `json:"role"` Role string `json:"role"`
@ -451,7 +434,7 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user information:"+err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user information:"+err.Error())
} }
accessToken, err := jwtutil.CreateJwt(user.ID, user.Role, user.OrganizationID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) accessToken, err := jwtutil.CreateJwt(user.ID, user.Role, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Failed to create new access token", h.mongoLoggerSvc.Error("Failed to create new access token",
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
@ -462,7 +445,7 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token:"+err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token:"+err.Error())
} }
res := loginCustomerRes{ res := loginUserRes{
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: req.RefreshToken, RefreshToken: req.RefreshToken,
Role: string(user.Role), Role: string(user.Role),
@ -482,22 +465,22 @@ type logoutReq struct {
RefreshToken string `json:"refresh_token" validate:"required" example:"<refresh-token>"` RefreshToken string `json:"refresh_token" validate:"required" example:"<refresh-token>"`
} }
// LogOutCustomer godoc // LogOutuser godoc
// @Summary Logout customer // @Summary Logout user
// @Description Logout customer // @Description Logout user
// @Tags auth // @Tags auth
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param logout body logoutReq true "Logout customer" // @Param logout body logoutReq true "Logout user"
// @Success 200 {object} response.APIResponse // @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/auth/logout [post] // @Router /api/v1/auth/logout [post]
func (h *Handler) LogOutCustomer(c *fiber.Ctx) error { func (h *Handler) LogOutuser(c *fiber.Ctx) error {
var req logoutReq var req logoutReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse LogOutCustomer request", h.mongoLoggerSvc.Info("Failed to parse LogOutuser request",
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
@ -512,7 +495,7 @@ func (h *Handler) LogOutCustomer(c *fiber.Ctx) error {
errMsg += fmt.Sprintf("%s: %s; ", field, msg) errMsg += fmt.Sprintf("%s: %s; ", field, msg)
} }
h.mongoLoggerSvc.Info("LogOutCustomer validation failed", h.mongoLoggerSvc.Info("LogOutuser validation failed",
zap.String("errMsg", errMsg), zap.String("errMsg", errMsg),
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.Any("validation_errors", valErrs), zap.Any("validation_errors", valErrs),

View File

@ -1,455 +0,0 @@
package handlers
import (
"Yimaru-Backend/internal/domain"
"Yimaru-Backend/internal/web_server/response"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
)
type CreateManagerReq struct {
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"`
CompanyID *int64 `json:"company_id,omitempty" example:"1"`
}
// CreateManager godoc
// @Summary Create Manager
// @Description Create Manager
// @Tags manager
// @Accept json
// @Produce json
// @Param manger body CreateManagerReq true "Create manager"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/managers [post]
// func (h *Handler) CreateManager(c *fiber.Ctx) error {
// // Get user_id from middleware
// var req CreateManagerReq
// if err := c.BodyParser(&req); err != nil {
// h.logger.Error("RegisterUser failed", "error", err)
// h.mongoLoggerSvc.Info("CreateManager failed to create manager",
// zap.Int("status_code", fiber.StatusBadRequest),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error())
// }
// valErrs, ok := h.validator.Validate(c, req)
// if !ok {
// var errMsg string
// for field, msg := range valErrs {
// errMsg += fmt.Sprintf("%s: %s; ", field, msg)
// }
// h.mongoLoggerSvc.Info("Failed to validate CreateManager",
// zap.Any("request", req),
// zap.Int("status_code", fiber.StatusBadRequest),
// zap.String("errMsg", errMsg),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusBadRequest, errMsg)
// }
// var companyID domain.ValidInt64
// role := c.Locals("role").(domain.Role)
// if role == domain.RoleSuperAdmin {
// if req.CompanyID == nil {
// h.logger.Error("RegisterUser failed error: company id is required")
// h.mongoLoggerSvc.Info("RegisterUser failed error: company id is required",
// zap.Int("status_code", fiber.StatusBadRequest),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusBadRequest, "Company ID is required for super-admin")
// }
// companyID = domain.ValidInt64{
// Value: *req.CompanyID,
// Valid: true,
// }
// } else {
// companyID = c.Locals("company_id").(domain.ValidInt64)
// }
// user := domain.CreateUserReq{
// FirstName: req.FirstName,
// LastName: req.LastName,
// Email: req.Email,
// PhoneNumber: req.PhoneNumber,
// Password: req.Password,
// Role: string(domain.RoleBranchManager),
// OrganizationID: companyID,
// }
// _, err := h.userSvc.CreateUser(c.Context(), user, true)
// if err != nil {
// h.mongoLoggerSvc.Error("CreateManager failed to create manager",
// zap.Int("status_code", fiber.StatusInternalServerError),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusInternalServerError, "Failed to create manager:"+err.Error())
// }
// return response.WriteJSON(c, fiber.StatusOK, "Manager created successfully", nil, nil)
// }
type ManagersRes struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
Role domain.Role `json:"role"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastLogin time.Time `json:"last_login"`
SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"`
}
// GetAllManagers godoc
// @Summary Get all Managers
// @Description Get all Managers
// @Tags manager
// @Accept json
// @Produce json
// @Param page query int false "Page number"
// @Param page_size query int false "Page size"
// @Success 200 {object} ManagersRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/managers [get]
// func (h *Handler) GetAllManagers(c *fiber.Ctx) error {
// role := c.Locals("role").(domain.Role)
// companyId := c.Locals("company_id").(domain.ValidInt64)
// // Checking to make sure that admin user has a company id in the token
// if role != domain.RoleSuperAdmin && !companyId.Valid {
// h.mongoLoggerSvc.Error("Cannot get company ID from context",
// zap.String("role", string(role)),
// zap.Int("status_code", fiber.StatusInternalServerError),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID from context")
// }
// searchQuery := c.Query("query")
// searchString := domain.ValidString{
// Value: searchQuery,
// Valid: searchQuery != "",
// }
// createdBeforeQuery := c.Query("created_before")
// var createdBefore domain.ValidTime
// if createdBeforeQuery != "" {
// createdBeforeParsed, err := time.Parse(time.RFC3339, createdBeforeQuery)
// if err != nil {
// h.mongoLoggerSvc.Info("invalid created_before format",
// zap.String("created_before", createdBeforeQuery),
// zap.Int("status_code", fiber.StatusBadRequest),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusBadRequest, "Invalid created_before format")
// }
// createdBefore = domain.ValidTime{
// Value: createdBeforeParsed,
// Valid: true,
// }
// }
// createdAfterQuery := c.Query("created_after")
// var createdAfter domain.ValidTime
// if createdAfterQuery != "" {
// createdAfterParsed, err := time.Parse(time.RFC3339, createdAfterQuery)
// if err != nil {
// h.mongoLoggerSvc.Info("invalid created_after format",
// zap.String("created_after", createdAfterQuery),
// zap.Int("status_code", fiber.StatusBadRequest),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusBadRequest, "Invalid created_after format")
// }
// createdAfter = domain.ValidTime{
// Value: createdAfterParsed,
// Valid: true,
// }
// }
// filter := domain.UserFilter{
// Role: string(domain.RoleBranchManager),
// OrganizationID: companyId,
// Page: domain.ValidInt{
// Value: c.QueryInt("page", 1) - 1,
// Valid: true,
// },
// PageSize: domain.ValidInt{
// Value: c.QueryInt("page_size", 10),
// Valid: true,
// },
// Query: searchString,
// CreatedBefore: createdBefore,
// CreatedAfter: createdAfter,
// }
// valErrs, ok := h.validator.Validate(c, filter)
// if !ok {
// var errMsg string
// for field, msg := range valErrs {
// errMsg += fmt.Sprintf("%s: %s; ", field, msg)
// }
// h.mongoLoggerSvc.Info("Failed to validate get all filters",
// zap.Any("filter", filter),
// zap.Int("status_code", fiber.StatusBadRequest),
// zap.String("errMsg", errMsg),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusBadRequest, errMsg)
// }
// managers, total, err := h.userSvc.GetAllUsers(c.Context(), filter)
// if err != nil {
// h.logger.Error("GetAllManagers failed", "error", err)
// h.mongoLoggerSvc.Error("GetAllManagers failed to get all managers",
// zap.Any("filter", filter),
// zap.Int("status_code", fiber.StatusInternalServerError),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusInternalServerError, "Failed to get Managers"+err.Error())
// }
// var result []ManagersRes = make([]ManagersRes, len(managers))
// for index, manager := range managers {
// lastLogin, err := h.authSvc.GetLastLogin(c.Context(), manager.ID)
// if err != nil {
// if err == authentication.ErrRefreshTokenNotFound {
// lastLogin = &manager.CreatedAt
// } else {
// h.mongoLoggerSvc.Error("Failed to get user last login",
// zap.Int64("userID", manager.ID),
// zap.Int("status_code", fiber.StatusInternalServerError),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login:"+err.Error())
// }
// }
// result[index] = ManagersRes{
// ID: manager.ID,
// FirstName: manager.FirstName,
// LastName: manager.LastName,
// Email: manager.Email,
// PhoneNumber: manager.PhoneNumber,
// Role: manager.Role,
// EmailVerified: manager.EmailVerified,
// PhoneVerified: manager.PhoneVerified,
// CreatedAt: manager.CreatedAt,
// UpdatedAt: manager.UpdatedAt,
// SuspendedAt: manager.SuspendedAt,
// Suspended: manager.Suspended,
// LastLogin: *lastLogin,
// }
// }
// return response.WritePaginatedJSON(c, fiber.StatusOK, "Managers retrieved successfully", result, nil, filter.Page.Value, int(total))
// }
// GetManagerByID godoc
// @Summary Get manager by id
// @Description Get a single manager by id
// @Tags manager
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} ManagersRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/managers/{id} [get]
// func (h *Handler) GetManagerByID(c *fiber.Ctx) error {
// role := c.Locals("role").(domain.Role)
// companyId := c.Locals("company_id").(domain.ValidInt64)
// requestUserID := c.Locals("user_id").(int64)
// // Only Super Admin / Admin / Branch Manager can view this route
// if role != domain.RoleSuperAdmin && role != domain.RoleAdmin && role != domain.RoleBranchManager {
// h.mongoLoggerSvc.Warn("Attempt to access from unauthorized role",
// zap.Int64("userID", requestUserID),
// zap.String("role", string(role)),
// zap.Int("status_code", fiber.StatusForbidden),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusForbidden, "This role cannot view this route")
// }
// if role != domain.RoleSuperAdmin && !companyId.Valid {
// h.mongoLoggerSvc.Error("Cannot get company ID in context",
// zap.String("role", string(role)),
// zap.Int("status_code", fiber.StatusInternalServerError),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID in context")
// }
// userIDstr := c.Params("id")
// userID, err := strconv.ParseInt(userIDstr, 10, 64)
// if err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid managers ID")
// }
// user, err := h.userSvc.GetUserByID(c.Context(), userID)
// if err != nil {
// h.mongoLoggerSvc.Error("Failed to get manager by id",
// zap.Int64("userID", userID),
// zap.Int("status_code", fiber.StatusInternalServerError),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusInternalServerError, "Failed to get managers:"+err.Error())
// }
// // A Branch Manager can only fetch his own branch info
// if role == domain.RoleBranchManager && user.ID != requestUserID {
// h.mongoLoggerSvc.Warn("Attempt to access another branch manager info",
// zap.String("userID", userIDstr),
// zap.Int("status_code", fiber.StatusForbidden),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusForbidden, "User Access Not Allowed")
// }
// // Check that only admin from company can view this route
// if role != domain.RoleSuperAdmin && user.OrganizationID.Value != companyId.Value {
// h.mongoLoggerSvc.Warn("Attempt to access info from another company",
// zap.String("userID", userIDstr),
// zap.Int("status_code", fiber.StatusForbidden),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusForbidden, "Cannot access another company information")
// }
// lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
// if err != nil {
// if err != authentication.ErrRefreshTokenNotFound {
// h.mongoLoggerSvc.Error("Failed to get user last login",
// zap.Int64("userID", userID),
// zap.Int("status_code", fiber.StatusInternalServerError),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login"+err.Error())
// }
// lastLogin = &user.CreatedAt
// }
// res := ManagersRes{
// ID: user.ID,
// FirstName: user.FirstName,
// LastName: user.LastName,
// Email: user.Email,
// PhoneNumber: user.PhoneNumber,
// Role: user.Role,
// EmailVerified: user.EmailVerified,
// PhoneVerified: user.PhoneVerified,
// CreatedAt: user.CreatedAt,
// UpdatedAt: user.UpdatedAt,
// SuspendedAt: user.SuspendedAt,
// Suspended: user.Suspended,
// LastLogin: *lastLogin,
// }
// return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil)
// }
type updateManagerReq struct {
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Suspended bool `json:"suspended" example:"false"`
CompanyID *int64 `json:"company_id,omitempty" example:"1"`
}
// UpdateManagers godoc
// @Summary Update Managers
// @Description Update Managers
// @Tags manager
// @Accept json
// @Produce json
// @Param Managers body updateManagerReq true "Update Managers"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/managers/{id} [put]
func (h *Handler) UpdateManagers(c *fiber.Ctx) error {
var req updateManagerReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("UpdateManagers failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
}
valErrs, ok := h.validator.Validate(c, req)
if !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
ManagersIdStr := c.Params("id")
ManagersId, err := strconv.ParseInt(ManagersIdStr, 10, 64)
if err != nil {
h.logger.Error("UpdateManagers failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Managers ID", nil, nil)
}
var companyID domain.ValidInt64
role := c.Locals("role").(domain.Role)
if req.CompanyID != nil {
if role != domain.RoleSuperAdmin {
h.logger.Error("UpdateManagers failed", "error", err)
return response.WriteJSON(c, fiber.StatusUnauthorized, "This user role cannot modify company ID", nil, nil)
}
companyID = domain.ValidInt64{
Value: *req.CompanyID,
Valid: true,
}
}
err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{
UserID: ManagersId,
FirstName: domain.ValidString{
Value: req.FirstName,
Valid: req.FirstName != "",
},
LastName: domain.ValidString{
Value: req.LastName,
Valid: req.LastName != "",
},
Suspended: domain.ValidBool{
Value: req.Suspended,
Valid: true,
},
OrganizationID: companyID,
},
)
if err != nil {
h.logger.Error("UpdateManagers failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update Managers", nil, nil)
}
return response.WriteJSON(c, fiber.StatusOK, "Managers updated successfully", nil, nil)
}

View File

@ -5,6 +5,7 @@ import (
"Yimaru-Backend/internal/web_server/ws" "Yimaru-Backend/internal/web_server/ws"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@ -449,3 +450,77 @@ func (h *Handler) GetAllNotifications(c *fiber.Ctx) error {
}) })
} }
type SendSingleAfroSMSReq struct {
Recipient string `json:"recipient" validate:"required" example:"+251912345678"`
Message string `json:"message" validate:"required" example:"Hello world"`
}
// SendSingleAfroSMS godoc
// @Summary Send single SMS via AfroMessage
// @Description Sends an SMS message to a single phone number using AfroMessage
// @Tags user
// @Accept json
// @Produce json
// @Param sendSMS body SendSingleAfroSMSReq true "Send SMS request"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/sendSMS [post]
func (h *Handler) SendSingleAfroSMS(c *fiber.Ctx) error {
var req SendSingleAfroSMSReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse SendSingleAfroSMS request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to send SMS",
Error: "Invalid request body: " + err.Error(),
})
}
// Validate request
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to send SMS",
Error: errMsg,
})
}
// Send SMS via service
if err := h.notificationSvc.SendAfroMessageSMSTemp(
c.Context(),
req.Recipient,
req.Message,
nil,
); err != nil {
h.mongoLoggerSvc.Error("Failed to send AfroMessage SMS",
zap.String("phone_number", req.Recipient),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to send SMS",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "SMS sent successfully",
Success: true,
StatusCode: fiber.StatusOK,
Data: req,
})
}

View File

@ -61,7 +61,7 @@ func (h *Handler) GetReferralCode(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user id") return fiber.NewError(fiber.StatusInternalServerError, "Invalid user id")
} }
user, err := h.userSvc.GetUserByID(c.Context(), userID) _, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Failed to get user", h.mongoLoggerSvc.Error("Failed to get user",
zap.Int64("userID", userID), zap.Int64("userID", userID),
@ -72,15 +72,15 @@ func (h *Handler) GetReferralCode(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user") return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user")
} }
if !user.OrganizationID.Valid || user.OrganizationID.Value != companyID.Value { // if !user.OrganizationID.Valid || user.OrganizationID.Value != companyID.Value {
h.mongoLoggerSvc.Warn("User attempt to login to different company", // h.mongoLoggerSvc.Warn("User attempt to login to different company",
zap.Int64("userID", userID), // zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError), // zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err), // zap.Error(err),
zap.Time("timestamp", time.Now()), // zap.Time("timestamp", time.Now()),
) // )
return fiber.NewError(fiber.StatusBadRequest, "Failed to retrieve user") // return fiber.NewError(fiber.StatusBadRequest, "Failed to retrieve user")
} // }
// referrals, err := h.referralSvc.GetReferralCodesByUser(c.Context(), user.ID) // referrals, err := h.referralSvc.GetReferralCodesByUser(c.Context(), user.ID)

View File

@ -333,9 +333,8 @@ func (h *Handler) GetTransactionApproverByID(c *fiber.Ctx) error {
EmailVerified: user.EmailVerified, EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified, PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt, CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt, // SuspendedAt: user.SuspendedAt,
SuspendedAt: user.SuspendedAt, // Suspended: user.Suspended,
Suspended: user.Suspended,
LastLogin: *lastLogin, LastLogin: *lastLogin,
} }
@ -369,21 +368,20 @@ type updateTransactionApproverReq struct {
func (h *Handler) UpdateTransactionApprover(c *fiber.Ctx) error { func (h *Handler) UpdateTransactionApprover(c *fiber.Ctx) error {
var req updateTransactionApproverReq var req updateTransactionApproverReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Error("UpdateAdmin failed - invalid request body", h.mongoLoggerSvc.Error("UpdateTransactionApprover failed - invalid request body",
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body: "+err.Error())
} }
valErrs, ok := h.validator.Validate(c, req) if valErrs, ok := h.validator.Validate(c, req); !ok {
if !ok {
var errMsg string var errMsg string
for field, msg := range valErrs { for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg) errMsg += fmt.Sprintf("%s: %s; ", field, msg)
} }
h.mongoLoggerSvc.Error("UpdateAdmin failed - validation errors", h.mongoLoggerSvc.Error("UpdateTransactionApprover failed - validation errors",
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.Any("validation_errors", valErrs), zap.Any("validation_errors", valErrs),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
@ -391,20 +389,20 @@ func (h *Handler) UpdateTransactionApprover(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, errMsg) return fiber.NewError(fiber.StatusBadRequest, errMsg)
} }
ApproverIDStr := c.Params("id") approverIDStr := c.Params("id")
ApproverID, err := strconv.ParseInt(ApproverIDStr, 10, 64) approverID, err := strconv.ParseInt(approverIDStr, 10, 64)
if err != nil { if err != nil {
h.mongoLoggerSvc.Info("UpdateAdmin failed - invalid Admin ID param", h.mongoLoggerSvc.Info("UpdateTransactionApprover failed - invalid approver ID",
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.String("admin_id_param", ApproverIDStr), zap.String("approver_id_param", approverIDStr),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusBadRequest, "Invalid Admin ID") return fiber.NewError(fiber.StatusBadRequest, "Invalid approver ID")
} }
err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{ updateReq := domain.UpdateUserReq{
UserID: ApproverID, UserID: approverID,
FirstName: domain.ValidString{ FirstName: domain.ValidString{
Value: req.FirstName, Value: req.FirstName,
Valid: req.FirstName != "", Valid: req.FirstName != "",
@ -413,26 +411,25 @@ func (h *Handler) UpdateTransactionApprover(c *fiber.Ctx) error {
Value: req.LastName, Value: req.LastName,
Valid: req.LastName != "", Valid: req.LastName != "",
}, },
Suspended: domain.ValidBool{ }
Value: req.Suspended,
Valid: true, err = h.userSvc.UpdateUser(c.Context(), updateReq)
},
})
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("UpdateAdmin failed - user service error", h.mongoLoggerSvc.Error("UpdateTransactionApprover failed - user service error",
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
zap.Int64("admin_id", ApproverID), zap.Int64("approver_id", approverID),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update admin:"+err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update approver: "+err.Error())
} }
h.mongoLoggerSvc.Info("UpdateAdmin succeeded", h.mongoLoggerSvc.Info("UpdateTransactionApprover succeeded",
zap.Int("status_code", fiber.StatusOK), zap.Int("status_code", fiber.StatusOK),
zap.Int64("admin_id", ApproverID), zap.Int64("approver_id", approverID),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return response.WriteJSON(c, fiber.StatusOK, "Managers updated successfully", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "Transaction approver updated successfully", nil, nil)
} }

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,7 @@ type JwtConfig struct {
JwtAccessExpiry int JwtAccessExpiry int
} }
func CreateJwt(userId int64, Role domain.Role, CompanyID domain.ValidInt64, key string, expiry int) (string, error) { func CreateJwt(userId int64, Role domain.Role, key string, expiry int) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{ token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
Issuer: "yimaru.com", Issuer: "yimaru.com",
@ -39,10 +39,10 @@ func CreateJwt(userId int64, Role domain.Role, CompanyID domain.ValidInt64, key
}, },
UserId: userId, UserId: userId,
Role: Role, Role: Role,
CompanyID: domain.NullJwtInt64{ // CompanyID: domain.NullJwtInt64{
Value: CompanyID.Value, // Value: CompanyID.Value,
Valid: CompanyID.Valid, // Valid: CompanyID.Valid,
}, // },
}) })
jwtToken, err := token.SignedString([]byte(key)) jwtToken, err := token.SignedString([]byte(key))
return jwtToken, err return jwtToken, err

View File

@ -80,11 +80,11 @@ func (a *App) initAppRoutes() {
}) })
// Auth Routes // Auth Routes
tenant.Post("/auth/customer-login", h.LoginCustomer) tenant.Post("/auth/customer-login", h.LoginUser)
tenant.Post("/auth/admin-login", h.LoginAdmin) tenant.Post("/auth/admin-login", h.LoginAdmin)
groupV1.Post("/auth/super-login", h.LoginSuper) groupV1.Post("/auth/super-login", h.LoginSuper)
groupV1.Post("/auth/refresh", h.RefreshToken) groupV1.Post("/auth/refresh", h.RefreshToken)
groupV1.Post("/auth/logout", a.authMiddleware, h.LogOutCustomer) groupV1.Post("/auth/logout", a.authMiddleware, h.LogOutuser)
groupV1.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error { groupV1.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64) userID, ok := c.Locals("user_id").(int64)
if !ok { if !ok {
@ -122,8 +122,12 @@ func (a *App) initAppRoutes() {
// groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler // groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler
// User Routes // User Routes
groupV1.Get("/user/:user_name/is-unique", h.CheckUserNameUnique)
groupV1.Get("/user/:user_name/is-pending", h.CheckUserPending)
groupV1.Post("/user/resetPassword", h.ResetPassword) groupV1.Post("/user/resetPassword", h.ResetPassword)
groupV1.Post("/user/sendResetCode", h.SendResetCode) groupV1.Post("/user/sendResetCode", h.SendResetCode)
groupV1.Post("/user/verify-otp", h.VerifyOtp)
groupV1.Post("/user/resend-otp", h.ResendOtp)
tenant.Post("/user/resetPassword", h.ResetTenantPassword) tenant.Post("/user/resetPassword", h.ResetTenantPassword)
tenant.Post("/user/sendResetCode", h.SendTenantResetCode) tenant.Post("/user/sendResetCode", h.SendTenantResetCode)
@ -133,10 +137,9 @@ func (a *App) initAppRoutes() {
groupV1.Get("/user/admin-profile", a.authMiddleware, h.AdminProfile) groupV1.Get("/user/admin-profile", a.authMiddleware, h.AdminProfile)
tenant.Get("/user/customer-profile", a.authMiddleware, h.CustomerProfile) tenant.Get("/user/user-profile", a.authMiddleware, h.GetUserProfile)
groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID) groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID)
groupV1.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend)
groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser) groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser)
groupV1.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone) groupV1.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone)
@ -150,7 +153,6 @@ func (a *App) initAppRoutes() {
// groupV1.Post("/t-approver", a.authMiddleware, a.OnlyAdminAndAbove, h.CreateTransactionApprover) // groupV1.Post("/t-approver", a.authMiddleware, a.OnlyAdminAndAbove, h.CreateTransactionApprover)
// groupV1.Put("/t-approver/:id", a.authMiddleware, a.OnlyAdminAndAbove, h.UpdateTransactionApprover) // groupV1.Put("/t-approver/:id", a.authMiddleware, a.OnlyAdminAndAbove, h.UpdateTransactionApprover)
//mongoDB logs //mongoDB logs
groupV1.Get("/logs", a.authMiddleware, a.SuperAdminOnly, handlers.GetLogsHandler(context.Background())) groupV1.Get("/logs", a.authMiddleware, a.SuperAdminOnly, handlers.GetLogsHandler(context.Background()))
@ -160,6 +162,7 @@ func (a *App) initAppRoutes() {
// groupV1.Put("/shop/transaction/:id", a.authMiddleware, a.CompanyOnly, h.UpdateTransactionVerified) // groupV1.Put("/shop/transaction/:id", a.authMiddleware, a.CompanyOnly, h.UpdateTransactionVerified)
// Notification Routes // Notification Routes
groupV1.Post("/sendSMS", h.SendSingleAfroSMS)
groupV1.Get("/ws/connect", a.WebsocketAuthMiddleware, h.ConnectSocket) groupV1.Get("/ws/connect", a.WebsocketAuthMiddleware, h.ConnectSocket)
groupV1.Get("/notifications", a.authMiddleware, h.GetUserNotification) groupV1.Get("/notifications", a.authMiddleware, h.GetUserNotification)
groupV1.Get("/notifications/all", a.authMiddleware, h.GetAllNotifications) groupV1.Get("/notifications/all", a.authMiddleware, h.GetAllNotifications)

View File

@ -1,108 +0,0 @@
include .env
.PHONY: test
test:
@docker compose up -d test
@docker compose exec test go test ./...
@docker compose stop test
.PHONY: coverage
coverage:
@mkdir -p coverage
@docker compose up -d test
@docker compose exec test sh -c "go test -coverprofile=coverage.out ./internal/... && go tool cover -func=coverage.out -o coverage/coverage.txt"
@docker cp $(shell docker ps -q -f "name=yimaru-test-1"):/app/coverage ./ || true
@docker compose stop test
.PHONY: build
build:
@docker compose build app
.PHONY: run
run:
@docker compose up
.PHONY: stop
stop:
@docker compose down
.PHONY: air
air:
@echo "Running air locally (not in Docker)"
@air -c .air.toml
.PHONY: migrations/new
migrations/new:
@echo 'Creating migration files for DB_URL'
@migrate create -seq -ext=.sql -dir=./db/migrations $(name)
.PHONY: migrations/up
migrations/up:
@echo 'Running up migrations...'
@docker compose up migrate
.PHONY: postgres
postgres:
@echo 'Running postgres db...'
docker compose -f docker-compose.yml exec postgres psql -U root -d gh
.PHONY: backup
backup:
@mkdir -p backup
@docker exec -t yimaru-backend-postgres-1 pg_dump -U root --data-only --exclude-table=schema_migrations gh | gzip > backup/dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.gz
restore:
@echo "Restoring latest backup..."
@latest_file=$$(ls -t backup/dump_*.sql.gz | head -n 1); \
echo "Restoring from $$latest_file"; \
gunzip -c $$latest_file | docker exec -i yimaru-backend-postgres-1 psql -U root -d gh
restore_file:
@echo "Restoring latest backup..."
gunzip -c $(file) | docker exec -i yimaru-backend-postgres-1 psql -U root -d gh
.PHONY: seed_data
seed_data:
@echo "Waiting for PostgreSQL to be ready..."
@until docker exec yimaru-backend-postgres-1 pg_isready -U root -d gh; do \
echo "PostgreSQL is not ready yet..."; \
sleep 1; \
done
@for file in db/data/*.sql; do \
echo "Seeding $$file..."; \
cat $$file | docker exec -i yimaru-backend-postgres-1 psql -U root -d gh; \
done
.PHONY: seed_dev_data
seed_dev_data:
@echo "Waiting for PostgreSQL to be ready..."
@until docker exec yimaru-backend-postgres-1 pg_isready -U root -d gh; do \
echo "PostgreSQL is not ready yet..."; \
sleep 1; \
done
cat db/scripts/fix_autoincrement_desync.sql | docker exec -i yimaru-backend-postgres-1 psql -U root -d gh;
@for file in db/dev_data/*.sql; do \
if [ -f "$$file" ]; then \
echo "Seeding $$file..."; \
cat $$file | docker exec -i yimaru-backend-postgres-1 psql -U root -d gh; \
fi \
done
postgres_log:
docker logs yimaru-backend-postgres-1
.PHONY: swagger
swagger:
@swag init -g cmd/main.go
.PHONY: db-up
logs:
@mkdir -p logs
db-up: | logs
@mkdir -p logs
@docker compose up -d postgres migrate mongo
@docker logs yimaru-backend-postgres-1 > logs/postgres.log 2>&1 &
.PHONY: db-down
db-down:
@docker compose down -v
# @docker volume rm yimaru-backend_postgres_data
.PHONY: sqlc-gen
sqlc-gen:
@sqlc generate
app_log:
@mkdir -p app_logs
export_logs: | app_log
@docker exec yimaru-mongo mongoexport --db=logdb --collection=applogs -u root -p secret --authenticationDatabase=admin --out - > app_logs/log_`date +%Y-%m-%d"_"%H_%M_%S`.json