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,
first_name,
last_name,
nick_name,
user_name,
email,
phone_number,
password,
@ -128,7 +128,7 @@ VALUES
ON CONFLICT (id) DO UPDATE
SET first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name,
nick_name = EXCLUDED.nick_name,
user_name = EXCLUDED.user_name,
email = EXCLUDED.email,
phone_number = EXCLUDED.phone_number,
password = EXCLUDED.password,

View File

@ -8,11 +8,6 @@ SELECT setval(
)
FROM users;
SELECT setval(
pg_get_serial_sequence('organizations', 'id'),
COALESCE(MAX(id), 1)
)
FROM organizations;
SELECT setval(
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 courses;
-- =========================================
-- Organization Settings
-- =========================================
DROP TABLE IF EXISTS organization_settings;
DROP TABLE IF EXISTS global_settings;
-- =========================================
-- Organizations (Tenants)
-- =========================================
DROP TABLE IF EXISTS organizations;
-- =========================================
-- Authentication & Security

View File

@ -2,10 +2,11 @@ CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
first_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,
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,
age INT,
education_level VARCHAR(100),
@ -13,16 +14,19 @@ CREATE TABLE IF NOT EXISTS users (
region VARCHAR(100),
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
suspended BOOLEAN NOT NULL DEFAULT FALSE,
suspended_at TIMESTAMPTZ,
organization_id BIGINT,
status VARCHAR(50) NOT NULL, -- PENDING, ACTIVE, SUSPENDED, DEACTIVATED
last_login TIMESTAMPTZ,
profile_completed BOOLEAN NOT NULL DEFAULT FALSE,
profile_picture_url TEXT,
preferred_language VARCHAR(50),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ,
CHECK (email IS NOT NULL OR phone_number IS NOT NULL),
UNIQUE (email, organization_id),
UNIQUE (phone_number, organization_id)
CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
);
CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@ -34,9 +38,10 @@ CREATE TABLE refresh_tokens (
CREATE TABLE otps (
id BIGSERIAL PRIMARY KEY,
user_name VARCHAR(100) NOT NULL,
sent_to VARCHAR(255) NOT NULL,
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,
used BOOLEAN NOT NULL DEFAULT FALSE,
used_at TIMESTAMPTZ,
@ -44,19 +49,8 @@ CREATE TABLE otps (
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 (
id BIGSERIAL PRIMARY KEY,
organization_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
instructor_id BIGINT NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
description TEXT,
@ -170,15 +164,6 @@ CREATE TABLE global_settings (
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 (
id BIGSERIAL PRIMARY KEY,
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
INSERT INTO otps (sent_to, medium, otp_for, otp, used, created_at, expires_at)
VALUES ($1, $2, $3, $4, FALSE, $5, $6);
INSERT INTO otps (user_name, sent_to, medium, otp_for, otp, used, created_at, expires_at)
VALUES ($1, $2, $3, $4, $5, FALSE, $6, $7);
-- 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
WHERE sent_to = $1 AND otp_for = $2 AND medium = $3
WHERE user_name = $1
ORDER BY created_at DESC LIMIT 1;
-- 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
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,
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,
phone_number,
role,
@ -52,22 +65,24 @@ RETURNING id,
region,
email_verified,
phone_verified,
status,
profile_completed,
preferred_language,
created_at,
updated_at,
suspended,
suspended_at,
organization_id;
updated_at;
-- name: GetUserByID :one
SELECT *
FROM users
WHERE id = $1;
-- name: GetAllUsers :many
SELECT
COUNT(*) OVER () AS total_count,
id,
first_name,
last_name,
nick_name,
user_name,
email,
phone_number,
role,
@ -77,52 +92,44 @@ SELECT
region,
email_verified,
phone_verified,
status,
profile_completed,
preferred_language,
created_at,
updated_at,
suspended,
suspended_at,
organization_id
updated_at
FROM users
WHERE (
role = $1
OR $1 IS NULL
)
AND (
organization_id = $2
OR $2 IS NULL
role = $1 OR $1 IS NULL
)
AND (
first_name ILIKE '%' || sqlc.narg('query') || '%'
OR last_name ILIKE '%' || sqlc.narg('query') || '%'
OR phone_number ILIKE '%' || sqlc.narg('query') || '%'
OR sqlc.narg('query') IS NULL
OR last_name ILIKE '%' || sqlc.narg('query') || '%'
OR phone_number ILIKE '%' || sqlc.narg('query') || '%'
OR email ILIKE '%' || sqlc.narg('query') || '%'
OR sqlc.narg('query') IS NULL
)
AND (
created_at > sqlc.narg('created_before')
OR sqlc.narg('created_before') IS NULL
)
AND (
created_at < sqlc.narg('created_after')
created_at >= sqlc.narg('created_after')
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')
OFFSET sqlc.narg('offset');
-- name: GetTotalUsers :one
SELECT COUNT(*)
FROM users
wHERE (
role = $1
OR $1 IS NULL
)
AND (
organization_id = $2
OR $2 IS NULL
);
WHERE (role = $1 OR $1 IS NULL);
-- name: SearchUserByNameOrPhone :many
SELECT id,
SELECT
id,
first_name,
last_name,
nick_name,
user_name,
email,
phone_number,
role,
@ -132,98 +139,113 @@ SELECT id,
region,
email_verified,
phone_verified,
status,
profile_completed,
created_at,
updated_at,
suspended,
suspended_at,
organization_id
updated_at
FROM users
WHERE (
organization_id = sqlc.narg('organization_id')
OR sqlc.narg('organization_id') IS NULL
)
AND (
first_name ILIKE '%' || $1 || '%'
OR last_name ILIKE '%' || $1 || '%'
OR phone_number LIKE '%' || $1 || '%'
OR last_name ILIKE '%' || $1 || '%'
OR phone_number ILIKE '%' || $1 || '%'
OR email ILIKE '%' || $1 || '%'
)
AND (
AND (
role = sqlc.narg('role')
OR sqlc.narg('role') IS NULL
);
-- name: UpdateUser :exec
UPDATE users
SET first_name = $1,
last_name = $2,
suspended = $3,
SET
first_name = $1,
last_name = $2,
status = $3,
updated_at = CURRENT_TIMESTAMP
WHERE id = $4;
-- name: UpdateUserOrganization :exec
UPDATE users
SET organization_id = $1
WHERE id = $2;
-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1;
-- name: CheckPhoneEmailExist :one
SELECT EXISTS (
SELECT
EXISTS (
SELECT 1
FROM users
WHERE users.phone_number = $1
AND users.phone_number IS NOT NULL
AND users.organization_id = $2
FROM users u1
WHERE u1.phone_number = $1
) AS phone_exists,
EXISTS (
SELECT 1
FROM users
WHERE users.email = $3
AND users.email IS NOT NULL
AND users.organization_id = $2
FROM users u2
WHERE u2.email = $2
) 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
SELECT
id,
first_name,
last_name,
nick_name,
user_name,
email,
phone_number,
role,
password, -- added this line
password,
age,
education_level,
country,
region,
email_verified,
phone_verified,
status,
profile_completed,
last_login,
profile_picture_url,
preferred_language,
created_at,
updated_at,
suspended,
suspended_at,
organization_id
updated_at
FROM users
WHERE organization_id = $3
AND (
(email = $1 AND $1 IS NOT NULL)
WHERE (email = $1 AND $1 IS NOT NULL)
OR (phone_number = $2 AND $2 IS NOT NULL)
)
LIMIT 1;
-- name: UpdatePassword :exec
UPDATE users
SET password = $1,
updated_at = $4
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,
SET
password = $1,
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": {
"post": {
"description": "Login super-admin",
@ -761,7 +709,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UserProfileRes"
"$ref": "#/definitions/domain.UserProfileResponse"
}
},
"400": {
@ -851,7 +799,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UserProfileRes"
"$ref": "#/definitions/domain.UserProfileResponse"
}
},
"400": {
@ -875,9 +823,9 @@ const docTemplate = `{
}
}
},
"/api/v1/user/suspend": {
"post": {
"description": "Suspend or unsuspend a user",
"/api/v1/user/{user_name}/is-unique": {
"get": {
"description": "Returns whether the specified user_name is available (unique)",
"consumes": [
"application/json"
],
@ -887,35 +835,33 @@ const docTemplate = `{
"tags": [
"user"
],
"summary": "Suspend or unsuspend a user",
"summary": "Check if user_name is unique",
"parameters": [
{
"description": "Suspend or unsuspend a user",
"name": "updateUserSuspend",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.UpdateUserSuspendReq"
}
"type": "string",
"description": "User Name",
"name": "user_name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UpdateUserSuspendRes"
"$ref": "#/definitions/domain.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
@ -1171,7 +1117,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"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": {
"post": {
"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": {
@ -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": {
"type": "object",
"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": {
"type": "string",
"enum": [
@ -1475,6 +1559,135 @@ const docTemplate = `{
"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": {
"type": "object",
"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": {
"type": "object",
"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": {
"type": "object",
"required": [
@ -1870,20 +1976,17 @@ const docTemplate = `{
"handlers.loginCustomerReq": {
"type": "object",
"required": [
"password"
"password",
"user_name"
],
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"password": {
"type": "string",
"example": "password123"
},
"phone_number": {
"user_name": {
"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": {
"type": "object",
"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": {
"post": {
"description": "Login super-admin",
@ -753,7 +701,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UserProfileRes"
"$ref": "#/definitions/domain.UserProfileResponse"
}
},
"400": {
@ -843,7 +791,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UserProfileRes"
"$ref": "#/definitions/domain.UserProfileResponse"
}
},
"400": {
@ -867,9 +815,9 @@
}
}
},
"/api/v1/user/suspend": {
"post": {
"description": "Suspend or unsuspend a user",
"/api/v1/user/{user_name}/is-unique": {
"get": {
"description": "Returns whether the specified user_name is available (unique)",
"consumes": [
"application/json"
],
@ -879,35 +827,33 @@
"tags": [
"user"
],
"summary": "Suspend or unsuspend a user",
"summary": "Check if user_name is unique",
"parameters": [
{
"description": "Suspend or unsuspend a user",
"name": "updateUserSuspend",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.UpdateUserSuspendReq"
}
"type": "string",
"description": "User Name",
"name": "user_name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UpdateUserSuspendRes"
"$ref": "#/definitions/domain.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
@ -1163,7 +1109,7 @@
"in": "body",
"required": true,
"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": {
"post": {
"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": {
@ -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": {
"type": "object",
"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": {
"type": "string",
"enum": [
@ -1467,6 +1551,135 @@
"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": {
"type": "object",
"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": {
"type": "object",
"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": {
"type": "object",
"required": [
@ -1862,20 +1968,17 @@
"handlers.loginCustomerReq": {
"type": "object",
"required": [
"password"
"password",
"user_name"
],
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"password": {
"type": "string",
"example": "password123"
},
"phone_number": {
"user_name": {
"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": {
"type": "object",
"properties": {

View File

@ -37,6 +37,22 @@ definitions:
pagination:
$ref: '#/definitions/domain.Pagination'
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:
properties:
current_page:
@ -48,6 +64,50 @@ definitions:
total_pages:
type: integer
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:
enum:
- super_admin
@ -62,6 +122,93 @@ definitions:
- RoleStudent
- RoleInstructor
- 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:
properties:
created_at:
@ -206,30 +353,6 @@ definitions:
example: "1234567890"
type: string
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:
properties:
email:
@ -265,55 +388,6 @@ definitions:
role:
$ref: '#/definitions/domain.Role'
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:
properties:
email:
@ -330,17 +404,15 @@ definitions:
type: object
handlers.loginCustomerReq:
properties:
email:
example: john.doe@example.com
type: string
password:
example: password123
type: string
phone_number:
example: "1234567890"
user_name:
example: johndoe
type: string
required:
- password
- user_name
type: object
handlers.loginCustomerRes:
properties:
@ -386,21 +458,6 @@ definitions:
example: false
type: boolean
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:
properties:
data: {}
@ -505,6 +562,39 @@ paths:
summary: Login customer
tags:
- 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:
get:
consumes:
@ -596,7 +686,7 @@ paths:
name: registerUser
required: true
schema:
$ref: '#/definitions/handlers.RegisterUserReq'
$ref: '#/definitions/domain.RegisterUserReq'
produces:
- application/json
responses:
@ -645,36 +735,6 @@ paths:
summary: Reset tenant password
tags:
- 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:
post:
consumes:
@ -735,6 +795,36 @@ paths:
summary: Send reset code
tags:
- 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:
get:
consumes:
@ -980,40 +1070,6 @@ paths:
summary: Retrieve application logs with filtering and pagination
tags:
- 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:
post:
consumes:
@ -1144,6 +1200,35 @@ paths:
summary: Check if phone number or email exist
tags:
- 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}:
delete:
consumes:
@ -1221,7 +1306,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.UserProfileRes'
$ref: '#/definitions/domain.UserProfileResponse'
"400":
description: Bad Request
schema:
@ -1280,7 +1365,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.UserProfileRes'
$ref: '#/definitions/domain.UserProfileResponse'
"400":
description: Bad Request
schema:
@ -1296,36 +1381,6 @@ paths:
summary: Get user by id
tags:
- 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:
Bearer:
in: header

View File

@ -30,7 +30,6 @@ type AssessmentSubmission struct {
type Course struct {
ID int64 `json:"id"`
OrganizationID int64 `json:"organization_id"`
InstructorID int64 `json:"instructor_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
@ -97,26 +96,9 @@ type Notification struct {
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 {
ID int64 `json:"id"`
UserName string `json:"user_name"`
SentTo string `json:"sent_to"`
Medium string `json:"medium"`
OtpFor string `json:"otp_for"`
@ -127,19 +109,6 @@ type Otp struct {
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 {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
@ -163,31 +132,25 @@ type ReportedIssue struct {
}
type User struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
NickName pgtype.Text `json:"nick_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"`
Suspended bool `json:"suspended"`
SuspendedAt pgtype.Timestamptz `json:"suspended_at"`
OrganizationID pgtype.Int8 `json:"organization_id"`
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"`
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"`
LastLogin pgtype.Timestamptz `json:"last_login"`
ProfileCompleted bool `json:"profile_completed"`
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"`
}

View File

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

View File

@ -72,6 +72,12 @@ var (
// 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 {
// BaseURL string `mapstructure:"ATLAS_BASE_URL"`
// SecretKey string `mapstructure:"ATLAS_SECRET_KEY"`
@ -120,6 +126,7 @@ type TELEBIRRConfig struct {
}
type Config struct {
AFROSMSConfig AFROSMSConfig `mapstructure:"afro_sms_config"`
APP_VERSION string
FIXER_API_KEY string
FIXER_BASE_URL string
@ -248,6 +255,11 @@ func (c *Config) loadEnv() error {
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
c.TELEBIRR.TelebirrBaseURL = os.Getenv("TELEBIRR_BASE_URL")
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")
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

View File

@ -26,10 +26,9 @@ const (
OtpMediumSms OtpMedium = "sms"
)
type Otp struct {
ID int64
UserName string
SentTo string
Medium OtpMedium
For OtpFor
@ -39,3 +38,12 @@ type Otp struct {
CreatedAt 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 (
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 {
ID int64
FirstName string
LastName string
NickName string
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
Password []byte
Role Role
ID int64
FirstName string
LastName string
UserName string
Email string
PhoneNumber string
Password []byte
Role Role
Age int
EducationLevel string
Country string
Region string
EmailVerified bool
PhoneVerified bool
Suspended bool
SuspendedAt time.Time
OrganizationID ValidInt64
CreatedAt time.Time
UpdatedAt time.Time
EmailVerified bool
PhoneVerified bool
Status UserStatus
LastLogin *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 {
Role string
OrganizationID ValidInt64
Role string
Page ValidInt
PageSize ValidInt
Query ValidString
@ -42,92 +87,62 @@ type UserFilter struct {
}
type RegisterUserReq struct {
FirstName string
LastName string
NickName string
Email string
PhoneNumber string
Password string
Role string
Otp string
ReferralCode string `json:"referral_code"`
OtpMedium OtpMedium
OrganizationID ValidInt64
Age int
EducationLevel string
Country string
Region string
FirstName string
LastName string
UserName string
Email string
PhoneNumber string
Password string
Role string
OtpMedium OtpMedium
Age int
EducationLevel string
Country string
Region string
PreferredLanguage string
}
type CreateUserReq struct {
FirstName string
LastName string
NickName string
Email string
PhoneNumber string
Password string
Role string
Suspended bool
OrganizationID ValidInt64
Age int
EducationLevel string
Country string
Region string
FirstName string
LastName string
UserName string
Email string
PhoneNumber string
Password string
Role string
Status UserStatus
Age int
EducationLevel string
Country string
Region string
PreferredLanguage string
}
type ResetPasswordReq struct {
Email string
PhoneNumber string
Password string
Otp string
OtpMedium OtpMedium
OrganizationID int64
UserName string
Password string
OtpCode string
}
type UpdateUserReq struct {
UserID int64
FirstName ValidString
LastName ValidString
NickName ValidString
Suspended ValidBool
OrganizationID ValidInt64
UserID int64
FirstName ValidString
LastName ValidString
UserName ValidString
Status ValidString
Age ValidInt
EducationLevel ValidString
Country 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 {
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)
IsUserNameUnique(ctx context.Context, userName string) (bool, error)
IsUserPending(ctx context.Context, UserName string) (bool, 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(
ctx context.Context,
role *string,
organizationID *int64,
query *string,
createdBefore, createdAfter *time.Time,
limit, offset int32,
) ([]domain.User, int64, error)
GetTotalUsers(ctx context.Context, role *string, organizationID *int64) (int64, error)
SearchUserByNameOrPhone(ctx context.Context, search string, organizationID *int64, role *string) ([]domain.User, error)
GetTotalUsers(ctx context.Context, role *string) (int64, error)
SearchUserByNameOrPhone(
ctx context.Context,
search string,
role *string,
) ([]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
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(
ctx context.Context,
email string,
phone string,
organizationID domain.ValidInt64,
) (domain.User, error)
UpdatePassword(ctx context.Context, password, email, phone string, organizationID int64, updatedAt time.Time) error
GetOwnerByOrganizationID(ctx context.Context, organizationID int64) (domain.User, error)
UpdateUserSuspend(ctx context.Context, id int64, status bool) 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
// UpdateUser(ctx context.Context, user domain.UpdateUserReq) 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
}
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
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"
"database/sql"
"errors"
"time"
dbgen "Yimaru-Backend/gen/db"
"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
func (s *Store) GetUserByEmailOrPhone(ctx context.Context, email, phone string, organizationID *int64) (domain.User, error) {
// prepare organizationID param for the query
// var orgParam pgtype.Int8
// if organizationID != nil {
// orgParam = pgtype.Int8{Int64: *organizationID}
// } else {
// orgParam = pgtype.Int8{Status: pgtype.Null}
// }
func (s *Store) GetUserByEmailOrPhone(
ctx context.Context,
email string,
phone string,
) (domain.User, error) {
u, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{
Email: pgtype.Text{String: email, Valid: email != ""},
PhoneNumber: pgtype.Text{String: phone, Valid: phone != ""},
OrganizationID: pgtype.Int8{Int64: *organizationID},
Email: pgtype.Text{
String: email,
Valid: email != "",
},
PhoneNumber: pgtype.Text{
String: phone,
Valid: phone != "",
},
// OrganizationID: pgtype.Int8{Int64: organizationID},
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
@ -94,20 +98,45 @@ func (s *Store) GetUserByEmailOrPhone(ctx context.Context, email, phone string,
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,
Email: u.Email.String,
PhoneNumber: u.PhoneNumber.String,
Role: domain.Role(u.Role),
Password: u.Password,
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,
Suspended: u.Suspended,
SuspendedAt: u.SuspendedAt.Time,
OrganizationID: domain.ValidInt64{Value: u.OrganizationID.Int64, Valid: u.OrganizationID.Valid},
CreatedAt: u.CreatedAt.Time,
UpdatedAt: u.UpdatedAt.Time,
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
}

View File

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

View File

@ -4,16 +4,29 @@ import (
"context"
"database/sql"
"fmt"
"time"
dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain"
"Yimaru-Backend/internal/ports"
"github.com/jackc/pgx/v5/pgtype"
)
// Interface for creating new otp store
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 {
return s.queries.CreateOtp(ctx, dbgen.CreateOtpParams{
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) {
row, err := s.queries.GetOtp(ctx, dbgen.GetOtpParams{
SentTo: sentTo,
Medium: string(medium),
OtpFor: string(sentfor),
})
func (s *Store) GetOtp(ctx context.Context, userName string) (domain.Otp, error) {
row, err := s.queries.GetOtp(ctx, userName)
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 {
return domain.Otp{}, domain.ErrOtpNotFound
}

View File

@ -14,129 +14,184 @@ import (
"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 (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{
FirstName: user.FirstName,
LastName: user.LastName,
NickName: pgtype.Text{String: user.NickName},
Email: pgtype.Text{String: user.Email, Valid: user.Email != ""},
PhoneNumber: pgtype.Text{String: user.PhoneNumber, Valid: user.PhoneNumber != ""},
Role: string(user.Role),
Password: user.Password,
FirstName: user.FirstName,
LastName: user.LastName,
UserName: user.UserName,
Email: pgtype.Text{String: user.Email, Valid: user.Email != ""},
PhoneNumber: pgtype.Text{String: user.PhoneNumber, Valid: user.PhoneNumber != ""},
Role: string(user.Role),
Password: user.Password,
Age: pgtype.Int4{Int32: int32(user.Age), Valid: user.Age > 0},
EducationLevel: pgtype.Text{String: user.EducationLevel, Valid: user.EducationLevel != ""},
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""},
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
Suspended: user.Suspended,
SuspendedAt: pgtype.Timestamptz{Time: user.SuspendedAt, Valid: !user.SuspendedAt.IsZero()},
OrganizationID: pgtype.Int8{Int64: user.OrganizationID.Value, Valid: user.OrganizationID.Valid},
CreatedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
UpdatedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
Status: string(user.Status),
ProfileCompleted: user.ProfileCompleted,
PreferredLanguage: pgtype.Text{
String: user.PreferredLanguage,
Valid: user.PreferredLanguage != "",
},
// OrganizationID: user.OrganizationID.ToPG(),
})
if err != nil {
return domain.User{}, err
}
var updatedAt *time.Time
if userRes.UpdatedAt.Valid {
updatedAt = &userRes.UpdatedAt.Time
}
return domain.User{
ID: userRes.ID,
FirstName: userRes.FirstName,
LastName: userRes.LastName,
NickName: userRes.NickName.String,
Email: userRes.Email.String,
PhoneNumber: userRes.PhoneNumber.String,
Role: domain.Role(userRes.Role),
ID: userRes.ID,
FirstName: userRes.FirstName,
LastName: userRes.LastName,
UserName: userRes.UserName,
Email: userRes.Email.String,
PhoneNumber: userRes.PhoneNumber.String,
Role: domain.Role(userRes.Role),
Password: user.Password,
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,
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
}
// 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
if usedOtpId > 0 {
err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{
if err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{
ID: usedOtpId,
UsedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
})
if err != nil {
}); err != nil {
return domain.User{}, err
}
}
userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{
FirstName: user.FirstName,
LastName: user.LastName,
NickName: pgtype.Text{String: user.NickName},
Email: pgtype.Text{String: user.Email, Valid: user.Email != ""},
PhoneNumber: pgtype.Text{String: user.PhoneNumber, Valid: user.PhoneNumber != ""},
Role: string(user.Role),
Password: user.Password,
FirstName: user.FirstName,
LastName: user.LastName,
UserName: user.UserName,
Email: pgtype.Text{String: user.Email, Valid: user.Email != ""},
PhoneNumber: pgtype.Text{String: user.PhoneNumber, Valid: user.PhoneNumber != ""},
Role: string(user.Role),
Password: user.Password,
Age: pgtype.Int4{Int32: int32(user.Age), Valid: user.Age > 0},
EducationLevel: pgtype.Text{String: user.EducationLevel, Valid: user.EducationLevel != ""},
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""},
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
Suspended: user.Suspended,
SuspendedAt: pgtype.Timestamptz{Time: user.SuspendedAt, Valid: !user.SuspendedAt.IsZero()},
OrganizationID: pgtype.Int8{Int64: user.OrganizationID.Value, Valid: user.OrganizationID.Valid},
CreatedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
UpdatedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
Status: string(user.Status),
ProfileCompleted: user.ProfileCompleted,
PreferredLanguage: pgtype.Text{
String: user.PreferredLanguage,
Valid: user.PreferredLanguage != "",
},
// OrganizationID: user.OrganizationID.ToPG(),
})
if err != nil {
return domain.User{}, err
}
var updatedAt *time.Time
if userRes.UpdatedAt.Valid {
updatedAt = &userRes.UpdatedAt.Time
}
return domain.User{
ID: userRes.ID,
FirstName: userRes.FirstName,
LastName: userRes.LastName,
NickName: userRes.NickName.String,
Email: userRes.Email.String,
PhoneNumber: userRes.PhoneNumber.String,
Role: domain.Role(userRes.Role),
ID: userRes.ID,
FirstName: userRes.FirstName,
LastName: userRes.LastName,
UserName: userRes.UserName,
Email: userRes.Email.String,
PhoneNumber: userRes.PhoneNumber.String,
Role: domain.Role(userRes.Role),
Password: user.Password,
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
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
func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
userRes, err := s.queries.GetUserByID(ctx, id)
func (s *Store) GetUserByID(
ctx context.Context,
id int64,
) (domain.User, error) {
u, err := s.queries.GetUserByID(ctx, id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
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{
ID: userRes.ID,
FirstName: userRes.FirstName,
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 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,
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
func (s *Store) GetAllUsers(
ctx context.Context,
role *string,
organizationID *int64,
query *string,
createdBefore, createdAfter *time.Time,
limit, offset int32,
@ -186,9 +259,9 @@ func (s *Store) GetAllUsers(
params.Role = *role
}
if organizationID != nil {
params.OrganizationID = pgtype.Int8{Int64: *organizationID, Valid: true}
}
// if organizationID != nil {
// params.OrganizationID = pgtype.Int8{Int64: *organizationID, Valid: true}
// }
if query != nil {
params.Query = pgtype.Text{String: *query, Valid: true}
@ -212,31 +285,42 @@ func (s *Store) GetAllUsers(
}
totalCount := rows[0].TotalCount
users := make([]domain.User, 0, len(rows))
for _, u := range rows {
var updatedAt *time.Time
if u.UpdatedAt.Valid {
updatedAt = &u.UpdatedAt.Time
}
users = append(users, domain.User{
ID: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
NickName: u.NickName.String,
Email: u.Email.String,
PhoneNumber: u.PhoneNumber.String,
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),
EducationLevel: u.EducationLevel.String,
Country: u.Country.String,
Region: u.Region.String,
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
Suspended: u.Suspended,
SuspendedAt: u.SuspendedAt.Time,
OrganizationID: domain.ValidInt64{
Value: u.OrganizationID.Int64,
Valid: u.OrganizationID.Valid,
},
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status),
ProfileCompleted: u.ProfileCompleted,
PreferredLanguage: u.PreferredLanguage.String,
// OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time,
UpdatedAt: u.UpdatedAt.Time,
UpdatedAt: updatedAt,
})
}
@ -244,11 +328,8 @@ func (s *Store) GetAllUsers(
}
// GetTotalUsers counts users with optional filters
func (s *Store) GetTotalUsers(ctx context.Context, role *string, organizationID *int64) (int64, error) {
count, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{
Role: *role,
OrganizationID: pgtype.Int8{Int64: *organizationID},
})
func (s *Store) GetTotalUsers(ctx context.Context, role *string) (int64, error) {
count, err := s.queries.GetTotalUsers(ctx, *role)
if err != nil {
return 0, err
}
@ -256,38 +337,73 @@ func (s *Store) GetTotalUsers(ctx context.Context, role *string, organizationID
}
// SearchUserByNameOrPhone searches users by name or phone
func (s *Store) SearchUserByNameOrPhone(ctx context.Context, search string, organizationID *int64, role *string) ([]domain.User, error) {
rows, err := s.queries.SearchUserByNameOrPhone(ctx, dbgen.SearchUserByNameOrPhoneParams{
Column1: pgtype.Text{String: search},
OrganizationID: pgtype.Int8{Int64: *organizationID},
Role: pgtype.Text{String: *role},
})
func (s *Store) SearchUserByNameOrPhone(
ctx context.Context,
search string,
role *string,
) ([]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 {
return nil, err
}
users := make([]domain.User, len(rows))
for i, u := range rows {
users[i] = domain.User{
ID: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
NickName: u.NickName.String,
Email: u.Email.String,
PhoneNumber: u.PhoneNumber.String,
Role: domain.Role(u.Role),
users := make([]domain.User, 0, len(rows))
for _, u := range rows {
var updatedAt *time.Time
if u.UpdatedAt.Valid {
updatedAt = &u.UpdatedAt.Time
}
users = append(users, 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,
Suspended: u.Suspended,
SuspendedAt: u.SuspendedAt.Time,
OrganizationID: domain.ValidInt64{Value: u.OrganizationID.Int64, Valid: u.OrganizationID.Valid},
CreatedAt: u.CreatedAt.Time,
UpdatedAt: u.UpdatedAt.Time,
}
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status),
ProfileCompleted: u.ProfileCompleted,
// OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt,
})
}
return users, nil
@ -296,29 +412,29 @@ func (s *Store) SearchUserByNameOrPhone(ctx context.Context, search string, orga
// UpdateUser updates basic user info
func (s *Store) UpdateUser(ctx context.Context, user domain.User) error {
return s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Suspended: user.Suspended,
ID: user.ID,
Status: string(user.Status),
})
}
// UpdateUserOrganization updates a user's organization
func (s *Store) UpdateUserOrganization(ctx context.Context, userID, organizationID int64) error {
return s.queries.UpdateUserOrganization(ctx, dbgen.UpdateUserOrganizationParams{
OrganizationID: pgtype.Int8{Int64: organizationID, Valid: true},
ID: userID,
})
}
// func (s *Store) UpdateUserOrganization(ctx context.Context, userID, organizationID int64) error {
// return s.queries.UpdateUserOrganization(ctx, dbgen.UpdateUserOrganizationParams{
// OrganizationID: pgtype.Int8{Int64: organizationID, Valid: true},
// ID: userID,
// })
// }
// SuspendUser suspends a user
func (s *Store) SuspendUser(ctx context.Context, userID int64, suspended bool, suspendedAt time.Time) error {
return s.queries.SuspendUser(ctx, dbgen.SuspendUserParams{
Suspended: suspended,
SuspendedAt: pgtype.Timestamptz{Time: suspendedAt, Valid: true},
ID: userID,
})
}
// func (s *Store) SuspendUser(ctx context.Context, userID int64, suspended bool, suspendedAt time.Time) error {
// return s.queries.SuspendUser(ctx, dbgen.SuspendUserParams{
// Suspended: suspended,
// SuspendedAt: pgtype.Timestamptz{Time: suspendedAt, Valid: true},
// ID: userID,
// })
// }
// DeleteUser removes a user
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
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{
PhoneNumber: pgtype.Text{String: phone},
Email: pgtype.Text{String: email},
OrganizationID: pgtype.Int8{Int64: organizationID.Value},
PhoneNumber: pgtype.Text{String: phone},
Email: pgtype.Text{String: email},
})
if err != nil {
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
}
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
func (s *Store) GetUserByEmailPhone(
ctx context.Context,
email string,
phone string,
organizationID domain.ValidInt64,
) (domain.User, error) {
user, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{
u, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{
Email: pgtype.Text{
String: email,
Valid: email != "",
@ -356,7 +525,6 @@ func (s *Store) GetUserByEmailPhone(
String: phone,
Valid: phone != "",
},
OrganizationID: organizationID.ToPG(),
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
@ -365,87 +533,113 @@ func (s *Store) GetUserByEmailPhone(
return domain.User{}, err
}
return domain.User{
ID: user.ID,
FirstName: user.FirstName,
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
var lastLogin *time.Time
if u.LastLogin.Valid {
lastLogin = &u.LastLogin.Time
}
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
var updatedAt *time.Time
if u.UpdatedAt.Valid {
updatedAt = &u.UpdatedAt.Time
}
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,
NickName: u.NickName.String,
Email: u.Email.String,
PhoneNumber: u.PhoneNumber.String,
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,
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,
Suspended: u.Suspended,
SuspendedAt: u.SuspendedAt.Time,
OrganizationID: domain.ValidInt64{Value: u.OrganizationID.Int64, Valid: u.OrganizationID.Valid},
CreatedAt: u.CreatedAt.Time,
UpdatedAt: u.UpdatedAt.Time,
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
}
// 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"
"Yimaru-Backend/internal/domain"
"golang.org/x/crypto/bcrypt"
)
@ -23,55 +24,65 @@ type LoginSuccess struct {
UserId int64
Role domain.Role
RfToken string
CompanyID domain.ValidInt64
}
func (s *Service) Login(ctx context.Context, email, phone string, password string, companyID domain.ValidInt64) (LoginSuccess, error) {
user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone, companyID)
func (s *Service) Login(
ctx context.Context,
userName, password string,
) (LoginSuccess, error) {
user, err := s.userStore.GetUserByUserName(ctx, userName)
if err != nil {
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
}
if user.Suspended {
// Status check instead of Suspended
if user.Status == domain.UserStatusSuspended {
return LoginSuccess{}, ErrUserSuspended
}
// Handle existing refresh token
oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
if err != nil && err != ErrRefreshTokenNotFound {
if err != nil && !errors.Is(err, ErrRefreshTokenNotFound) {
return LoginSuccess{}, err
}
// If old refresh token is not revoked, revoke it
// Revoke if exists and not revoked
if err == nil && !oldRefreshToken.Revoked {
err = s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token)
if err != nil {
if err := s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token); err != nil {
return LoginSuccess{}, err
}
}
// Generate new refresh token
refreshToken, err := generateRefreshToken()
if err != nil {
return LoginSuccess{}, err
}
err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
if err := s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
Token: refreshToken,
UserID: user.ID,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
})
if err != nil {
}); err != nil {
return LoginSuccess{}, err
}
// Return login success payload
return LoginSuccess{
UserId: user.ID,
Role: user.Role,
RfToken: refreshToken,
CompanyID: user.OrganizationID,
}, nil
}

View File

@ -3,8 +3,13 @@ package messenger
import (
"Yimaru-Backend/internal/domain"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
afro "github.com/amanuelabay/afrosms-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)
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 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 {
case domain.AfroMessage:
return s.SendAfroMessageSMS(ctx, receiverPhone, message)
return s.SendAfroMessageSMSLatest(ctx, receiverPhone, message, nil)
case domain.TwilioSms:
return s.SendTwilioSMS(ctx, receiverPhone, message)
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 {
accountSid := s.config.TwilioAccountSid
authToken := s.config.TwilioAuthToken

View File

@ -9,12 +9,17 @@ import (
"Yimaru-Backend/internal/services/user"
"Yimaru-Backend/internal/web_server/ws"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
// "errors"
"log/slog"
"sync"
"time"
// "github.com/segmentio/kafka-go"
"go.uber.org/zap"
// afro "github.com/amanuelabay/afrosms-go"
@ -67,6 +72,79 @@ func New(
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 {
if c == nil {
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 == "" {
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 {
s.mongoLogger.Error("[NotificationSvc.HandleNotification] Failed to send notification SMS",
zap.Int64("recipient_id", recipientID),
zap.String("user_phone_number", user.PhoneNumber),
zap.String("message", message),
zap.Int64("company_id", user.OrganizationID.Value),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
@ -371,7 +448,6 @@ func (s *Service) SendNotificationEmail(ctx context.Context, recipientID int64,
zap.Int64("recipient_id", recipientID),
zap.String("user_email", user.Email),
zap.String("message", message),
zap.Int64("company_id", user.OrganizationID.Value),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)

View File

@ -10,6 +10,51 @@ import (
"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 {
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)
}
// helper function to get a pointer to time.Time
func timePtr(t time.Time) time.Time {
return t
}
func hashPassword(plaintextPassword string) ([]byte, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
if err != nil {

View File

@ -5,36 +5,35 @@ import (
"context"
)
func (s *Service) CreateUser(ctx context.Context, User domain.CreateUserReq, is_company bool) (domain.User, error) {
// Create User
// creator, err := s.userStore.GetUserByID(ctx, createrUserId)
// if err != nil {
// return domain.User{}, err
// }
// if creator.Role != domain.RoleAdmin {
// User.BranchID = creator.BranchID
// User.Role = string(domain.RoleCashier)
// } else {
// User.BranchID = branchId
// User.Role = string(domain.RoleBranchManager)
// }
func (s *Service) CreateUser(
ctx context.Context,
req domain.CreateUserReq,
isCompany bool,
) (domain.User, error) {
hashedPassword, err := hashPassword(User.Password)
// Hash the password
hashedPassword, err := hashPassword(req.Password)
if err != nil {
return domain.User{}, err
}
// Create the user
return s.userStore.CreateUserWithoutOtp(ctx, domain.User{
FirstName: User.FirstName,
LastName: User.LastName,
Email: User.Email,
PhoneNumber: User.PhoneNumber,
Password: hashedPassword,
Role: domain.Role(User.Role),
EmailVerified: true,
PhoneVerified: true,
Suspended: User.Suspended,
OrganizationID: User.OrganizationID,
FirstName: req.FirstName,
LastName: req.LastName,
UserName: req.UserName,
Email: req.Email,
PhoneNumber: req.PhoneNumber,
Password: hashedPassword,
Role: domain.Role(req.Role),
EmailVerified: true, // assuming auto-verified on creation
PhoneVerified: true,
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) {
// 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) {

View File

@ -1,41 +1,48 @@
package user
// import (
// "context"
import (
"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 {
// 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)
// GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([]domain.GetCashier, int64, error)
// GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error)
// GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, 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
// 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)
// }
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 {
ResendOtp(
ctx context.Context,
userName string,
) error
CreateOtp(ctx context.Context, otp domain.Otp) error
GetOtp(ctx context.Context, userName string) (domain.Otp, error)
}

View File

@ -6,18 +6,51 @@ import (
"time"
)
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string, companyID domain.ValidInt64) (bool, bool, error) { // email,phone,error
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email, companyID)
func (s *Service) VerifyOtp(ctx context.Context, userName string, otpCode string) error {
// 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
// check if user exists
switch medium {
case domain.OtpMediumEmail:
_, err = s.userStore.GetUserByEmailPhone(ctx, sentTo, "", companyID)
_, err = s.userStore.GetUserByEmailPhone(ctx, sentTo, "")
case domain.OtpMediumSms:
_, err = s.userStore.GetUserByEmailPhone(ctx, "", sentTo, companyID)
_, err = s.userStore.GetUserByEmailPhone(ctx, "", sentTo)
}
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)
}
func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) { // normal
// get otp
func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) {
// 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 provider domain.Provid
if registerReq.OtpMedium == domain.OtpMediumEmail {
sentTo = registerReq.Email
} else {
sentTo = registerReq.PhoneNumber
}
//
otp, err := s.otpStore.GetOtp(
ctx, sentTo,
domain.OtpRegister, registerReq.OtpMedium)
if err != nil {
// Send OTP to the user (email/SMS)
if err := s.SendOtp(ctx, sentTo, domain.OtpRegister, registerReq.OtpMedium, domain.TwilioSms); err != nil {
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)
if err != nil {
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)
// Create the user (no OTP validation yet)
user, err := s.userStore.CreateUserWithoutOtp(ctx, userR)
if err != nil {
return domain.User{}, err
}
return user, nil
}

View File

@ -7,15 +7,15 @@ import (
"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
// check if user exists
switch medium {
case domain.OtpMediumEmail:
_, err = s.userStore.GetUserByEmailPhone(ctx, sentTo, "", companyID)
_, err = s.userStore.GetUserByEmailPhone(ctx, sentTo, "")
case domain.OtpMediumSms:
_, err = s.userStore.GetUserByEmailPhone(ctx, "", sentTo, companyID)
_, err = s.userStore.GetUserByEmailPhone(ctx, "", sentTo)
}
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 {
var sentTo string
if resetReq.OtpMedium == domain.OtpMediumEmail {
sentTo = resetReq.Email
} else {
sentTo = resetReq.PhoneNumber
}
otp, err := s.otpStore.GetOtp(
ctx, sentTo,
domain.OtpReset, resetReq.OtpMedium)
otp, err := s.otpStore.GetOtp(ctx, resetReq.UserName)
if err != nil {
return err
}
//
user, err := s.userStore.GetUserByUserName(ctx, resetReq.UserName)
if err != nil {
return err
}
if otp.Used {
return domain.ErrOtpAlreadyUsed
}
if time.Now().After(otp.ExpiresAt) {
return domain.ErrOtpExpired
}
if otp.Otp != resetReq.Otp {
if otp.Otp != resetReq.OtpCode {
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 {
return err
}

View File

@ -3,41 +3,54 @@ package user
import (
"Yimaru-Backend/internal/domain"
"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
return s.userStore.SearchUserByNameOrPhone(ctx, searchString, role, companyID)
}
func (s *Service) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error {
// 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,
var roleStr *string
if role != nil {
tmp := strconv.FormatInt(*role, 10)
roleStr = &tmp
}
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)
}
// func (s *Service) UpdateUserCompany(ctx context.Context, id int64, companyID int64) error {
// // update user
// return s.userStore.UpdateUserCompany(ctx, id, companyID)
// }
func (s *Service) UpdateUserSuspend(ctx context.Context, id int64, status bool) error {
// update user
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) {
return s.userStore.GetUserByID(ctx, id)
}

View File

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

View File

@ -13,41 +13,40 @@ import (
"go.uber.org/zap"
)
// loginCustomerReq represents the request body for the LoginCustomer endpoint.
type loginCustomerReq struct {
Email string `json:"email" validate:"required_without=PhoneNumber" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
Password string `json:"password" validate:"required" example:"password123"`
// loginUserReq represents the request body for the Loginuser endpoint.
type loginUserReq struct {
UserName string `json:"user_name" validate:"required" example:"johndoe"`
Password string `json:"password" validate:"required" example:"password123"`
}
// loginCustomerRes represents the response body for the LoginCustomer endpoint.
type loginCustomerRes struct {
// loginUserRes represents the response body for the Loginuser endpoint.
type loginUserRes struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
Role string `json:"role"`
}
// LoginCustomer godoc
// @Summary Login customer
// @Description Login customer
// Loginuser godoc
// @Summary Login user
// @Description Login user
// @Tags auth
// @Accept json
// @Produce json
// @Param login body loginCustomerReq true "Login customer"
// @Success 200 {object} loginCustomerRes
// @Param login body loginUserReq true "Login user"
// @Success 200 {object} loginUserRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/customer-login [post]
func (h *Handler) LoginCustomer(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 loginCustomerReq
// @Router /api/v1/{tenant_slug}/user-login [post]
func (h *Handler) LoginUser(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 loginUserReq
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.Error(err),
zap.Time("timestamp", time.Now()),
@ -63,15 +62,14 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
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 {
switch {
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
zap.Int("status_code", fiber.StatusUnauthorized),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.String("user_name", req.UserName),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
@ -79,8 +77,7 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
case errors.Is(err, authentication.ErrUserSuspended):
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
zap.Int("status_code", fiber.StatusUnauthorized),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.String("user_name", req.UserName),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
@ -96,21 +93,19 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
}
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.String("role", string(successRes.Role)),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.String("user_name", req.UserName),
zap.Error(err),
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(
successRes.UserId,
successRes.Role,
successRes.CompanyID,
h.jwtConfig.JwtAccessKey,
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")
}
res := loginCustomerRes{
res := loginUserRes{
AccessToken: accessToken,
RefreshToken: successRes.RfToken,
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.
type loginAdminReq struct {
Email string `json:"email" validate:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
Password string `json:"password" validate:"required" example:"password123"`
UserName string `json:"user_name" validate:"required" example:"adminuser"`
Password string `json:"password" validate:"required" example:"password123"`
}
// loginAdminRes represents the response body for the LoginAdmin endpoint.
@ -155,8 +149,8 @@ type LoginAdminRes struct {
}
// LoginAdmin godoc
// @Summary Login customer
// @Description Login customer
// @Summary Login user
// @Description Login user
// @Tags auth
// @Accept json
// @Produce json
@ -167,11 +161,6 @@ type LoginAdminRes struct {
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/admin-login [post]
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
if err := c.BodyParser(&req); err != nil {
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)
}
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 {
switch {
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
zap.Int("status_code", fiber.StatusBadRequest),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.String("user_name", req.UserName),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
@ -205,8 +193,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
case errors.Is(err, authentication.ErrUserSuspended):
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
zap.Int("status_code", fiber.StatusForbidden),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.String("user_name", req.UserName),
zap.Error(err),
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 {
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.String("role", string(successRes.Role)),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.String("user_name", req.UserName),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
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 {
h.mongoLoggerSvc.Error("Failed to create access token",
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")
}
res := loginCustomerRes{
res := loginUserRes{
AccessToken: accessToken,
RefreshToken: successRes.RfToken,
Role: string(successRes.Role),
@ -291,14 +277,13 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
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 {
switch {
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
zap.Int("status_code", fiber.StatusBadRequest),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.String("user_name", req.UserName),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
@ -306,8 +291,7 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
case errors.Is(err, authentication.ErrUserSuspended):
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
zap.Int("status_code", fiber.StatusForbidden),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.String("user_name", req.UserName),
zap.Error(err),
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",
zap.Int("status_code", fiber.StatusForbidden),
zap.String("role", string(successRes.Role)),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.String("user_name", req.UserName),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
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 {
h.mongoLoggerSvc.Error("Failed to create access token",
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")
}
res := loginCustomerRes{
res := loginUserRes{
AccessToken: accessToken,
RefreshToken: successRes.RfToken,
Role: string(successRes.Role),
@ -373,14 +356,14 @@ type refreshToken struct {
// @Accept json
// @Produce json
// @Param refresh body refreshToken true "tokens"
// @Success 200 {object} loginCustomerRes
// @Success 200 {object} loginUserRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/auth/refresh [post]
func (h *Handler) RefreshToken(c *fiber.Ctx) error {
type loginCustomerRes struct {
type loginUserRes struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
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())
}
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 {
h.mongoLoggerSvc.Error("Failed to create new access token",
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())
}
res := loginCustomerRes{
res := loginUserRes{
AccessToken: accessToken,
RefreshToken: req.RefreshToken,
Role: string(user.Role),
@ -482,22 +465,22 @@ type logoutReq struct {
RefreshToken string `json:"refresh_token" validate:"required" example:"<refresh-token>"`
}
// LogOutCustomer godoc
// @Summary Logout customer
// @Description Logout customer
// LogOutuser godoc
// @Summary Logout user
// @Description Logout user
// @Tags auth
// @Accept json
// @Produce json
// @Param logout body logoutReq true "Logout customer"
// @Param logout body logoutReq true "Logout user"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @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
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.Error(err),
zap.Time("timestamp", time.Now()),
@ -512,7 +495,7 @@ func (h *Handler) LogOutCustomer(c *fiber.Ctx) error {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
h.mongoLoggerSvc.Info("LogOutCustomer validation failed",
h.mongoLoggerSvc.Info("LogOutuser validation failed",
zap.String("errMsg", errMsg),
zap.Int("status_code", fiber.StatusBadRequest),
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"
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"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")
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
_, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get user",
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")
}
if !user.OrganizationID.Valid || user.OrganizationID.Value != companyID.Value {
h.mongoLoggerSvc.Warn("User attempt to login to different company",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Failed to retrieve user")
}
// if !user.OrganizationID.Valid || user.OrganizationID.Value != companyID.Value {
// h.mongoLoggerSvc.Warn("User attempt to login to different company",
// zap.Int64("userID", userID),
// zap.Int("status_code", fiber.StatusInternalServerError),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusBadRequest, "Failed to retrieve user")
// }
// 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,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
SuspendedAt: user.SuspendedAt,
Suspended: user.Suspended,
// SuspendedAt: user.SuspendedAt,
// Suspended: user.Suspended,
LastLogin: *lastLogin,
}
@ -369,21 +368,20 @@ type updateTransactionApproverReq struct {
func (h *Handler) UpdateTransactionApprover(c *fiber.Ctx) error {
var req updateTransactionApproverReq
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.Error(err),
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 !ok {
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
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.Any("validation_errors", valErrs),
zap.Time("timestamp", time.Now()),
@ -391,20 +389,20 @@ func (h *Handler) UpdateTransactionApprover(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
ApproverIDStr := c.Params("id")
ApproverID, err := strconv.ParseInt(ApproverIDStr, 10, 64)
approverIDStr := c.Params("id")
approverID, err := strconv.ParseInt(approverIDStr, 10, 64)
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.String("admin_id_param", ApproverIDStr),
zap.String("approver_id_param", approverIDStr),
zap.Error(err),
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{
UserID: ApproverID,
updateReq := domain.UpdateUserReq{
UserID: approverID,
FirstName: domain.ValidString{
Value: req.FirstName,
Valid: req.FirstName != "",
@ -413,26 +411,25 @@ func (h *Handler) UpdateTransactionApprover(c *fiber.Ctx) error {
Value: req.LastName,
Valid: req.LastName != "",
},
Suspended: domain.ValidBool{
Value: req.Suspended,
Valid: true,
},
})
}
err = h.userSvc.UpdateUser(c.Context(), updateReq)
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.Int64("admin_id", ApproverID),
zap.Int64("approver_id", approverID),
zap.Error(err),
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.Int64("admin_id", ApproverID),
zap.Int64("approver_id", approverID),
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
}
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{
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "yimaru.com",
@ -39,10 +39,10 @@ func CreateJwt(userId int64, Role domain.Role, CompanyID domain.ValidInt64, key
},
UserId: userId,
Role: Role,
CompanyID: domain.NullJwtInt64{
Value: CompanyID.Value,
Valid: CompanyID.Valid,
},
// CompanyID: domain.NullJwtInt64{
// Value: CompanyID.Value,
// Valid: CompanyID.Valid,
// },
})
jwtToken, err := token.SignedString([]byte(key))
return jwtToken, err

View File

@ -80,11 +80,11 @@ func (a *App) initAppRoutes() {
})
// Auth Routes
tenant.Post("/auth/customer-login", h.LoginCustomer)
tenant.Post("/auth/customer-login", h.LoginUser)
tenant.Post("/auth/admin-login", h.LoginAdmin)
groupV1.Post("/auth/super-login", h.LoginSuper)
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 {
userID, ok := c.Locals("user_id").(int64)
if !ok {
@ -122,8 +122,12 @@ func (a *App) initAppRoutes() {
// groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler
// 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/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/sendResetCode", h.SendTenantResetCode)
@ -133,10 +137,9 @@ func (a *App) initAppRoutes() {
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.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend)
groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser)
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.Put("/t-approver/:id", a.authMiddleware, a.OnlyAdminAndAbove, h.UpdateTransactionApprover)
//mongoDB logs
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)
// Notification Routes
groupV1.Post("/sendSMS", h.SendSingleAfroSMS)
groupV1.Get("/ws/connect", a.WebsocketAuthMiddleware, h.ConnectSocket)
groupV1.Get("/notifications", a.authMiddleware, h.GetUserNotification)
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