From 915185c3178ebf3d349c4ca053f3e3efa5c61d92 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 23 Dec 2025 18:57:48 +0300 Subject: [PATCH] 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 --- db/data/001_initial_seed_data.sql | 4 +- db/data/003_fix_autoincrement_desync.sql | 5 - db/migrations/000001_yimaru.down.sql | 10 - db/migrations/000001_yimaru.up.sql | 43 +- db/query/otp.sql | 19 +- db/query/user.sql | 344 +++---- docs/docs.go | 582 +++++++----- docs/swagger.json | 582 +++++++----- docs/swagger.yaml | 435 +++++---- gen/db/models.go | 81 +- gen/db/otp.sql.go | 45 +- gen/db/user.sql.go | 697 +++++++------- internal/config/config.go | 14 +- internal/domain/otp.go | 12 +- internal/domain/user.go | 201 ++-- internal/ports/user.go | 47 +- internal/repository/auth.go | 75 +- internal/repository/notification.go | 3 +- internal/repository/otp.go | 23 +- internal/repository/user.go | 676 +++++++++----- internal/services/authentication/impl.go | 43 +- internal/services/messenger/sms.go | 97 +- internal/services/notification/service.go | 82 +- internal/services/user/common.go | 50 + internal/services/user/direct.go | 49 +- internal/services/user/interface.go | 81 +- internal/services/user/register.go | 127 ++- internal/services/user/reset.go | 33 +- internal/services/user/user.go | 69 +- internal/web_server/handlers/admin.go | 265 ++---- internal/web_server/handlers/auth_handler.go | 127 ++- internal/web_server/handlers/manager.go | 455 --------- .../handlers/notification_handler.go | 75 ++ .../web_server/handlers/referal_handlers.go | 20 +- .../handlers/transaction_approver.go | 49 +- internal/web_server/handlers/user.go | 878 ++++++++++-------- internal/web_server/jwt/jwt.go | 10 +- internal/web_server/routes.go | 13 +- makefile copy | 108 --- 39 files changed, 3403 insertions(+), 3126 deletions(-) delete mode 100644 internal/web_server/handlers/manager.go delete mode 100644 makefile copy diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index a4a5ab1..e1d6bb0 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -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, diff --git a/db/data/003_fix_autoincrement_desync.sql b/db/data/003_fix_autoincrement_desync.sql index cfa2ab9..1de6eac 100644 --- a/db/data/003_fix_autoincrement_desync.sql +++ b/db/data/003_fix_autoincrement_desync.sql @@ -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'), diff --git a/db/migrations/000001_yimaru.down.sql b/db/migrations/000001_yimaru.down.sql index 4a6d42a..b4958f8 100644 --- a/db/migrations/000001_yimaru.down.sql +++ b/db/migrations/000001_yimaru.down.sql @@ -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 diff --git a/db/migrations/000001_yimaru.up.sql b/db/migrations/000001_yimaru.up.sql index 6c923a0..6223ed4 100644 --- a/db/migrations/000001_yimaru.up.sql +++ b/db/migrations/000001_yimaru.up.sql @@ -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), diff --git a/db/query/otp.sql b/db/query/otp.sql index c2bd2e7..d858b33 100644 --- a/db/query/otp.sql +++ b/db/query/otp.sql @@ -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 diff --git a/db/query/user.sql b/db/query/user.sql index 7fc1f42..a18bf5b 100644 --- a/db/query/user.sql +++ b/db/query/user.sql @@ -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,153 +92,160 @@ 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, - first_name, - last_name, - nick_name, - email, - phone_number, - role, - age, - education_level, - country, - region, - email_verified, - phone_verified, - created_at, - updated_at, - suspended, - suspended_at, - organization_id -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 || '%' - ) - 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, - 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 1 - FROM users - WHERE users.phone_number = $1 - AND users.phone_number IS NOT NULL - AND users.organization_id = $2 - ) AS phone_exists, - EXISTS ( - SELECT 1 - FROM users - WHERE users.email = $3 - AND users.email IS NOT NULL - AND users.organization_id = $2 - ) AS email_exists; --- name: GetUserByEmailPhone :one -SELECT +SELECT id, first_name, last_name, - nick_name, + user_name, email, phone_number, role, - password, -- added this line age, education_level, country, region, email_verified, phone_verified, + status, + profile_completed, created_at, - updated_at, - suspended, - suspended_at, - organization_id + updated_at FROM users -WHERE organization_id = $3 +WHERE ( + first_name ILIKE '%' || $1 || '%' + OR last_name ILIKE '%' || $1 || '%' + OR phone_number ILIKE '%' || $1 || '%' + OR email ILIKE '%' || $1 || '%' + ) AND ( - (email = $1 AND $1 IS NOT NULL) - OR (phone_number = $2 AND $2 IS NOT NULL) - ) + role = sqlc.narg('role') + OR sqlc.narg('role') IS NULL + ); + +-- name: UpdateUser :exec +UPDATE users +SET + first_name = $1, + last_name = $2, + status = $3, + updated_at = CURRENT_TIMESTAMP +WHERE id = $4; + +-- name: DeleteUser :exec +DELETE FROM users +WHERE id = $1; + +-- name: CheckPhoneEmailExist :one +SELECT + EXISTS ( + SELECT 1 + FROM users u1 + WHERE u1.phone_number = $1 + ) AS phone_exists, + EXISTS ( + SELECT 1 + 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, + 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 (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; \ No newline at end of file +WHERE email = $2 OR phone_number = $3; + +-- name: UpdateUserStatus :exec +UPDATE users +SET + status = $1, + updated_at = CURRENT_TIMESTAMP +WHERE id = $2; diff --git a/docs/docs.go b/docs/docs.go index 0bd3b47..844a6d9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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": { diff --git a/docs/swagger.json b/docs/swagger.json index 81c2588..7deed14 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 8805e3e..4e355f9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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 diff --git a/gen/db/models.go b/gen/db/models.go index 924d777..80b9d3e 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -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"` } diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index 8f7a863..a1b4841 100644 --- a/gen/db/otp.sql.go +++ b/gen/db/otp.sql.go @@ -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 +} diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 9637e29..d990ff6 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -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,131 +317,85 @@ 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 } const GetUserByEmailPhone = `-- name: GetUserByEmailPhone :one -SELECT +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 } diff --git a/internal/config/config.go b/internal/config/config.go index 21d5f7e..cc323de 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 diff --git a/internal/domain/otp.go b/internal/domain/otp.go index fc302e0..66cdf9c 100644 --- a/internal/domain/otp.go +++ b/internal/domain/otp.go @@ -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"` +} diff --git a/internal/domain/user.go b/internal/domain/user.go index 0a9f173..794d20f 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -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 -// } diff --git a/internal/ports/user.go b/internal/ports/user.go index d1847a8..862ba11 100644 --- a/internal/ports/user.go +++ b/internal/ports/user.go @@ -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) } diff --git a/internal/repository/auth.go b/internal/repository/auth.go index 7a92857..2284352 100644 --- a/internal/repository/auth.go +++ b/internal/repository/auth.go @@ -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 } diff --git a/internal/repository/notification.go b/internal/repository/notification.go index 820af84..6e06c13 100644 --- a/internal/repository/notification.go +++ b/internal/repository/notification.go @@ -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), diff --git a/internal/repository/otp.go b/internal/repository/otp.go index 9f8cb42..4698bef 100644 --- a/internal/repository/otp.go +++ b/internal/repository/otp.go @@ -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 } diff --git a/internal/repository/user.go b/internal/repository/user.go index 45e8fc2..e0b3ace 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -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, } } diff --git a/internal/services/authentication/impl.go b/internal/services/authentication/impl.go index b4eebfc..dce8474 100644 --- a/internal/services/authentication/impl.go +++ b/internal/services/authentication/impl.go @@ -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 } diff --git a/internal/services/messenger/sms.go b/internal/services/messenger/sms.go index 616946b..5f7f809 100644 --- a/internal/services/messenger/sms.go +++ b/internal/services/messenger/sms.go @@ -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 diff --git a/internal/services/notification/service.go b/internal/services/notification/service.go index 7fabe16..b85dc36 100644 --- a/internal/services/notification/service.go +++ b/internal/services/notification/service.go @@ -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()), ) diff --git a/internal/services/user/common.go b/internal/services/user/common.go index aceb3ed..a49fcea 100644 --- a/internal/services/user/common.go +++ b/internal/services/user/common.go @@ -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 { diff --git a/internal/services/user/direct.go b/internal/services/user/direct.go index 6ad7dd4..08fcec3 100644 --- a/internal/services/user/direct.go +++ b/internal/services/user/direct.go @@ -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) { diff --git a/internal/services/user/interface.go b/internal/services/user/interface.go index 479bf6f..b5e2c0f 100644 --- a/internal/services/user/interface.go +++ b/internal/services/user/interface.go @@ -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) +} diff --git a/internal/services/user/register.go b/internal/services/user/register.go index ba9c94f..ba8df9b 100644 --- a/internal/services/user/register.go +++ b/internal/services/user/register.go @@ -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 } diff --git a/internal/services/user/reset.go b/internal/services/user/reset.go index ded24b5..12ece7a 100644 --- a/internal/services/user/reset.go +++ b/internal/services/user/reset.go @@ -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 } diff --git a/internal/services/user/user.go b/internal/services/user/user.go index d1ccde7..b199197 100644 --- a/internal/services/user/user.go +++ b/internal/services/user/user.go @@ -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) } diff --git a/internal/web_server/handlers/admin.go b/internal/web_server/handlers/admin.go index 6afabe9..6130afb 100644 --- a/internal/web_server/handlers/admin.go +++ b/internal/web_server/handlers/admin.go @@ -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) } diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index e5cb0cf..8366e11 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -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:""` } -// 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), diff --git a/internal/web_server/handlers/manager.go b/internal/web_server/handlers/manager.go deleted file mode 100644 index f549618..0000000 --- a/internal/web_server/handlers/manager.go +++ /dev/null @@ -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) - -} diff --git a/internal/web_server/handlers/notification_handler.go b/internal/web_server/handlers/notification_handler.go index c5913f3..e08b83c 100644 --- a/internal/web_server/handlers/notification_handler.go +++ b/internal/web_server/handlers/notification_handler.go @@ -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, + }) +} diff --git a/internal/web_server/handlers/referal_handlers.go b/internal/web_server/handlers/referal_handlers.go index 1241199..57c1770 100644 --- a/internal/web_server/handlers/referal_handlers.go +++ b/internal/web_server/handlers/referal_handlers.go @@ -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) diff --git a/internal/web_server/handlers/transaction_approver.go b/internal/web_server/handlers/transaction_approver.go index bf45f1f..c16917e 100644 --- a/internal/web_server/handlers/transaction_approver.go +++ b/internal/web_server/handlers/transaction_approver.go @@ -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) } + diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index c3984bd..0bd121d 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -13,6 +13,257 @@ import ( "go.uber.org/zap" ) +// ResendOtp godoc +// @Summary Resend OTP +// @Description Resend OTP if the previous one is expired +// @Tags otp +// @Accept json +// @Produce json +// @Param resendOtp body domain.ResendOtpReq true "Resend OTP" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}/otp/resend [post] +func (h *Handler) ResendOtp(c *fiber.Ctx) error { + var req domain.ResendOtpReq + if err := c.BodyParser(&req); err != nil { + h.mongoLoggerSvc.Info("Failed to parse ResendOtp 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 resend OTP", + Error: "Invalid request body: " + err.Error(), + }) + } + + 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 resend OTP", + Error: errMsg, + }) + } + + user, err := h.userSvc.GetUserByUserName(c.Context(), req.UserName) + if err != nil { + h.mongoLoggerSvc.Info("Failed to get user by user name", + zap.String("user_name", req.UserName), + 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 resend OTP", + Error: err.Error(), + }) + } + + medium, err := getMedium(user.Email, user.PhoneNumber) + if err != nil { + h.mongoLoggerSvc.Info("Failed to determine OTP medium", + zap.String("email", user.Email), + zap.String("phone_number", user.PhoneNumber), + 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 resend OTP", + Error: err.Error(), + }) + } + + sentTo := user.Email + if medium == domain.OtpMediumSms { + sentTo = user.PhoneNumber + } + + if err := h.userSvc.ResendOtp( + c.Context(), + req.UserName, + ); err != nil { + + h.mongoLoggerSvc.Error("Failed to resend OTP", + zap.String("sent_to", sentTo), + zap.String("medium", string(medium)), + 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 resend OTP", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "OTP resent successfully", + Data: nil, + }) +} + +// CheckUserNameUnique godoc +// @Summary Check if user_name is unique +// @Description Returns whether the specified user_name is available (unique) +// @Tags user +// @Accept json +// @Produce json +// @Param user_name path string true "User Name" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/user/{user_name}/is-unique [get] +func (h *Handler) CheckUserNameUnique(c *fiber.Ctx) error { + userName := c.Params("user_name") + if userName == "" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid user name", + Error: "user_name path parameter cannot be empty", + }) + } + + isUnique, err := h.userSvc.IsUserNameUnique(c.Context(), userName) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to check user name uniqueness", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "User name uniqueness checked successfully", + Data: map[string]bool{ + "is_unique": isUnique, + }, + }) +} + +// CheckUserPending godoc +// @Summary Check if user status is pending +// @Description Returns whether the specified user has a status of "pending" +// @Tags user +// @Accept json +// @Produce json +// @Param user_name path string true "User Name" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/{tenant_slug}/user/{user_name}/is-pending [get] +func (h *Handler) CheckUserPending(c *fiber.Ctx) error { + userName := c.Params("user_name") + if userName == "" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid user name", + Error: "User name cannot be empty", + }) + } + + isPending, err := h.userSvc.IsUserPending(c.Context(), userName) + if err != nil { + if errors.Is(err, authentication.ErrUserNotFound) { + return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ + Message: "User not found", + Error: err.Error(), + }) + } + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to check user status", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "User status fetched successfully", + Data: map[string]bool{ + "is_pending": isPending, + }, + }) +} + +// VerifyOtp godoc +// @Summary Verify OTP +// @Description Verify OTP for registration or other actions +// @Tags user +// @Accept json +// @Produce json +// @Param verifyOtp body domain.VerifyOtpReq true "Verify OTP" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/user/verify-otp [post] +func (h *Handler) VerifyOtp(c *fiber.Ctx) error { + var req domain.VerifyOtpReq + if err := c.BodyParser(&req); err != nil { + h.mongoLoggerSvc.Info("Failed to parse VerifyOtp 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 verify OTP", + Error: "Invalid request body: " + err.Error(), + }) + } + + 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 verify OTP", + Error: errMsg, + }) + } + + // Call service to verify OTP + err := h.userSvc.VerifyOtp(c.Context(), req.UserName, req.Otp) + if err != nil { + var errMsg string + switch { + case errors.Is(err, domain.ErrOtpAlreadyUsed): + errMsg = "OTP already used" + case errors.Is(err, domain.ErrOtpExpired): + errMsg = "OTP expired" + case errors.Is(err, domain.ErrInvalidOtp): + errMsg = "Invalid OTP" + case errors.Is(err, domain.ErrOtpNotFound): + errMsg = "OTP not found" + default: + h.mongoLoggerSvc.Error("Failed to verify OTP", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + errMsg = "Failed to verify OTP: " + err.Error() + } + statusCode := fiber.StatusBadRequest + if errMsg == "Failed to verify OTP: "+err.Error() { + statusCode = fiber.StatusInternalServerError + } + return c.Status(statusCode).JSON(domain.ErrorResponse{ + Message: "Failed to verify OTP", + Error: errMsg, + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "OTP verified successfully", + Data: nil, + }) +} + type GetTenantSlugByToken struct { Slug string `json:"slug"` } @@ -39,7 +290,7 @@ func (h *Handler) GetTenantSlugByToken(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification") } - 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 profile", zap.Int64("userID", userID), @@ -50,18 +301,18 @@ func (h *Handler) GetTenantSlugByToken(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile:"+err.Error()) } - if !user.OrganizationID.Valid { - if user.Role == domain.RoleSuperAdmin { - return fiber.NewError(fiber.StatusBadRequest, "Role Super-Admin Doesn't have a company-id") - } - h.mongoLoggerSvc.Error("Unknown Error: User doesn't have a company-id", - zap.Int64("userID", userID), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusInternalServerError, "Unknown Error: User doesn't have a company-id") - } + // if !user.OrganizationID.Valid { + // if user.Role == domain.RoleSuperAdmin { + // return fiber.NewError(fiber.StatusBadRequest, "Role Super-Admin Doesn't have a company-id") + // } + // h.mongoLoggerSvc.Error("Unknown Error: User doesn't have a company-id", + // zap.Int64("userID", userID), + // zap.Int("status_code", fiber.StatusInternalServerError), + // zap.Error(err), + // zap.Time("timestamp", time.Now()), + // ) + // return fiber.NewError(fiber.StatusInternalServerError, "Unknown Error: User doesn't have a company-id") + // } // company, err := h.companySvc.GetCompanyByID(c.Context(), user.CompanyID.Value) // if err != nil { @@ -75,7 +326,7 @@ func (h *Handler) GetTenantSlugByToken(c *fiber.Ctx) error { // } res := GetTenantSlugByToken{ - Slug: strconv.FormatInt(user.OrganizationID.Value, 10), + Slug: strconv.FormatInt(userID, 10), } return response.WriteJSON(c, fiber.StatusOK, "Tenant Slug retrieved successfully", res, nil) @@ -125,7 +376,7 @@ func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, errMsg) } - emailExist, phoneExist, err := h.userSvc.CheckPhoneEmailExist(c.Context(), req.PhoneNumber, req.Email, domain.ValidInt64{}) + emailExist, phoneExist, err := h.userSvc.CheckPhoneEmailExist(c.Context(), req.PhoneNumber, req.Email) if err != nil { h.mongoLoggerSvc.Error("Failed to check phone/email existence", zap.Any("request", req), @@ -195,7 +446,7 @@ func (h *Handler) SendRegisterCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") } - if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo, domain.AfroMessage, domain.ValidInt64{}); err != nil { + if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo, domain.AfroMessage); err != nil { h.mongoLoggerSvc.Error("Failed to send register code", zap.String("Medium", string(medium)), zap.String("Send To", string(sentTo)), @@ -209,36 +460,29 @@ func (h *Handler) SendRegisterCode(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil) } -type RegisterUserReq 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"` - Otp string `json:"otp" example:"123456"` - ReferralCode string `json:"referral_code" example:"ABC123"` -} - // RegisterUser godoc // @Summary Register user // @Description Register user // @Tags user // @Accept json // @Produce json -// @Param registerUser body RegisterUserReq true "Register user" +// @Param registerUser body domain.RegisterUserReq true "Register user" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/{tenant_slug}/user/register [post] func (h *Handler) RegisterUser(c *fiber.Ctx) error { - var req RegisterUserReq + var req domain.RegisterUserReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse RegisterUser request", 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 c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to register user", + Error: "Invalid request body: " + err.Error(), + }) } if valErrs, ok := h.validator.Validate(c, req); !ok { @@ -246,20 +490,28 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } - return fiber.NewError(fiber.StatusBadRequest, errMsg) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to register user", + Error: errMsg, + }) } + user := domain.RegisterUserReq{ - FirstName: req.FirstName, - LastName: req.LastName, - Email: req.Email, - PhoneNumber: req.PhoneNumber, - Password: req.Password, - Otp: req.Otp, - ReferralCode: req.ReferralCode, - OtpMedium: domain.OtpMediumEmail, - OrganizationID: domain.ValidInt64{}, - Role: string(domain.RoleStudent), + FirstName: req.FirstName, + LastName: req.LastName, + UserName: req.UserName, + Email: req.Email, + PhoneNumber: req.PhoneNumber, + Password: req.Password, + OtpMedium: domain.OtpMediumEmail, + Role: string(domain.RoleStudent), + Age: req.Age, + EducationLevel: req.EducationLevel, + Country: req.Country, + Region: req.Region, + PreferredLanguage: req.PreferredLanguage, } + medium, err := getMedium(req.Email, req.PhoneNumber) if err != nil { h.mongoLoggerSvc.Info("Failed to get medium", @@ -269,25 +521,15 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { zap.Error(err), zap.Time("timestamp", time.Now()), ) - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to register user", + Error: err.Error(), + }) } - user.OtpMedium = medium _, err = h.userSvc.RegisterUser(c.Context(), user) if err != nil { - if errors.Is(err, domain.ErrOtpAlreadyUsed) { - return fiber.NewError(fiber.StatusBadRequest, "Otp already used") - } - if errors.Is(err, domain.ErrOtpExpired) { - return fiber.NewError(fiber.StatusBadRequest, "Otp expired") - } - if errors.Is(err, domain.ErrInvalidOtp) { - return fiber.NewError(fiber.StatusBadRequest, "Invalid otp") - } - if errors.Is(err, domain.ErrOtpNotFound) { - return fiber.NewError(fiber.StatusBadRequest, "User already exist") - } h.mongoLoggerSvc.Error("Failed to register user", zap.String("email", req.Email), zap.String("phone number", req.PhoneNumber), @@ -295,10 +537,17 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { zap.Error(err), zap.Time("timestamp", time.Now()), ) - return fiber.NewError(fiber.StatusInternalServerError, "failed to register user:"+err.Error()) + + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to register user", + Error: err.Error(), + }) } - return response.WriteJSON(c, fiber.StatusOK, "Registration successful", nil, nil) + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Registration successful", + Data: nil, + }) } type ResetCodeReq struct { @@ -355,7 +604,7 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") } - if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, domain.AfroMessage, domain.ValidInt64{}); err != nil { + if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, domain.AfroMessage); err != nil { h.mongoLoggerSvc.Error("Failed to send reset code", zap.String("medium", string(medium)), zap.String("sentTo", string(sentTo)), @@ -422,7 +671,7 @@ func (h *Handler) SendTenantResetCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") } - if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, domain.AfroMessage, domain.ValidInt64{}); err != nil { + if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, domain.AfroMessage); err != nil { h.mongoLoggerSvc.Error("Failed to send reset code", zap.String("medium", string(medium)), zap.String("sentTo", string(sentTo)), @@ -437,10 +686,9 @@ func (h *Handler) SendTenantResetCode(c *fiber.Ctx) error { } type ResetPasswordReq struct { - Email string `json:"email,omitempty" validate:"required_without=PhoneNumber,omitempty,email" example:"john.doe@example.com"` - PhoneNumber string `json:"phone_number,omitempty" validate:"required_without=Email,omitempty" example:"1234567890"` - Password string `json:"password" validate:"required,min=8" example:"newpassword123"` - Otp string `json:"otp" validate:"required" example:"123456"` + UserName string `json:"user_name" validate:"required" example:"johndoe"` + Password string `json:"password" validate:"required,min=8" example:"newpassword123"` + Otp string `json:"otp" validate:"required" example:"123456"` } // ResetPassword godoc @@ -474,24 +722,32 @@ func (h *Handler) ResetPassword(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, errMsg) } - medium, err := getMedium(req.Email, req.PhoneNumber) - if err != nil { - h.mongoLoggerSvc.Info("Failed to determine medium for ResetPassword", - zap.String("Email", req.Email), - zap.String("Phone Number", req.PhoneNumber), - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } + // user, err := h.userSvc.GetUserByUserName(c.Context(), req.UserName) + // if err != nil { + // h.mongoLoggerSvc.Info("Failed to get user by user name", + // zap.String("user_name", req.UserName), + // zap.Int("status_code", fiber.StatusBadRequest), + // zap.Error(err), + // zap.Time("timestamp", time.Now()), + // ) + // } + + // medium, err := getMedium(user.Email, user.PhoneNumber) + // if err != nil { + // h.mongoLoggerSvc.Info("Failed to determine medium for ResetPassword", + // zap.String("Email", user.Email), + // zap.String("Phone Number", user.PhoneNumber), + // zap.Int("status_code", fiber.StatusBadRequest), + // zap.Error(err), + // zap.Time("timestamp", time.Now()), + // ) + // return fiber.NewError(fiber.StatusBadRequest, err.Error()) + // } resetReq := domain.ResetPasswordReq{ - Email: req.Email, - PhoneNumber: req.PhoneNumber, - Password: req.Password, - Otp: req.Otp, - OtpMedium: medium, + UserName: req.UserName, + Password: req.Password, + OtpCode: req.Otp, } if err := h.userSvc.ResetPassword(c.Context(), resetReq); err != nil { @@ -543,25 +799,22 @@ func (h *Handler) ResetTenantPassword(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, errMsg) } - medium, err := getMedium(req.Email, req.PhoneNumber) - if err != nil { - h.mongoLoggerSvc.Info("Failed to determine medium for ResetPassword", - zap.String("Email", req.Email), - zap.String("Phone Number", req.PhoneNumber), - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } + // medium, err := getMedium(req.Email, req.PhoneNumber) + // if err != nil { + // h.mongoLoggerSvc.Info("Failed to determine medium for ResetPassword", + // zap.String("Email", req.Email), + // zap.String("Phone Number", req.PhoneNumber), + // zap.Int("status_code", fiber.StatusBadRequest), + // zap.Error(err), + // zap.Time("timestamp", time.Now()), + // ) + // return fiber.NewError(fiber.StatusBadRequest, err.Error()) + // } resetReq := domain.ResetPasswordReq{ - Email: req.Email, - PhoneNumber: req.PhoneNumber, - Password: req.Password, - Otp: req.Otp, - OtpMedium: medium, - OrganizationID: 1, + UserName: req.UserName, + Password: req.Password, + OtpCode: req.Otp, } if err := h.userSvc.ResetPassword(c.Context(), resetReq); err != nil { @@ -577,52 +830,18 @@ func (h *Handler) ResetTenantPassword(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Password reset successful", nil, nil) } -type UserProfileRes 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"` - ReferralCode string `json:"referral_code"` -} - -type CustomerProfileRes 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"` - ReferralCode string `json:"referral_code"` -} - // CustomerProfile godoc // @Summary Get user profile // @Description Get user profile // @Tags user // @Accept json // @Produce json -// @Success 200 {object} CustomerProfileRes +// @Success 200 {object} domain.UserProfileResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Security Bearer -// @Router /api/v1/{tenant_slug}/user/customer-profile [get] -func (h *Handler) CustomerProfile(c *fiber.Ctx) error { +// @Router /api/v1/{tenant_slug}/user/user-profile [get] +func (h *Handler) GetUserProfile(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { @@ -660,20 +879,27 @@ func (h *Handler) CustomerProfile(c *fiber.Ctx) error { lastLogin = &user.CreatedAt } - res := CustomerProfileRes{ - 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, + res := domain.UserProfileResponse{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + UserName: user.UserName, + Email: user.Email, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + Age: user.Age, + EducationLevel: user.EducationLevel, + Country: user.Country, + Region: user.Region, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + Status: user.Status, + LastLogin: lastLogin, + ProfileCompleted: user.ProfileCompleted, + ProfilePictureURL: user.ProfilePictureURL, + PreferredLanguage: user.PreferredLanguage, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, } return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil) } @@ -742,21 +968,29 @@ func (h *Handler) AdminProfile(c *fiber.Ctx) error { lastLogin = &user.CreatedAt } - res := UserProfileRes{ - 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, + res := domain.UserProfileResponse{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + UserName: user.UserName, + Email: user.Email, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + Age: user.Age, + EducationLevel: user.EducationLevel, + Country: user.Country, + Region: user.Region, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + Status: user.Status, + LastLogin: lastLogin, + ProfileCompleted: user.ProfileCompleted, + ProfilePictureURL: user.ProfilePictureURL, + PreferredLanguage: user.PreferredLanguage, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, } + return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil) } @@ -783,25 +1017,23 @@ type SearchUserByNameOrPhoneReq struct { // @Accept json // @Produce json // @Param searchUserByNameOrPhone body SearchUserByNameOrPhoneReq true "Search for using his name or phone" -// @Success 200 {object} UserProfileRes +// @Success 200 {object} domain.UserProfileResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/user/search [post] func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { - - // TODO: Add filtering by role based on which user is calling this var req SearchUserByNameOrPhoneReq if err := c.BodyParser(&req); err != nil { - h.mongoLoggerSvc.Error("Failed to Search UserBy Name Or Phone failed", + h.mongoLoggerSvc.Error("SearchUserByNameOrPhone failed - invalid request body", zap.Any("request", req), 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) @@ -809,146 +1041,80 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, errMsg) } - // companyID := c.Locals("company_id").(domain.ValidInt64) - // strCompanyID := fmt.Sprintf("%v", companyID) - role, err := strconv.ParseInt(string(*req.Role), 10, 64) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "failed to get users"+err.Error()) + // Optional role filter + var rolePtr *int64 + if req.Role != nil && *req.Role != "" { + roleStr := string(*req.Role) + roleVal, err := strconv.ParseInt(roleStr, 10, 64) + if err != nil { + h.mongoLoggerSvc.Info("Invalid role value", + zap.String("role", roleStr), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "invalid role value") + } + rolePtr = &roleVal } - users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, &role, nil) + users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, rolePtr) if err != nil { - h.mongoLoggerSvc.Error("Failed to get user by name or phone", + h.mongoLoggerSvc.Error("SearchUserByNameOrPhone - failed to fetch users", zap.Any("request", req), - zap.Int("status_code", fiber.StatusBadRequest), + zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) - return fiber.NewError(fiber.StatusBadRequest, "failed to get users"+err.Error()) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get users: "+err.Error()) } - var res []UserProfileRes = make([]UserProfileRes, 0, len(users)) + + res := make([]domain.UserProfileResponse, 0, len(users)) for _, user := range users { 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.Any("userID", user.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()) - } - + if err != nil && err != authentication.ErrRefreshTokenNotFound { + h.mongoLoggerSvc.Error("Failed to get user last login", + zap.Int64("user_id", user.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()) + } + if err == authentication.ErrRefreshTokenNotFound { lastLogin = &user.CreatedAt } - res = append(res, UserProfileRes{ - 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, + + // var orgID *int64 + // if user.OrganizationID.Valid { + // orgID = &user.OrganizationID.Value + // } + + res = append(res, domain.UserProfileResponse{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + UserName: user.UserName, + Email: user.Email, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + Age: user.Age, + EducationLevel: user.EducationLevel, + Country: user.Country, + Region: user.Region, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + Status: user.Status, + LastLogin: lastLogin, + ProfileCompleted: user.ProfileCompleted, + ProfilePictureURL: user.ProfilePictureURL, + PreferredLanguage: user.PreferredLanguage, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, }) } - return response.WriteJSON(c, fiber.StatusOK, "Search Successful", res, nil) - -} - -// SearchUserByNameOrPhone godoc -// @Summary Search for user using name or phone -// @Description Search for user using name or phone -// @Tags user -// @Accept json -// @Produce json -// @Param searchUserByNameOrPhone body SearchUserByNameOrPhoneReq true "Search for using his name or phone" -// @Success 200 {object} UserProfileRes -// @Failure 400 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Router /api/v1/{tenant_slug}/user/search [post] -func (h *Handler) SearchCompanyUserByNameOrPhone(c *fiber.Ctx) error { - // companyID := c.Locals("company_id").(domain.ValidInt64) - // if !companyID.Valid { - // h.BadRequestLogger().Error("invalid company id") - // return fiber.NewError(fiber.StatusBadRequest, "invalid company id") - // } - - var req SearchUserByNameOrPhoneReq - if err := c.BodyParser(&req); err != nil { - h.mongoLoggerSvc.Error("Failed to Search UserBy Name Or Phone failed", - zap.Any("request", req), - 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) - } - return fiber.NewError(fiber.StatusBadRequest, errMsg) - } - - // strCompanyID := fmt.Sprintf("%v", companyID) - role, err := strconv.ParseInt(string(*req.Role), 10, 64) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "failed to get users"+err.Error()) - } - - users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, &role, nil) - if err != nil { - h.mongoLoggerSvc.Error("Failed to get user by name or phone", - zap.Any("request", req), - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, "failed to get users"+err.Error()) - } - var res []UserProfileRes = make([]UserProfileRes, 0, len(users)) - for _, user := range users { - 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.Any("userID", user.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()) - } - - lastLogin = &user.CreatedAt - } - res = append(res, UserProfileRes{ - 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, "Search Successful", res, nil) + return response.WriteJSON(c, fiber.StatusOK, "Search successful", res, nil) } // GetUserByID godoc @@ -958,27 +1124,12 @@ func (h *Handler) SearchCompanyUserByNameOrPhone(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param id path int true "User ID" -// @Success 200 {object} UserProfileRes +// @Success 200 {object} domain.UserProfileResponse // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/user/single/{id} [get] func (h *Handler) GetUserByID(c *fiber.Ctx) error { - // branchId := int64(12) //c.Locals("branch_id").(int64) - // filter := user.Filter{ - // Role: string(domain.RoleUser), - // BranchId: user.ValidBranchId{ - // Value: branchId, - // Valid: true, - // }, - // Page: c.QueryInt("page", 1), - // PageSize: c.QueryInt("page_size", 10), - // } - // valErrs, ok := validator.Validate(c, filter) - // if !ok { - // return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - // } - userIDstr := c.Params("id") userID, err := strconv.ParseInt(userIDstr, 10, 64) if err != nil { @@ -999,42 +1150,52 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error { zap.Error(err), zap.Time("timestamp", time.Now()), ) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get cashiers:"+err.Error()) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get user: "+err.Error()) } 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", user.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()) - } - + if err != nil && err != authentication.ErrRefreshTokenNotFound { + h.mongoLoggerSvc.Error("Failed to get user last login", + zap.Int64("userID", user.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()) + } + if err == authentication.ErrRefreshTokenNotFound { lastLogin = &user.CreatedAt } - res := UserProfileRes{ - 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, + // var orgID *int64 + // if user.OrganizationID.Valid { + // orgID = &user.OrganizationID.Value + // } + + res := domain.UserProfileResponse{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + UserName: user.UserName, + Email: user.Email, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + Age: user.Age, + EducationLevel: user.EducationLevel, + Country: user.Country, + Region: user.Region, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + Status: user.Status, + LastLogin: lastLogin, + ProfileCompleted: user.ProfileCompleted, + ProfilePictureURL: user.ProfilePictureURL, + PreferredLanguage: user.PreferredLanguage, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, } return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil) - } // DeleteUser godoc @@ -1083,50 +1244,3 @@ type UpdateUserSuspendRes struct { UserID int64 `json:"user_id"` Suspended bool `json:"suspended"` } - -// UpdateUserSuspend godoc -// @Summary Suspend or unsuspend a user -// @Description Suspend or unsuspend a user -// @Tags user -// @Accept json -// @Produce json -// @Param updateUserSuspend body UpdateUserSuspendReq true "Suspend or unsuspend a user" -// @Success 200 {object} UpdateUserSuspendRes -// @Failure 400 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Router /api/v1/user/suspend [post] -func (h *Handler) UpdateUserSuspend(c *fiber.Ctx) error { - var req UpdateUserSuspendReq - if err := c.BodyParser(&req); err != nil { - h.mongoLoggerSvc.Info("Failed to parse UpdateUserSuspend request", - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) - } - 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 fiber.NewError(fiber.StatusBadRequest, errMsg) - } - - err := h.userSvc.UpdateUserSuspend(c.Context(), req.UserID, req.Suspended) - if err != nil { - h.mongoLoggerSvc.Error("Failed to update user suspend status", - zap.Any("UpdateUserSuspendReq", req), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update user suspend status:"+err.Error()) - } - - res := UpdateUserSuspendRes{ - UserID: req.UserID, - Suspended: req.Suspended, - } - return response.WriteJSON(c, fiber.StatusOK, "User suspend status updated successfully", res, nil) -} diff --git a/internal/web_server/jwt/jwt.go b/internal/web_server/jwt/jwt.go index d50a8db..cadd7a9 100644 --- a/internal/web_server/jwt/jwt.go +++ b/internal/web_server/jwt/jwt.go @@ -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 diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index e334834..9f88ffc 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -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) diff --git a/makefile copy b/makefile copy deleted file mode 100644 index fba54ad..0000000 --- a/makefile copy +++ /dev/null @@ -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