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