changed age to agegroup, added refresh route, token generation after otp verification
This commit is contained in:
parent
513927f48f
commit
9ee1d7f714
285
README.md
285
README.md
|
|
@ -69,3 +69,288 @@ cd Yimaru-backend
|
||||||
├── makefile # Development and operations commands
|
├── makefile # Development and operations commands
|
||||||
├── .env # Environment configuration file
|
├── .env # Environment configuration file
|
||||||
└── README.md # Project documentation
|
└── README.md # Project documentation
|
||||||
|
|
||||||
|
|
||||||
|
1. Course Category (Top-Level Classification)
|
||||||
|
|
||||||
|
Table: course_categories
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
Logical grouping of courses (e.g., Learning English, Other Courses).
|
||||||
|
|
||||||
|
Key Fields:
|
||||||
|
|
||||||
|
id – Primary identifier
|
||||||
|
|
||||||
|
name – Category name
|
||||||
|
|
||||||
|
is_active – Soft enable/disable
|
||||||
|
|
||||||
|
created_at – Audit timestamp
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
One Course Category → Many Courses
|
||||||
|
|
||||||
|
Course Category
|
||||||
|
└── Courses[]
|
||||||
|
|
||||||
|
2. Course
|
||||||
|
|
||||||
|
Table: courses
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
Represents a full course offering under a category.
|
||||||
|
|
||||||
|
Key Fields:
|
||||||
|
|
||||||
|
category_id – FK → course_categories.id
|
||||||
|
|
||||||
|
title, description
|
||||||
|
|
||||||
|
is_active
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
Belongs to one Course Category
|
||||||
|
|
||||||
|
Has many Programs
|
||||||
|
|
||||||
|
Course Category
|
||||||
|
└── Course
|
||||||
|
└── Programs[]
|
||||||
|
|
||||||
|
3. Program
|
||||||
|
|
||||||
|
Table: programs
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
A structured learning track or syllabus within a course
|
||||||
|
(e.g., Beginner Track, Advanced Track).
|
||||||
|
|
||||||
|
Key Fields:
|
||||||
|
|
||||||
|
course_id – FK → courses.id
|
||||||
|
|
||||||
|
title, description
|
||||||
|
|
||||||
|
thumbnail
|
||||||
|
|
||||||
|
display_order
|
||||||
|
|
||||||
|
is_active
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
Belongs to one Course
|
||||||
|
|
||||||
|
Has many Levels
|
||||||
|
|
||||||
|
Course
|
||||||
|
└── Program
|
||||||
|
└── Levels[]
|
||||||
|
|
||||||
|
4. Level
|
||||||
|
|
||||||
|
Table: levels
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
Represents a progression stage inside a program (Level 1, Level 2, etc.).
|
||||||
|
|
||||||
|
Key Fields:
|
||||||
|
|
||||||
|
program_id – FK → programs.id
|
||||||
|
|
||||||
|
title, description
|
||||||
|
|
||||||
|
level_index
|
||||||
|
|
||||||
|
Aggregates:
|
||||||
|
|
||||||
|
number_of_modules
|
||||||
|
|
||||||
|
number_of_practices
|
||||||
|
|
||||||
|
number_of_videos
|
||||||
|
|
||||||
|
is_active
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
Belongs to one Program
|
||||||
|
|
||||||
|
Has many Modules
|
||||||
|
|
||||||
|
Can directly own Practices
|
||||||
|
|
||||||
|
Program
|
||||||
|
└── Level
|
||||||
|
├── Modules[]
|
||||||
|
└── Practices[] (owner_type = LEVEL)
|
||||||
|
|
||||||
|
5. Module
|
||||||
|
|
||||||
|
Table: modules
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
A lesson or unit inside a level.
|
||||||
|
|
||||||
|
Key Fields:
|
||||||
|
|
||||||
|
level_id – FK → levels.id
|
||||||
|
|
||||||
|
title
|
||||||
|
|
||||||
|
content
|
||||||
|
|
||||||
|
display_order
|
||||||
|
|
||||||
|
is_active
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
Belongs to one Level
|
||||||
|
|
||||||
|
Has many Videos
|
||||||
|
|
||||||
|
Can directly own Practices
|
||||||
|
|
||||||
|
Level
|
||||||
|
└── Module
|
||||||
|
├── Module Videos[]
|
||||||
|
└── Practices[] (owner_type = MODULE)
|
||||||
|
|
||||||
|
6. Module Video
|
||||||
|
|
||||||
|
Table: module_videos
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
Actual video learning content attached to a module.
|
||||||
|
|
||||||
|
Key Fields:
|
||||||
|
|
||||||
|
module_id – FK → modules.id
|
||||||
|
|
||||||
|
title, description
|
||||||
|
|
||||||
|
video_url
|
||||||
|
|
||||||
|
duration, resolution
|
||||||
|
|
||||||
|
Publishing controls:
|
||||||
|
|
||||||
|
is_published
|
||||||
|
|
||||||
|
publish_date
|
||||||
|
|
||||||
|
visibility
|
||||||
|
|
||||||
|
instructor_id
|
||||||
|
|
||||||
|
thumbnail
|
||||||
|
|
||||||
|
is_active
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
Belongs to one Module
|
||||||
|
|
||||||
|
Module
|
||||||
|
└── Module Video
|
||||||
|
|
||||||
|
7. Practice (Polymorphic Ownership)
|
||||||
|
|
||||||
|
Table: practices
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
Exercises or assessments that can belong to either a Level or a Module.
|
||||||
|
|
||||||
|
Key Fields:
|
||||||
|
|
||||||
|
owner_type – LEVEL | MODULE
|
||||||
|
|
||||||
|
owner_id – ID of level or module
|
||||||
|
|
||||||
|
title, description
|
||||||
|
|
||||||
|
banner_image
|
||||||
|
|
||||||
|
persona
|
||||||
|
|
||||||
|
is_active
|
||||||
|
|
||||||
|
Constraint:
|
||||||
|
|
||||||
|
Enforced by CHECK (owner_type IN ('LEVEL', 'MODULE'))
|
||||||
|
|
||||||
|
Ownership enforced at the application layer
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
One Practice → Many Practice Questions
|
||||||
|
|
||||||
|
Level or Module
|
||||||
|
└── Practice
|
||||||
|
└── Practice Questions[]
|
||||||
|
|
||||||
|
8. Practice Question (Lowest Level)
|
||||||
|
|
||||||
|
Table: practice_questions
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
Individual questions within a practice session.
|
||||||
|
|
||||||
|
Key Fields:
|
||||||
|
|
||||||
|
practice_id – FK → practices.id
|
||||||
|
|
||||||
|
question
|
||||||
|
|
||||||
|
Voice support:
|
||||||
|
|
||||||
|
question_voice_prompt
|
||||||
|
|
||||||
|
sample_answer_voice_prompt
|
||||||
|
|
||||||
|
sample_answer
|
||||||
|
|
||||||
|
tips
|
||||||
|
|
||||||
|
type – MCQ | TRUE_FALSE | SHORT
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
Belongs to one Practice
|
||||||
|
|
||||||
|
Practice
|
||||||
|
└── Practice Question
|
||||||
|
|
||||||
|
Complete Hierarchical Flow (Compact View)
|
||||||
|
Course Category
|
||||||
|
└── Course
|
||||||
|
└── Program
|
||||||
|
└── Level
|
||||||
|
├── Module
|
||||||
|
│ ├── Module Video
|
||||||
|
│ └── Practice (MODULE)
|
||||||
|
│ └── Practice Question
|
||||||
|
└── Practice (LEVEL)
|
||||||
|
└── Practice Question
|
||||||
|
|
||||||
|
Architectural Observations
|
||||||
|
|
||||||
|
Strict top-down hierarchy until Level
|
||||||
|
|
||||||
|
Polymorphic design for practices allows reuse without table duplication
|
||||||
|
|
||||||
|
Cascade deletes ensure referential integrity
|
||||||
|
|
||||||
|
Aggregated counters in levels support fast analytics and UI summaries
|
||||||
|
|
||||||
|
Schema is well-suited for:
|
||||||
|
|
||||||
|
LMS platforms
|
||||||
|
|
||||||
|
Progressive learning apps
|
||||||
|
|
||||||
|
Video + assessment-based education systems
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
// "context"
|
// "context"
|
||||||
|
|
||||||
// "context"
|
// "context"
|
||||||
|
_ "Yimaru-Backend/docs"
|
||||||
"Yimaru-Backend/internal/config"
|
"Yimaru-Backend/internal/config"
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
customlogger "Yimaru-Backend/internal/logger"
|
customlogger "Yimaru-Backend/internal/logger"
|
||||||
|
|
@ -91,6 +92,7 @@ func main() {
|
||||||
// )
|
// )
|
||||||
|
|
||||||
userSvc := user.NewService(
|
userSvc := user.NewService(
|
||||||
|
repository.NewTokenStore(store),
|
||||||
repository.NewUserStore(store),
|
repository.NewUserStore(store),
|
||||||
repository.NewOTPStore(store),
|
repository.NewOTPStore(store),
|
||||||
messengerSvc,
|
messengerSvc,
|
||||||
|
|
@ -98,6 +100,7 @@ func main() {
|
||||||
)
|
)
|
||||||
|
|
||||||
authSvc := authentication.NewService(
|
authSvc := authentication.NewService(
|
||||||
|
repository.NewOTPStore(store),
|
||||||
repository.NewUserStore(store),
|
repository.NewUserStore(store),
|
||||||
*userSvc,
|
*userSvc,
|
||||||
repository.NewTokenStore(store),
|
repository.NewTokenStore(store),
|
||||||
|
|
|
||||||
|
|
@ -5,70 +5,133 @@ INSERT INTO users (
|
||||||
id,
|
id,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
-- user_name,
|
gender,
|
||||||
|
birth_day,
|
||||||
email,
|
email,
|
||||||
phone_number,
|
phone_number,
|
||||||
role,
|
role,
|
||||||
password,
|
password,
|
||||||
status,
|
age,
|
||||||
|
education_level,
|
||||||
|
country,
|
||||||
|
region,
|
||||||
|
knowledge_level,
|
||||||
|
nick_name,
|
||||||
|
occupation,
|
||||||
|
learning_goal,
|
||||||
|
language_goal,
|
||||||
|
language_challange,
|
||||||
|
favourite_topic,
|
||||||
|
initial_assessment_completed,
|
||||||
email_verified,
|
email_verified,
|
||||||
phone_verified,
|
phone_verified,
|
||||||
|
status,
|
||||||
|
last_login,
|
||||||
profile_completed,
|
profile_completed,
|
||||||
|
profile_picture_url,
|
||||||
preferred_language,
|
preferred_language,
|
||||||
created_at
|
created_at,
|
||||||
|
updated_at
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
10,
|
10,
|
||||||
'Demo',
|
'Demo',
|
||||||
'Student',
|
'Student',
|
||||||
-- 'demo_student',
|
'Male',
|
||||||
|
'2000-01-01',
|
||||||
'student10@yimaru.com',
|
'student10@yimaru.com',
|
||||||
NULL,
|
NULL,
|
||||||
'USER',
|
'USER',
|
||||||
crypt('password@123', gen_salt('bf'))::bytea,
|
crypt('password@123', gen_salt('bf'))::bytea,
|
||||||
'ACTIVE',
|
22,
|
||||||
|
'Bachelor',
|
||||||
|
'Ethiopia',
|
||||||
|
'Addis Ababa',
|
||||||
|
'BEGINNER',
|
||||||
|
'Demo',
|
||||||
|
'Student',
|
||||||
|
'Learn programming',
|
||||||
|
'English',
|
||||||
|
'Grammar',
|
||||||
|
'Technology',
|
||||||
|
FALSE,
|
||||||
TRUE,
|
TRUE,
|
||||||
FALSE,
|
FALSE,
|
||||||
|
'ACTIVE',
|
||||||
|
NULL,
|
||||||
FALSE,
|
FALSE,
|
||||||
|
NULL,
|
||||||
'en',
|
'en',
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP,
|
||||||
|
NULL
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
11,
|
11,
|
||||||
'System',
|
'System',
|
||||||
'Admin',
|
'Admin',
|
||||||
-- 'sys_admin',
|
'Female',
|
||||||
|
'1995-01-01',
|
||||||
'admin@yimaru.com',
|
'admin@yimaru.com',
|
||||||
'0911001100',
|
'0911001100',
|
||||||
'ADMIN',
|
'ADMIN',
|
||||||
crypt('password@123', gen_salt('bf'))::bytea,
|
crypt('password@123', gen_salt('bf'))::bytea,
|
||||||
|
28,
|
||||||
|
'Master',
|
||||||
|
'Ethiopia',
|
||||||
|
'Addis Ababa',
|
||||||
|
'ADVANCED',
|
||||||
|
'SysAdmin',
|
||||||
|
'Administrator',
|
||||||
|
'Manage system',
|
||||||
|
'English',
|
||||||
|
'Writing',
|
||||||
|
'Management',
|
||||||
|
TRUE,
|
||||||
|
TRUE,
|
||||||
|
TRUE,
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
|
NULL,
|
||||||
TRUE,
|
TRUE,
|
||||||
TRUE,
|
NULL,
|
||||||
TRUE,
|
|
||||||
'en',
|
'en',
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP,
|
||||||
|
NULL
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
12,
|
12,
|
||||||
'Support',
|
'Support',
|
||||||
'Agent',
|
'Agent',
|
||||||
-- 'support_agent',
|
'Female',
|
||||||
|
'1998-01-01',
|
||||||
'support@yimaru.com',
|
'support@yimaru.com',
|
||||||
'0911223344',
|
'0911223344',
|
||||||
'SUPPORT',
|
'SUPPORT',
|
||||||
crypt('password@123', gen_salt('bf'))::bytea,
|
crypt('password@123', gen_salt('bf'))::bytea,
|
||||||
|
25,
|
||||||
|
'Diploma',
|
||||||
|
'Ethiopia',
|
||||||
|
'Addis Ababa',
|
||||||
|
'INTERMEDIATE',
|
||||||
|
'Support',
|
||||||
|
'Agent',
|
||||||
|
'Assist users',
|
||||||
|
'English',
|
||||||
|
'Conversation',
|
||||||
|
'Customer Service',
|
||||||
|
TRUE,
|
||||||
|
TRUE,
|
||||||
|
TRUE,
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
|
NULL,
|
||||||
TRUE,
|
TRUE,
|
||||||
TRUE,
|
NULL,
|
||||||
TRUE,
|
|
||||||
'en',
|
'en',
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP,
|
||||||
|
NULL
|
||||||
)
|
)
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
|
||||||
-- ======================================================
|
-- ======================================================
|
||||||
-- Global Settings (LMS)
|
-- Global Settings (LMS)
|
||||||
-- ======================================================
|
-- ======================================================
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,28 @@ CREATE TABLE IF NOT EXISTS users (
|
||||||
CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
|
CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Remove the old column
|
||||||
|
ALTER TABLE users
|
||||||
|
DROP COLUMN age;
|
||||||
|
|
||||||
|
-- Add age_group with constrained values
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN age_group VARCHAR(20);
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD CONSTRAINT users_age_group_check
|
||||||
|
CHECK (
|
||||||
|
age_group IN (
|
||||||
|
'UNDER_13',
|
||||||
|
'13_17',
|
||||||
|
'18_24',
|
||||||
|
'25_34',
|
||||||
|
'35_44',
|
||||||
|
'45_54',
|
||||||
|
'55_PLUS'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS assessment_questions (
|
CREATE TABLE IF NOT EXISTS assessment_questions (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
|
@ -242,3 +264,4 @@ CREATE TABLE IF NOT EXISTS reported_issues (
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ FROM users
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
|
|
||||||
-- name: IsProfileCompleted :one
|
-- name: IsProfileCompleted :one
|
||||||
SELECT
|
SELECT
|
||||||
CASE WHEN profile_completed = true THEN true ELSE false END AS is_pending
|
CASE WHEN profile_completed = true THEN true ELSE false END AS is_pending
|
||||||
|
|
@ -12,6 +13,7 @@ FROM users
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
|
|
||||||
-- name: IsUserNameUnique :one
|
-- name: IsUserNameUnique :one
|
||||||
SELECT
|
SELECT
|
||||||
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique
|
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique
|
||||||
|
|
@ -19,6 +21,7 @@ FROM users
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- name: CreateUser :one
|
-- name: CreateUser :one
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
first_name,
|
first_name,
|
||||||
|
|
@ -29,7 +32,7 @@ INSERT INTO users (
|
||||||
phone_number,
|
phone_number,
|
||||||
role,
|
role,
|
||||||
password,
|
password,
|
||||||
age,
|
age_group,
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
|
@ -51,33 +54,33 @@ INSERT INTO users (
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, -- first_name
|
$1,
|
||||||
$2, -- last_name
|
$2,
|
||||||
$3, -- gender
|
$3,
|
||||||
$4, -- birth_day
|
$4,
|
||||||
$5, -- email
|
$5,
|
||||||
$6, -- phone_number
|
$6,
|
||||||
$7, -- role
|
$7,
|
||||||
$8, -- password
|
$8,
|
||||||
$9, -- age
|
$9, -- age_group
|
||||||
$10, -- education_level
|
$10,
|
||||||
$11, -- country
|
$11,
|
||||||
$12, -- region
|
$12,
|
||||||
|
|
||||||
$13, -- nick_name
|
$13,
|
||||||
$14, -- occupation
|
$14,
|
||||||
$15, -- learning_goal
|
$15,
|
||||||
$16, -- language_goal
|
$16,
|
||||||
$17, -- language_challange
|
$17,
|
||||||
$18, -- favourite_topic
|
$18,
|
||||||
|
|
||||||
$19, -- initial_assessment_completed
|
$19,
|
||||||
$20, -- email_verified
|
$20,
|
||||||
$21, -- phone_verified
|
$21,
|
||||||
$22, -- status
|
$22,
|
||||||
$23, -- profile_completed
|
$23,
|
||||||
$24, -- profile_picture_url
|
$24,
|
||||||
$25, -- preferred_language
|
$25,
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
|
|
@ -89,7 +92,7 @@ RETURNING
|
||||||
email,
|
email,
|
||||||
phone_number,
|
phone_number,
|
||||||
role,
|
role,
|
||||||
age,
|
age_group,
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
|
@ -111,11 +114,13 @@ RETURNING
|
||||||
created_at,
|
created_at,
|
||||||
updated_at;
|
updated_at;
|
||||||
|
|
||||||
|
|
||||||
-- name: GetUserByID :one
|
-- name: GetUserByID :one
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
|
|
||||||
-- name: GetAllUsers :many
|
-- name: GetAllUsers :many
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) OVER () AS total_count,
|
COUNT(*) OVER () AS total_count,
|
||||||
|
|
@ -127,10 +132,11 @@ SELECT
|
||||||
email,
|
email,
|
||||||
phone_number,
|
phone_number,
|
||||||
role,
|
role,
|
||||||
age,
|
age_group,
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
knowledge_level,
|
||||||
nick_name,
|
nick_name,
|
||||||
occupation,
|
occupation,
|
||||||
learning_goal,
|
learning_goal,
|
||||||
|
|
@ -138,30 +144,26 @@ SELECT
|
||||||
language_challange,
|
language_challange,
|
||||||
favourite_topic,
|
favourite_topic,
|
||||||
initial_assessment_completed,
|
initial_assessment_completed,
|
||||||
profile_picture_url,
|
|
||||||
preferred_language,
|
|
||||||
email_verified,
|
email_verified,
|
||||||
phone_verified,
|
phone_verified,
|
||||||
status,
|
status,
|
||||||
|
last_login,
|
||||||
profile_completed,
|
profile_completed,
|
||||||
|
profile_picture_url,
|
||||||
|
preferred_language,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM users
|
FROM users
|
||||||
WHERE ($1 IS NULL OR role = $1)
|
LIMIT sqlc.narg('limit')::INT
|
||||||
AND ($2 IS NULL OR first_name ILIKE '%' || $2 || '%'
|
OFFSET sqlc.narg('offset')::INT;
|
||||||
OR last_name ILIKE '%' || $2 || '%'
|
|
||||||
OR phone_number ILIKE '%' || $2 || '%'
|
|
||||||
OR email ILIKE '%' || $2 || '%')
|
|
||||||
AND ($3 IS NULL OR created_at >= $3)
|
|
||||||
AND ($4 IS NULL OR created_at <= $4)
|
|
||||||
LIMIT $5
|
|
||||||
OFFSET $6;
|
|
||||||
|
|
||||||
-- name: GetTotalUsers :one
|
-- name: GetTotalUsers :one
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM users
|
FROM users
|
||||||
WHERE (role = $1 OR $1 IS NULL);
|
WHERE (role = $1 OR $1 IS NULL);
|
||||||
|
|
||||||
|
|
||||||
-- name: SearchUserByNameOrPhone :many
|
-- name: SearchUserByNameOrPhone :many
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
|
@ -172,7 +174,7 @@ SELECT
|
||||||
email,
|
email,
|
||||||
phone_number,
|
phone_number,
|
||||||
role,
|
role,
|
||||||
age,
|
age_group,
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
|
@ -205,17 +207,14 @@ WHERE (
|
||||||
OR sqlc.narg('role') IS NULL
|
OR sqlc.narg('role') IS NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
-- name: UpdateUser :exec
|
-- name: UpdateUser :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
first_name = COALESCE($1, first_name),
|
first_name = COALESCE($1, first_name),
|
||||||
last_name = COALESCE($2, last_name),
|
last_name = COALESCE($2, last_name),
|
||||||
|
|
||||||
-- email = COALESCE($3, email),
|
|
||||||
-- phone_number = COALESCE($4, phone_number),
|
|
||||||
|
|
||||||
knowledge_level = COALESCE($3, knowledge_level),
|
knowledge_level = COALESCE($3, knowledge_level),
|
||||||
age = COALESCE($4, age),
|
age_group = COALESCE($4, age_group),
|
||||||
education_level = COALESCE($5, education_level),
|
education_level = COALESCE($5, education_level),
|
||||||
country = COALESCE($6, country),
|
country = COALESCE($6, country),
|
||||||
region = COALESCE($7, region),
|
region = COALESCE($7, region),
|
||||||
|
|
@ -226,14 +225,11 @@ SET
|
||||||
language_challange = COALESCE($12, language_challange),
|
language_challange = COALESCE($12, language_challange),
|
||||||
favourite_topic = COALESCE($13, favourite_topic),
|
favourite_topic = COALESCE($13, favourite_topic),
|
||||||
initial_assessment_completed = COALESCE($14, initial_assessment_completed),
|
initial_assessment_completed = COALESCE($14, initial_assessment_completed),
|
||||||
-- email_verified = COALESCE($15, email_verified),
|
|
||||||
-- phone_verified = COALESCE($16, phone_verified),
|
|
||||||
-- status = COALESCE($19, status),
|
|
||||||
profile_completed = COALESCE($15, profile_completed),
|
profile_completed = COALESCE($15, profile_completed),
|
||||||
profile_picture_url = COALESCE($16, profile_picture_url),
|
profile_picture_url = COALESCE($16, profile_picture_url),
|
||||||
preferred_language = COALESCE($17, preferred_language),
|
preferred_language = COALESCE($17, preferred_language),
|
||||||
gender = COALESCE($18, gender),
|
gender = COALESCE($18, gender),
|
||||||
birth_day = COALESCE($19, gender),
|
birth_day = COALESCE($19, birth_day),
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $20;
|
WHERE id = $20;
|
||||||
|
|
||||||
|
|
@ -241,6 +237,7 @@ WHERE id = $20;
|
||||||
DELETE FROM users
|
DELETE FROM users
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
|
|
||||||
-- name: CheckPhoneEmailExist :one
|
-- name: CheckPhoneEmailExist :one
|
||||||
SELECT
|
SELECT
|
||||||
EXISTS (
|
EXISTS (
|
||||||
|
|
@ -250,6 +247,7 @@ SELECT
|
||||||
SELECT 1 FROM users u2 WHERE u2.email = $2
|
SELECT 1 FROM users u2 WHERE u2.email = $2
|
||||||
) AS email_exists;
|
) AS email_exists;
|
||||||
|
|
||||||
|
|
||||||
-- -- name: GetUserByUserName :one
|
-- -- name: GetUserByUserName :one
|
||||||
-- SELECT
|
-- SELECT
|
||||||
-- id,
|
-- id,
|
||||||
|
|
@ -295,7 +293,7 @@ SELECT
|
||||||
phone_number,
|
phone_number,
|
||||||
role,
|
role,
|
||||||
password,
|
password,
|
||||||
age,
|
age_group,
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
|
@ -341,3 +339,4 @@ SET
|
||||||
knowledge_level = $1,
|
knowledge_level = $1,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $2;
|
WHERE id = $2;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,6 @@ type User struct {
|
||||||
PhoneNumber pgtype.Text `json:"phone_number"`
|
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Password []byte `json:"password"`
|
Password []byte `json:"password"`
|
||||||
Age pgtype.Int4 `json:"age"`
|
|
||||||
EducationLevel pgtype.Text `json:"education_level"`
|
EducationLevel pgtype.Text `json:"education_level"`
|
||||||
Country pgtype.Text `json:"country"`
|
Country pgtype.Text `json:"country"`
|
||||||
Region pgtype.Text `json:"region"`
|
Region pgtype.Text `json:"region"`
|
||||||
|
|
@ -242,4 +241,5 @@ type User struct {
|
||||||
PreferredLanguage pgtype.Text `json:"preferred_language"`
|
PreferredLanguage pgtype.Text `json:"preferred_language"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
AgeGroup pgtype.Text `json:"age_group"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ INSERT INTO users (
|
||||||
phone_number,
|
phone_number,
|
||||||
role,
|
role,
|
||||||
password,
|
password,
|
||||||
age,
|
age_group,
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
|
@ -70,33 +70,33 @@ INSERT INTO users (
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, -- first_name
|
$1,
|
||||||
$2, -- last_name
|
$2,
|
||||||
$3, -- gender
|
$3,
|
||||||
$4, -- birth_day
|
$4,
|
||||||
$5, -- email
|
$5,
|
||||||
$6, -- phone_number
|
$6,
|
||||||
$7, -- role
|
$7,
|
||||||
$8, -- password
|
$8,
|
||||||
$9, -- age
|
$9, -- age_group
|
||||||
$10, -- education_level
|
$10,
|
||||||
$11, -- country
|
$11,
|
||||||
$12, -- region
|
$12,
|
||||||
|
|
||||||
$13, -- nick_name
|
$13,
|
||||||
$14, -- occupation
|
$14,
|
||||||
$15, -- learning_goal
|
$15,
|
||||||
$16, -- language_goal
|
$16,
|
||||||
$17, -- language_challange
|
$17,
|
||||||
$18, -- favourite_topic
|
$18,
|
||||||
|
|
||||||
$19, -- initial_assessment_completed
|
$19,
|
||||||
$20, -- email_verified
|
$20,
|
||||||
$21, -- phone_verified
|
$21,
|
||||||
$22, -- status
|
$22,
|
||||||
$23, -- profile_completed
|
$23,
|
||||||
$24, -- profile_picture_url
|
$24,
|
||||||
$25, -- preferred_language
|
$25,
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
|
|
@ -108,7 +108,7 @@ RETURNING
|
||||||
email,
|
email,
|
||||||
phone_number,
|
phone_number,
|
||||||
role,
|
role,
|
||||||
age,
|
age_group,
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
|
@ -140,7 +140,7 @@ type CreateUserParams struct {
|
||||||
PhoneNumber pgtype.Text `json:"phone_number"`
|
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Password []byte `json:"password"`
|
Password []byte `json:"password"`
|
||||||
Age pgtype.Int4 `json:"age"`
|
AgeGroup pgtype.Text `json:"age_group"`
|
||||||
EducationLevel pgtype.Text `json:"education_level"`
|
EducationLevel pgtype.Text `json:"education_level"`
|
||||||
Country pgtype.Text `json:"country"`
|
Country pgtype.Text `json:"country"`
|
||||||
Region pgtype.Text `json:"region"`
|
Region pgtype.Text `json:"region"`
|
||||||
|
|
@ -168,7 +168,7 @@ type CreateUserRow struct {
|
||||||
Email pgtype.Text `json:"email"`
|
Email pgtype.Text `json:"email"`
|
||||||
PhoneNumber pgtype.Text `json:"phone_number"`
|
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Age pgtype.Int4 `json:"age"`
|
AgeGroup pgtype.Text `json:"age_group"`
|
||||||
EducationLevel pgtype.Text `json:"education_level"`
|
EducationLevel pgtype.Text `json:"education_level"`
|
||||||
Country pgtype.Text `json:"country"`
|
Country pgtype.Text `json:"country"`
|
||||||
Region pgtype.Text `json:"region"`
|
Region pgtype.Text `json:"region"`
|
||||||
|
|
@ -199,7 +199,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
|
||||||
arg.PhoneNumber,
|
arg.PhoneNumber,
|
||||||
arg.Role,
|
arg.Role,
|
||||||
arg.Password,
|
arg.Password,
|
||||||
arg.Age,
|
arg.AgeGroup,
|
||||||
arg.EducationLevel,
|
arg.EducationLevel,
|
||||||
arg.Country,
|
arg.Country,
|
||||||
arg.Region,
|
arg.Region,
|
||||||
|
|
@ -227,7 +227,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
|
||||||
&i.Email,
|
&i.Email,
|
||||||
&i.PhoneNumber,
|
&i.PhoneNumber,
|
||||||
&i.Role,
|
&i.Role,
|
||||||
&i.Age,
|
&i.AgeGroup,
|
||||||
&i.EducationLevel,
|
&i.EducationLevel,
|
||||||
&i.Country,
|
&i.Country,
|
||||||
&i.Region,
|
&i.Region,
|
||||||
|
|
@ -271,10 +271,11 @@ SELECT
|
||||||
email,
|
email,
|
||||||
phone_number,
|
phone_number,
|
||||||
role,
|
role,
|
||||||
age,
|
age_group,
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
knowledge_level,
|
||||||
nick_name,
|
nick_name,
|
||||||
occupation,
|
occupation,
|
||||||
learning_goal,
|
learning_goal,
|
||||||
|
|
@ -282,33 +283,23 @@ SELECT
|
||||||
language_challange,
|
language_challange,
|
||||||
favourite_topic,
|
favourite_topic,
|
||||||
initial_assessment_completed,
|
initial_assessment_completed,
|
||||||
profile_picture_url,
|
|
||||||
preferred_language,
|
|
||||||
email_verified,
|
email_verified,
|
||||||
phone_verified,
|
phone_verified,
|
||||||
status,
|
status,
|
||||||
|
last_login,
|
||||||
profile_completed,
|
profile_completed,
|
||||||
|
profile_picture_url,
|
||||||
|
preferred_language,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM users
|
FROM users
|
||||||
WHERE ($1 IS NULL OR role = $1)
|
LIMIT $2::INT
|
||||||
AND ($2 IS NULL OR first_name ILIKE '%' || $2 || '%'
|
OFFSET $1::INT
|
||||||
OR last_name ILIKE '%' || $2 || '%'
|
|
||||||
OR phone_number ILIKE '%' || $2 || '%'
|
|
||||||
OR email ILIKE '%' || $2 || '%')
|
|
||||||
AND ($3 IS NULL OR created_at >= $3)
|
|
||||||
AND ($4 IS NULL OR created_at <= $4)
|
|
||||||
LIMIT $5
|
|
||||||
OFFSET $6
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetAllUsersParams struct {
|
type GetAllUsersParams struct {
|
||||||
Column1 interface{} `json:"column_1"`
|
Offset pgtype.Int4 `json:"offset"`
|
||||||
Column2 interface{} `json:"column_2"`
|
Limit pgtype.Int4 `json:"limit"`
|
||||||
Column3 interface{} `json:"column_3"`
|
|
||||||
Column4 interface{} `json:"column_4"`
|
|
||||||
Limit int32 `json:"limit"`
|
|
||||||
Offset int32 `json:"offset"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetAllUsersRow struct {
|
type GetAllUsersRow struct {
|
||||||
|
|
@ -321,10 +312,11 @@ type GetAllUsersRow struct {
|
||||||
Email pgtype.Text `json:"email"`
|
Email pgtype.Text `json:"email"`
|
||||||
PhoneNumber pgtype.Text `json:"phone_number"`
|
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Age pgtype.Int4 `json:"age"`
|
AgeGroup pgtype.Text `json:"age_group"`
|
||||||
EducationLevel pgtype.Text `json:"education_level"`
|
EducationLevel pgtype.Text `json:"education_level"`
|
||||||
Country pgtype.Text `json:"country"`
|
Country pgtype.Text `json:"country"`
|
||||||
Region pgtype.Text `json:"region"`
|
Region pgtype.Text `json:"region"`
|
||||||
|
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
|
||||||
NickName pgtype.Text `json:"nick_name"`
|
NickName pgtype.Text `json:"nick_name"`
|
||||||
Occupation pgtype.Text `json:"occupation"`
|
Occupation pgtype.Text `json:"occupation"`
|
||||||
LearningGoal pgtype.Text `json:"learning_goal"`
|
LearningGoal pgtype.Text `json:"learning_goal"`
|
||||||
|
|
@ -332,25 +324,19 @@ type GetAllUsersRow struct {
|
||||||
LanguageChallange pgtype.Text `json:"language_challange"`
|
LanguageChallange pgtype.Text `json:"language_challange"`
|
||||||
FavouriteTopic pgtype.Text `json:"favourite_topic"`
|
FavouriteTopic pgtype.Text `json:"favourite_topic"`
|
||||||
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
|
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
|
||||||
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
|
|
||||||
PreferredLanguage pgtype.Text `json:"preferred_language"`
|
|
||||||
EmailVerified bool `json:"email_verified"`
|
EmailVerified bool `json:"email_verified"`
|
||||||
PhoneVerified bool `json:"phone_verified"`
|
PhoneVerified bool `json:"phone_verified"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
LastLogin pgtype.Timestamptz `json:"last_login"`
|
||||||
ProfileCompleted pgtype.Bool `json:"profile_completed"`
|
ProfileCompleted pgtype.Bool `json:"profile_completed"`
|
||||||
|
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
|
||||||
|
PreferredLanguage pgtype.Text `json:"preferred_language"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]GetAllUsersRow, error) {
|
func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]GetAllUsersRow, error) {
|
||||||
rows, err := q.db.Query(ctx, GetAllUsers,
|
rows, err := q.db.Query(ctx, GetAllUsers, arg.Offset, arg.Limit)
|
||||||
arg.Column1,
|
|
||||||
arg.Column2,
|
|
||||||
arg.Column3,
|
|
||||||
arg.Column4,
|
|
||||||
arg.Limit,
|
|
||||||
arg.Offset,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -368,10 +354,11 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
|
||||||
&i.Email,
|
&i.Email,
|
||||||
&i.PhoneNumber,
|
&i.PhoneNumber,
|
||||||
&i.Role,
|
&i.Role,
|
||||||
&i.Age,
|
&i.AgeGroup,
|
||||||
&i.EducationLevel,
|
&i.EducationLevel,
|
||||||
&i.Country,
|
&i.Country,
|
||||||
&i.Region,
|
&i.Region,
|
||||||
|
&i.KnowledgeLevel,
|
||||||
&i.NickName,
|
&i.NickName,
|
||||||
&i.Occupation,
|
&i.Occupation,
|
||||||
&i.LearningGoal,
|
&i.LearningGoal,
|
||||||
|
|
@ -379,12 +366,13 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
|
||||||
&i.LanguageChallange,
|
&i.LanguageChallange,
|
||||||
&i.FavouriteTopic,
|
&i.FavouriteTopic,
|
||||||
&i.InitialAssessmentCompleted,
|
&i.InitialAssessmentCompleted,
|
||||||
&i.ProfilePictureUrl,
|
|
||||||
&i.PreferredLanguage,
|
|
||||||
&i.EmailVerified,
|
&i.EmailVerified,
|
||||||
&i.PhoneVerified,
|
&i.PhoneVerified,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
|
&i.LastLogin,
|
||||||
&i.ProfileCompleted,
|
&i.ProfileCompleted,
|
||||||
|
&i.ProfilePictureUrl,
|
||||||
|
&i.PreferredLanguage,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
@ -425,7 +413,7 @@ SELECT
|
||||||
phone_number,
|
phone_number,
|
||||||
role,
|
role,
|
||||||
password,
|
password,
|
||||||
age,
|
age_group,
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
|
@ -467,7 +455,7 @@ type GetUserByEmailPhoneRow struct {
|
||||||
PhoneNumber pgtype.Text `json:"phone_number"`
|
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Password []byte `json:"password"`
|
Password []byte `json:"password"`
|
||||||
Age pgtype.Int4 `json:"age"`
|
AgeGroup pgtype.Text `json:"age_group"`
|
||||||
EducationLevel pgtype.Text `json:"education_level"`
|
EducationLevel pgtype.Text `json:"education_level"`
|
||||||
Country pgtype.Text `json:"country"`
|
Country pgtype.Text `json:"country"`
|
||||||
Region pgtype.Text `json:"region"`
|
Region pgtype.Text `json:"region"`
|
||||||
|
|
@ -534,7 +522,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
||||||
&i.PhoneNumber,
|
&i.PhoneNumber,
|
||||||
&i.Role,
|
&i.Role,
|
||||||
&i.Password,
|
&i.Password,
|
||||||
&i.Age,
|
&i.AgeGroup,
|
||||||
&i.EducationLevel,
|
&i.EducationLevel,
|
||||||
&i.Country,
|
&i.Country,
|
||||||
&i.Region,
|
&i.Region,
|
||||||
|
|
@ -558,7 +546,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetUserByID = `-- name: GetUserByID :one
|
const GetUserByID = `-- name: GetUserByID :one
|
||||||
SELECT id, first_name, last_name, gender, birth_day, email, phone_number, role, password, age, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at
|
SELECT id, first_name, last_name, gender, birth_day, email, phone_number, role, password, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at, age_group
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -576,7 +564,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||||
&i.PhoneNumber,
|
&i.PhoneNumber,
|
||||||
&i.Role,
|
&i.Role,
|
||||||
&i.Password,
|
&i.Password,
|
||||||
&i.Age,
|
|
||||||
&i.EducationLevel,
|
&i.EducationLevel,
|
||||||
&i.Country,
|
&i.Country,
|
||||||
&i.Region,
|
&i.Region,
|
||||||
|
|
@ -597,6 +584,7 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||||
&i.PreferredLanguage,
|
&i.PreferredLanguage,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.AgeGroup,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -655,7 +643,7 @@ SELECT
|
||||||
email,
|
email,
|
||||||
phone_number,
|
phone_number,
|
||||||
role,
|
role,
|
||||||
age,
|
age_group,
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
|
@ -703,7 +691,7 @@ type SearchUserByNameOrPhoneRow struct {
|
||||||
Email pgtype.Text `json:"email"`
|
Email pgtype.Text `json:"email"`
|
||||||
PhoneNumber pgtype.Text `json:"phone_number"`
|
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Age pgtype.Int4 `json:"age"`
|
AgeGroup pgtype.Text `json:"age_group"`
|
||||||
EducationLevel pgtype.Text `json:"education_level"`
|
EducationLevel pgtype.Text `json:"education_level"`
|
||||||
Country pgtype.Text `json:"country"`
|
Country pgtype.Text `json:"country"`
|
||||||
Region pgtype.Text `json:"region"`
|
Region pgtype.Text `json:"region"`
|
||||||
|
|
@ -742,7 +730,7 @@ func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByN
|
||||||
&i.Email,
|
&i.Email,
|
||||||
&i.PhoneNumber,
|
&i.PhoneNumber,
|
||||||
&i.Role,
|
&i.Role,
|
||||||
&i.Age,
|
&i.AgeGroup,
|
||||||
&i.EducationLevel,
|
&i.EducationLevel,
|
||||||
&i.Country,
|
&i.Country,
|
||||||
&i.Region,
|
&i.Region,
|
||||||
|
|
@ -795,12 +783,8 @@ UPDATE users
|
||||||
SET
|
SET
|
||||||
first_name = COALESCE($1, first_name),
|
first_name = COALESCE($1, first_name),
|
||||||
last_name = COALESCE($2, last_name),
|
last_name = COALESCE($2, last_name),
|
||||||
|
|
||||||
-- email = COALESCE($3, email),
|
|
||||||
-- phone_number = COALESCE($4, phone_number),
|
|
||||||
|
|
||||||
knowledge_level = COALESCE($3, knowledge_level),
|
knowledge_level = COALESCE($3, knowledge_level),
|
||||||
age = COALESCE($4, age),
|
age_group = COALESCE($4, age_group),
|
||||||
education_level = COALESCE($5, education_level),
|
education_level = COALESCE($5, education_level),
|
||||||
country = COALESCE($6, country),
|
country = COALESCE($6, country),
|
||||||
region = COALESCE($7, region),
|
region = COALESCE($7, region),
|
||||||
|
|
@ -811,14 +795,11 @@ SET
|
||||||
language_challange = COALESCE($12, language_challange),
|
language_challange = COALESCE($12, language_challange),
|
||||||
favourite_topic = COALESCE($13, favourite_topic),
|
favourite_topic = COALESCE($13, favourite_topic),
|
||||||
initial_assessment_completed = COALESCE($14, initial_assessment_completed),
|
initial_assessment_completed = COALESCE($14, initial_assessment_completed),
|
||||||
-- email_verified = COALESCE($15, email_verified),
|
|
||||||
-- phone_verified = COALESCE($16, phone_verified),
|
|
||||||
-- status = COALESCE($19, status),
|
|
||||||
profile_completed = COALESCE($15, profile_completed),
|
profile_completed = COALESCE($15, profile_completed),
|
||||||
profile_picture_url = COALESCE($16, profile_picture_url),
|
profile_picture_url = COALESCE($16, profile_picture_url),
|
||||||
preferred_language = COALESCE($17, preferred_language),
|
preferred_language = COALESCE($17, preferred_language),
|
||||||
gender = COALESCE($18, gender),
|
gender = COALESCE($18, gender),
|
||||||
birth_day = COALESCE($19, gender),
|
birth_day = COALESCE($19, birth_day),
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $20
|
WHERE id = $20
|
||||||
`
|
`
|
||||||
|
|
@ -827,7 +808,7 @@ type UpdateUserParams struct {
|
||||||
FirstName pgtype.Text `json:"first_name"`
|
FirstName pgtype.Text `json:"first_name"`
|
||||||
LastName pgtype.Text `json:"last_name"`
|
LastName pgtype.Text `json:"last_name"`
|
||||||
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
|
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
|
||||||
Age pgtype.Int4 `json:"age"`
|
AgeGroup pgtype.Text `json:"age_group"`
|
||||||
EducationLevel pgtype.Text `json:"education_level"`
|
EducationLevel pgtype.Text `json:"education_level"`
|
||||||
Country pgtype.Text `json:"country"`
|
Country pgtype.Text `json:"country"`
|
||||||
Region pgtype.Text `json:"region"`
|
Region pgtype.Text `json:"region"`
|
||||||
|
|
@ -851,7 +832,7 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
||||||
arg.FirstName,
|
arg.FirstName,
|
||||||
arg.LastName,
|
arg.LastName,
|
||||||
arg.KnowledgeLevel,
|
arg.KnowledgeLevel,
|
||||||
arg.Age,
|
arg.AgeGroup,
|
||||||
arg.EducationLevel,
|
arg.EducationLevel,
|
||||||
arg.Country,
|
arg.Country,
|
||||||
arg.Region,
|
arg.Region,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,21 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginSuccess struct {
|
||||||
|
UserId int64
|
||||||
|
Role Role
|
||||||
|
RfToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
PhoneNumber string `json:"phone_number"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
OTPCode string `json:"otp_code"`
|
||||||
|
}
|
||||||
|
|
||||||
type RefreshToken struct {
|
type RefreshToken struct {
|
||||||
ID int64
|
ID int64
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,18 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AgeGroup string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AgeUnder13 AgeGroup = "UNDER_13"
|
||||||
|
Age13To17 AgeGroup = "13_17"
|
||||||
|
Age18To24 AgeGroup = "18_24"
|
||||||
|
Age25To34 AgeGroup = "25_34"
|
||||||
|
Age35To44 AgeGroup = "35_44"
|
||||||
|
Age45To54 AgeGroup = "45_54"
|
||||||
|
Age55Plus AgeGroup = "55_PLUS"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrUserNotVerified = errors.New("user not verified")
|
ErrUserNotVerified = errors.New("user not verified")
|
||||||
ErrUserNotFound = errors.New("user not found")
|
ErrUserNotFound = errors.New("user not found")
|
||||||
|
|
@ -42,7 +54,7 @@ type User struct {
|
||||||
Password []byte
|
Password []byte
|
||||||
Role Role
|
Role Role
|
||||||
|
|
||||||
Age int
|
AgeGroup string
|
||||||
EducationLevel string
|
EducationLevel string
|
||||||
Country string
|
Country string
|
||||||
Region string
|
Region string
|
||||||
|
|
@ -81,7 +93,7 @@ type UserProfileResponse struct {
|
||||||
PhoneNumber string `json:"phone_number,omitempty"`
|
PhoneNumber string `json:"phone_number,omitempty"`
|
||||||
Role Role `json:"role"`
|
Role Role `json:"role"`
|
||||||
|
|
||||||
Age int `json:"age,omitempty"`
|
AgeGroup string `json:"age_group,omitempty"`
|
||||||
EducationLevel string `json:"education_level,omitempty"`
|
EducationLevel string `json:"education_level,omitempty"`
|
||||||
Country string `json:"country,omitempty"`
|
Country string `json:"country,omitempty"`
|
||||||
Region string `json:"region,omitempty"`
|
Region string `json:"region,omitempty"`
|
||||||
|
|
@ -138,7 +150,7 @@ type CreateUserReq struct {
|
||||||
|
|
||||||
Status UserStatus
|
Status UserStatus
|
||||||
|
|
||||||
Age int
|
AgeGroup string
|
||||||
EducationLevel string
|
EducationLevel string
|
||||||
Country string
|
Country string
|
||||||
Region string
|
Region string
|
||||||
|
|
@ -166,27 +178,20 @@ type UpdateUserStatusReq struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserReq struct {
|
type UpdateUserReq struct {
|
||||||
// Identity (enforced from auth context, not request body)
|
|
||||||
UserID int64 `json:"-"`
|
UserID int64 `json:"-"`
|
||||||
|
|
||||||
// Basic profile
|
|
||||||
FirstName string `json:"first_name"`
|
FirstName string `json:"first_name"`
|
||||||
LastName string `json:"last_name"`
|
LastName string `json:"last_name"`
|
||||||
|
|
||||||
Gender string `json:"gender"`
|
Gender string `json:"gender"`
|
||||||
BirthDay time.Time `json:"birth_day"`
|
BirthDay *string `json:"birth_day"` // YYYY-MM-DD
|
||||||
|
|
||||||
// Contact (optional – at least one must exist at DB level)
|
AgeGroup *AgeGroup `json:"age_group"`
|
||||||
// Email string `json:"email"`
|
|
||||||
// PhoneNumber string `json:"phone_number"`
|
|
||||||
|
|
||||||
// Personal details
|
|
||||||
Age int64 `json:"age"`
|
|
||||||
EducationLevel string `json:"education_level"`
|
EducationLevel string `json:"education_level"`
|
||||||
Country string `json:"country"`
|
Country string `json:"country"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
|
|
||||||
// Learning / profile
|
|
||||||
KnowledgeLevel string `json:"knowledge_level"`
|
KnowledgeLevel string `json:"knowledge_level"`
|
||||||
NickName string `json:"nick_name"`
|
NickName string `json:"nick_name"`
|
||||||
Occupation string `json:"occupation"`
|
Occupation string `json:"occupation"`
|
||||||
|
|
@ -196,11 +201,8 @@ type UpdateUserReq struct {
|
||||||
FavouriteTopic string `json:"favourite_topic"`
|
FavouriteTopic string `json:"favourite_topic"`
|
||||||
|
|
||||||
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
|
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
|
||||||
// EmailVerified bool `json:"email_verified"`
|
|
||||||
// PhoneVerified bool `json:"phone_verified"`
|
|
||||||
ProfileCompleted bool `json:"profile_completed"`
|
ProfileCompleted bool `json:"profile_completed"`
|
||||||
|
|
||||||
// Media & preferences
|
|
||||||
ProfilePictureURL string `json:"profile_picture_url"`
|
ProfilePictureURL string `json:"profile_picture_url"`
|
||||||
PreferredLanguage string `json:"preferred_language"`
|
PreferredLanguage string `json:"preferred_language"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ func (s *Store) GetUserByEmailOrPhone(
|
||||||
Password: u.Password,
|
Password: u.Password,
|
||||||
Role: domain.Role(u.Role),
|
Role: domain.Role(u.Role),
|
||||||
|
|
||||||
Age: int(u.Age.Int32),
|
AgeGroup: u.AgeGroup.String,
|
||||||
EducationLevel: u.EducationLevel.String,
|
EducationLevel: u.EducationLevel.String,
|
||||||
Country: u.Country.String,
|
Country: u.Country.String,
|
||||||
Region: u.Region.String,
|
Region: u.Region.String,
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ func (s *Store) CreateUserWithoutOtp(
|
||||||
Role: string(user.Role),
|
Role: string(user.Role),
|
||||||
Password: user.Password,
|
Password: user.Password,
|
||||||
|
|
||||||
Age: pgtype.Int4{Int32: int32(user.Age), Valid: user.Age > 0},
|
AgeGroup: pgtype.Text{String: user.AgeGroup, Valid: user.AgeGroup != ""},
|
||||||
EducationLevel: pgtype.Text{String: user.EducationLevel, Valid: user.EducationLevel != ""},
|
EducationLevel: pgtype.Text{String: user.EducationLevel, Valid: user.EducationLevel != ""},
|
||||||
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
|
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
|
||||||
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""},
|
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""},
|
||||||
|
|
@ -178,7 +178,7 @@ func (s *Store) CreateUser(
|
||||||
Role: string(user.Role),
|
Role: string(user.Role),
|
||||||
Password: user.Password,
|
Password: user.Password,
|
||||||
|
|
||||||
Age: pgtype.Int4{Int32: int32(user.Age), Valid: user.Age > 0},
|
AgeGroup: pgtype.Text{String: user.AgeGroup, Valid: user.AgeGroup != ""},
|
||||||
EducationLevel: pgtype.Text{String: user.EducationLevel, Valid: user.EducationLevel != ""},
|
EducationLevel: pgtype.Text{String: user.EducationLevel, Valid: user.EducationLevel != ""},
|
||||||
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
|
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
|
||||||
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""},
|
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""},
|
||||||
|
|
@ -254,7 +254,7 @@ func (s *Store) GetUserByID(
|
||||||
PhoneNumber: u.PhoneNumber.String,
|
PhoneNumber: u.PhoneNumber.String,
|
||||||
Role: domain.Role(u.Role),
|
Role: domain.Role(u.Role),
|
||||||
|
|
||||||
Age: int(u.Age.Int32),
|
AgeGroup: u.AgeGroup.String,
|
||||||
EducationLevel: u.EducationLevel.String,
|
EducationLevel: u.EducationLevel.String,
|
||||||
Country: u.Country.String,
|
Country: u.Country.String,
|
||||||
Region: u.Region.String,
|
Region: u.Region.String,
|
||||||
|
|
@ -289,41 +289,56 @@ func (s *Store) GetAllUsers(
|
||||||
limit, offset int32,
|
limit, offset int32,
|
||||||
) ([]domain.User, int64, error) {
|
) ([]domain.User, int64, error) {
|
||||||
|
|
||||||
var roleParam sql.NullString
|
// var roleParam sql.NullString
|
||||||
if role != nil && *role != "" {
|
// if role != nil && *role != "" {
|
||||||
roleParam = sql.NullString{String: *role, Valid: true}
|
// roleParam = sql.NullString{String: *role, Valid: true}
|
||||||
} else {
|
// } else {
|
||||||
roleParam = sql.NullString{Valid: false} // This will make $1 IS NULL work
|
// roleParam = sql.NullString{Valid: false} // This will make $1 IS NULL work
|
||||||
}
|
// }
|
||||||
|
|
||||||
var queryParam sql.NullString
|
// var queryParam sql.NullString
|
||||||
if query != nil && *query != "" {
|
// if query != nil && *query != "" {
|
||||||
queryParam = sql.NullString{String: *query, Valid: true}
|
// queryParam = sql.NullString{String: *query, Valid: true}
|
||||||
} else {
|
// } else {
|
||||||
queryParam = sql.NullString{Valid: false}
|
// queryParam = sql.NullString{Valid: false}
|
||||||
}
|
// }
|
||||||
|
|
||||||
var createdAfterParam sql.NullTime
|
// var createdAfterParam sql.NullTime
|
||||||
if createdAfter != nil {
|
// if createdAfter != nil {
|
||||||
createdAfterParam = sql.NullTime{Time: *createdAfter, Valid: true}
|
// createdAfterParam = sql.NullTime{Time: *createdAfter, Valid: true}
|
||||||
} else {
|
// } else {
|
||||||
createdAfterParam = sql.NullTime{Valid: false}
|
// createdAfterParam = sql.NullTime{Valid: false}
|
||||||
}
|
// }
|
||||||
|
|
||||||
var createdBeforeParam sql.NullTime
|
// var createdBeforeParam sql.NullTime
|
||||||
if createdBefore != nil {
|
// if createdBefore != nil {
|
||||||
createdBeforeParam = sql.NullTime{Time: *createdBefore, Valid: true}
|
// createdBeforeParam = sql.NullTime{Time: *createdBefore, Valid: true}
|
||||||
} else {
|
// } else {
|
||||||
createdBeforeParam = sql.NullTime{Valid: false}
|
// createdBeforeParam = sql.NullTime{Valid: false}
|
||||||
}
|
// }
|
||||||
|
|
||||||
params := dbgen.GetAllUsersParams{
|
params := dbgen.GetAllUsersParams{
|
||||||
Column1: roleParam.String,
|
// Role: pgtype.Text{
|
||||||
Column2: pgtype.Text{String: queryParam.String, Valid: queryParam.Valid},
|
// String: roleParam.String,
|
||||||
Column3: pgtype.Timestamptz{Time: createdAfterParam.Time, Valid: createdAfterParam.Valid},
|
// Valid: roleParam.String != "",
|
||||||
Column4: pgtype.Timestamptz{Time: createdBeforeParam.Time, Valid: createdBeforeParam.Valid},
|
// },
|
||||||
Limit: int32(limit),
|
// Query: queryParam.String,
|
||||||
Offset: int32(offset),
|
// CreatedAfter: pgtype.Timestamptz{
|
||||||
|
// Time: createdAfterParam.Time,
|
||||||
|
// Valid: createdAfterParam.Valid,
|
||||||
|
// },
|
||||||
|
// CreatedBefore: pgtype.Timestamptz{
|
||||||
|
// Time: createdBeforeParam.Time,
|
||||||
|
// Valid: createdBeforeParam.Valid,
|
||||||
|
// },
|
||||||
|
Limit: pgtype.Int4{
|
||||||
|
Int32: limit,
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
Offset: pgtype.Int4{
|
||||||
|
Int32: offset,
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := s.queries.GetAllUsers(ctx, params)
|
rows, err := s.queries.GetAllUsers(ctx, params)
|
||||||
|
|
@ -356,7 +371,7 @@ func (s *Store) GetAllUsers(
|
||||||
PhoneNumber: u.PhoneNumber.String,
|
PhoneNumber: u.PhoneNumber.String,
|
||||||
Role: domain.Role(u.Role),
|
Role: domain.Role(u.Role),
|
||||||
|
|
||||||
Age: int(u.Age.Int32),
|
AgeGroup: u.AgeGroup.String,
|
||||||
EducationLevel: u.EducationLevel.String,
|
EducationLevel: u.EducationLevel.String,
|
||||||
Country: u.Country.String,
|
Country: u.Country.String,
|
||||||
Region: u.Region.String,
|
Region: u.Region.String,
|
||||||
|
|
@ -443,7 +458,7 @@ func (s *Store) SearchUserByNameOrPhone(
|
||||||
PhoneNumber: u.PhoneNumber.String,
|
PhoneNumber: u.PhoneNumber.String,
|
||||||
Role: domain.Role(u.Role),
|
Role: domain.Role(u.Role),
|
||||||
|
|
||||||
Age: int(u.Age.Int32),
|
AgeGroup: u.AgeGroup.String,
|
||||||
EducationLevel: u.EducationLevel.String,
|
EducationLevel: u.EducationLevel.String,
|
||||||
Country: u.Country.String,
|
Country: u.Country.String,
|
||||||
Region: u.Region.String,
|
Region: u.Region.String,
|
||||||
|
|
@ -477,19 +492,27 @@ func (s *Store) UpdateUser(
|
||||||
req domain.UpdateUserReq,
|
req domain.UpdateUserReq,
|
||||||
) error {
|
) error {
|
||||||
|
|
||||||
|
var birthDate pgtype.Date
|
||||||
|
if req.BirthDay != nil && *req.BirthDay != "" {
|
||||||
|
t, err := time.Parse("2006-01-02", *req.BirthDay)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
birthDate = pgtype.Date{Time: t, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ageGroup pgtype.Text
|
||||||
|
if req.AgeGroup != nil {
|
||||||
|
ageGroup = pgtype.Text{String: string(*req.AgeGroup), Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
return s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
|
return s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
|
||||||
FirstName: pgtype.Text{String: req.FirstName, Valid: req.FirstName != ""},
|
FirstName: pgtype.Text{String: req.FirstName, Valid: req.FirstName != ""},
|
||||||
LastName: pgtype.Text{String: req.LastName, Valid: req.LastName != ""},
|
LastName: pgtype.Text{String: req.LastName, Valid: req.LastName != ""},
|
||||||
Gender: pgtype.Text{
|
|
||||||
String: req.Gender,
|
|
||||||
Valid: req.Gender != "",
|
|
||||||
},
|
|
||||||
BirthDay: pgtype.Date{
|
|
||||||
Time: req.BirthDay,
|
|
||||||
Valid: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
Age: pgtype.Int4{Int32: int32(req.Age), Valid: req.Age > 0},
|
KnowledgeLevel: pgtype.Text{String: req.KnowledgeLevel, Valid: req.KnowledgeLevel != ""},
|
||||||
|
AgeGroup: ageGroup,
|
||||||
|
|
||||||
EducationLevel: pgtype.Text{String: req.EducationLevel, Valid: req.EducationLevel != ""},
|
EducationLevel: pgtype.Text{String: req.EducationLevel, Valid: req.EducationLevel != ""},
|
||||||
Country: pgtype.Text{String: req.Country, Valid: req.Country != ""},
|
Country: pgtype.Text{String: req.Country, Valid: req.Country != ""},
|
||||||
Region: pgtype.Text{String: req.Region, Valid: req.Region != ""},
|
Region: pgtype.Text{String: req.Region, Valid: req.Region != ""},
|
||||||
|
|
@ -501,17 +524,17 @@ func (s *Store) UpdateUser(
|
||||||
LanguageChallange: pgtype.Text{String: req.LanguageChallange, Valid: req.LanguageChallange != ""},
|
LanguageChallange: pgtype.Text{String: req.LanguageChallange, Valid: req.LanguageChallange != ""},
|
||||||
FavouriteTopic: pgtype.Text{String: req.FavouriteTopic, Valid: req.FavouriteTopic != ""},
|
FavouriteTopic: pgtype.Text{String: req.FavouriteTopic, Valid: req.FavouriteTopic != ""},
|
||||||
|
|
||||||
ProfileCompleted: pgtype.Bool{
|
InitialAssessmentCompleted: req.InitialAssessmentCompleted,
|
||||||
Bool: req.ProfileCompleted,
|
ProfileCompleted: pgtype.Bool{Bool: req.ProfileCompleted, Valid: true},
|
||||||
Valid: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
ProfilePictureUrl: pgtype.Text{String: req.ProfilePictureURL, Valid: req.ProfilePictureURL != ""},
|
ProfilePictureUrl: pgtype.Text{String: req.ProfilePictureURL, Valid: req.ProfilePictureURL != ""},
|
||||||
PreferredLanguage: pgtype.Text{String: req.PreferredLanguage, Valid: req.PreferredLanguage != ""},
|
PreferredLanguage: pgtype.Text{String: req.PreferredLanguage, Valid: req.PreferredLanguage != ""},
|
||||||
|
|
||||||
|
Gender: pgtype.Text{String: req.Gender, Valid: req.Gender != ""},
|
||||||
|
BirthDay: birthDate,
|
||||||
|
|
||||||
ID: req.UserID,
|
ID: req.UserID,
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser removes a user
|
// DeleteUser removes a user
|
||||||
|
|
@ -637,7 +660,7 @@ func (s *Store) GetUserByEmailPhone(
|
||||||
Password: u.Password,
|
Password: u.Password,
|
||||||
Role: domain.Role(u.Role),
|
Role: domain.Role(u.Role),
|
||||||
|
|
||||||
Age: int(u.Age.Int32),
|
AgeGroup: u.AgeGroup.String,
|
||||||
EducationLevel: u.EducationLevel.String,
|
EducationLevel: u.EducationLevel.String,
|
||||||
Country: u.Country.String,
|
Country: u.Country.String,
|
||||||
Region: u.Region.String,
|
Region: u.Region.String,
|
||||||
|
|
@ -690,7 +713,7 @@ func mapCreateUserResult(
|
||||||
Role: domain.Role(userRes.Role),
|
Role: domain.Role(userRes.Role),
|
||||||
Password: password,
|
Password: password,
|
||||||
|
|
||||||
Age: int(userRes.Age.Int32),
|
AgeGroup: userRes.AgeGroup.String,
|
||||||
EducationLevel: userRes.EducationLevel.String,
|
EducationLevel: userRes.EducationLevel.String,
|
||||||
Country: userRes.Country.String,
|
Country: userRes.Country.String,
|
||||||
Region: userRes.Region.String,
|
Region: userRes.Region.String,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ func NewArifpayService(cfg *config.Config, transactionSvc transaction.Service, h
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool, userId int64) (map[string]any, error) {
|
func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool, userId int64) (map[string]any, error) {
|
||||||
// Generate unique nonce
|
// Generate unique nonce
|
||||||
nonce := uuid.NewString()
|
nonce := uuid.NewString()
|
||||||
|
|
@ -42,6 +43,7 @@ func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientR
|
||||||
NotifyURL = s.cfg.ARIFPAY.B2CNotifyUrl
|
NotifyURL = s.cfg.ARIFPAY.B2CNotifyUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Construct full checkout request
|
// Construct full checkout request
|
||||||
checkoutReq := domain.CheckoutSessionRequest{
|
checkoutReq := domain.CheckoutSessionRequest{
|
||||||
CancelURL: s.cfg.ARIFPAY.CancelUrl,
|
CancelURL: s.cfg.ARIFPAY.CancelUrl,
|
||||||
|
|
|
||||||
|
|
@ -20,67 +20,44 @@ var (
|
||||||
ErrUserSuspended = errors.New("user has been suspended")
|
ErrUserSuspended = errors.New("user has been suspended")
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginSuccess struct {
|
|
||||||
UserId int64
|
|
||||||
Role domain.Role
|
|
||||||
RfToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoginRequest struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
PhoneNumber string `json:"phone_number"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
OTPCode string `json:"otp_code"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) Login(
|
func (s *Service) Login(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req LoginRequest,
|
req domain.LoginRequest,
|
||||||
) (LoginSuccess, error) {
|
) (domain.LoginSuccess, error) {
|
||||||
|
|
||||||
// Try to find user by username first
|
|
||||||
user, err := s.userStore.GetUserByEmailPhone(ctx, req.Email, req.PhoneNumber)
|
user, err := s.userStore.GetUserByEmailPhone(ctx, req.Email, req.PhoneNumber)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If not found by username, try email or phone lookup using the same identifier
|
return domain.LoginSuccess{}, err
|
||||||
return LoginSuccess{}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Status == domain.UserStatusPending {
|
if user.Status == domain.UserStatusPending {
|
||||||
return LoginSuccess{}, domain.ErrUserNotVerified
|
return domain.LoginSuccess{}, domain.ErrUserNotVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status check instead of Suspended
|
|
||||||
if user.Status == domain.UserStatusSuspended {
|
if user.Status == domain.UserStatusSuspended {
|
||||||
return LoginSuccess{}, ErrUserSuspended
|
return domain.LoginSuccess{}, ErrUserSuspended
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Email + password login
|
||||||
if req.Email != "" {
|
if req.Email != "" {
|
||||||
if err := matchPassword(req.Password, user.Password); err != nil {
|
if err := matchPassword(req.Password, user.Password); err != nil {
|
||||||
return LoginSuccess{}, err
|
return domain.LoginSuccess{}, err
|
||||||
}
|
|
||||||
} else if req.PhoneNumber != "" {
|
|
||||||
if err := s.UserSvc.VerifyOtp(ctx, req.Email, req.PhoneNumber, req.OTPCode); err != nil {
|
|
||||||
return LoginSuccess{}, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle existing refresh token
|
|
||||||
oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
|
oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
|
||||||
if err != nil && !errors.Is(err, ErrRefreshTokenNotFound) {
|
if err != nil && !errors.Is(err, ErrRefreshTokenNotFound) {
|
||||||
return LoginSuccess{}, err
|
return domain.LoginSuccess{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revoke if exists and not revoked
|
|
||||||
if err == nil && !oldRefreshToken.Revoked {
|
if err == nil && !oldRefreshToken.Revoked {
|
||||||
if err := s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token); err != nil {
|
if err := s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token); err != nil {
|
||||||
return LoginSuccess{}, err
|
return domain.LoginSuccess{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new refresh token
|
|
||||||
refreshToken, err := generateRefreshToken()
|
refreshToken, err := generateRefreshToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return LoginSuccess{}, err
|
return domain.LoginSuccess{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
|
if err := s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
|
||||||
|
|
@ -89,17 +66,114 @@ func (s *Service) Login(
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
|
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return LoginSuccess{}, err
|
return domain.LoginSuccess{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return login success payload
|
return domain.LoginSuccess{
|
||||||
return LoginSuccess{
|
|
||||||
UserId: user.ID,
|
UserId: user.ID,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
RfToken: refreshToken,
|
RfToken: refreshToken,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phone + OTP login
|
||||||
|
if req.PhoneNumber != "" {
|
||||||
|
return s.VerifyOtp(ctx, req.Email, req.PhoneNumber, req.OTPCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❗ Mandatory fallback return
|
||||||
|
return domain.LoginSuccess{}, ErrInvalidPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) VerifyOtp(
|
||||||
|
ctx context.Context,
|
||||||
|
email, phone, otpCode string,
|
||||||
|
) (domain.LoginSuccess, error) {
|
||||||
|
|
||||||
|
user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone)
|
||||||
|
if err != nil {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Retrieve OTP
|
||||||
|
storedOtp, err := s.otpStore.GetOtp(ctx, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Already used
|
||||||
|
if storedOtp.Used {
|
||||||
|
return domain.LoginSuccess{}, domain.ErrOtpAlreadyUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Expired
|
||||||
|
if time.Now().After(storedOtp.ExpiresAt) {
|
||||||
|
return domain.LoginSuccess{}, domain.ErrOtpExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Invalid
|
||||||
|
if storedOtp.Otp != otpCode {
|
||||||
|
return domain.LoginSuccess{}, 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 domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Activate user if still pending
|
||||||
|
if user.Status == domain.UserStatusPending {
|
||||||
|
if err := s.userStore.UpdateUserStatus(ctx, domain.UpdateUserStatusReq{
|
||||||
|
UserID: user.ID,
|
||||||
|
Status: string(domain.UserStatusActive),
|
||||||
|
}); err != nil {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Handle existing refresh token
|
||||||
|
oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
|
||||||
|
if err != nil && !errors.Is(err, ErrRefreshTokenNotFound) {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && !oldRefreshToken.Revoked {
|
||||||
|
if err := s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token); err != nil {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Generate new refresh token
|
||||||
|
refreshToken, err := generateRefreshToken()
|
||||||
|
if err != nil {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}); err != nil {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Return success payload
|
||||||
|
return domain.LoginSuccess{
|
||||||
|
UserId: user.ID,
|
||||||
|
Role: user.Role,
|
||||||
|
RfToken: refreshToken,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to get a pointer to time.Time
|
||||||
|
func timePtr(t time.Time) time.Time {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) RefreshToken(ctx context.Context, refToken string) (domain.RefreshToken, error) {
|
func (s *Service) RefreshToken(ctx context.Context, refToken string) (domain.RefreshToken, error) {
|
||||||
|
|
||||||
token, err := s.tokenStore.GetRefreshToken(ctx, refToken)
|
token, err := s.tokenStore.GetRefreshToken(ctx, refToken)
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,16 @@ type Tokens struct {
|
||||||
RefreshToken string
|
RefreshToken string
|
||||||
}
|
}
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
otpStore ports.OtpStore
|
||||||
userStore ports.UserStore
|
userStore ports.UserStore
|
||||||
UserSvc user.Service
|
UserSvc user.Service
|
||||||
tokenStore ports.TokenStore
|
tokenStore ports.TokenStore
|
||||||
RefreshExpiry int
|
RefreshExpiry int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(userStore ports.UserStore, userSvc user.Service, tokenStore ports.TokenStore, RefreshExpiry int) *Service {
|
func NewService(otpStore ports.OtpStore, userStore ports.UserStore, userSvc user.Service, tokenStore ports.TokenStore, RefreshExpiry int) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
|
otpStore: otpStore,
|
||||||
userStore: userStore,
|
userStore: userStore,
|
||||||
UserSvc: userSvc,
|
UserSvc: userSvc,
|
||||||
tokenStore: tokenStore,
|
tokenStore: tokenStore,
|
||||||
|
|
|
||||||
|
|
@ -10,56 +10,6 @@ import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) VerifyOtp(ctx context.Context, email, phone, otpCode string) error {
|
|
||||||
|
|
||||||
user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// 1. Retrieve the OTP from the store
|
|
||||||
storedOtp, err := s.otpStore.GetOtp(ctx, user.ID)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// user, err := s.userStore.GetUserByUserName(ctx, userName)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
newUser := domain.UpdateUserStatusReq{
|
|
||||||
UserID: user.ID,
|
|
||||||
Status: string(domain.UserStatusActive),
|
|
||||||
}
|
|
||||||
|
|
||||||
s.userStore.UpdateUserStatus(ctx, newUser)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ResendOtp(
|
func (s *Service) ResendOtp(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
email, phone string,
|
email, phone string,
|
||||||
|
|
@ -150,10 +100,6 @@ func (s *Service) SendOtp(ctx context.Context, userID int64, sentTo string, otpF
|
||||||
return s.otpStore.CreateOtp(ctx, otp)
|
return s.otpStore.CreateOtp(ctx, otp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper function to get a pointer to time.Time
|
|
||||||
func timePtr(t time.Time) time.Time {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func hashPassword(plaintextPassword string) ([]byte, error) {
|
func hashPassword(plaintextPassword string) ([]byte, error) {
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
|
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package user
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error {
|
func (s *Service) UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error {
|
||||||
|
|
@ -33,7 +34,7 @@ func (s *Service) CreateUser(
|
||||||
EmailVerified: true, // assuming auto-verified on creation
|
EmailVerified: true, // assuming auto-verified on creation
|
||||||
PhoneVerified: true,
|
PhoneVerified: true,
|
||||||
Status: domain.UserStatusActive,
|
Status: domain.UserStatusActive,
|
||||||
Age: req.Age,
|
AgeGroup: req.AgeGroup,
|
||||||
EducationLevel: req.EducationLevel,
|
EducationLevel: req.EducationLevel,
|
||||||
Country: req.Country,
|
Country: req.Country,
|
||||||
Region: req.Region,
|
Region: req.Region,
|
||||||
|
|
@ -46,10 +47,44 @@ func (s *Service) DeleteUser(ctx context.Context, id int64) error {
|
||||||
return s.userStore.DeleteUser(ctx, id)
|
return s.userStore.DeleteUser(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) {
|
func (s *Service) GetAllUsers(
|
||||||
// Get all Users
|
ctx context.Context,
|
||||||
return s.userStore.GetAllUsers(ctx, &filter.Role, &filter.Query, &filter.CreatedBefore.Value, &filter.CreatedAfter.Value, int32(filter.PageSize), int32(filter.Page))
|
filter domain.UserFilter,
|
||||||
|
) ([]domain.User, int64, error) {
|
||||||
|
|
||||||
|
var before *time.Time
|
||||||
|
if filter.CreatedBefore.Valid {
|
||||||
|
before = &filter.CreatedBefore.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var after *time.Time
|
||||||
|
if filter.CreatedAfter.Valid {
|
||||||
|
after = &filter.CreatedAfter.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
var role *string
|
||||||
|
if filter.Role != "" {
|
||||||
|
role = &filter.Role
|
||||||
|
}
|
||||||
|
|
||||||
|
var query *string
|
||||||
|
if filter.Query != "" {
|
||||||
|
query = &filter.Query
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := int32(filter.Page * filter.PageSize)
|
||||||
|
|
||||||
|
return s.userStore.GetAllUsers(
|
||||||
|
ctx,
|
||||||
|
role,
|
||||||
|
query,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
int32(filter.PageSize),
|
||||||
|
offset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) GetUserById(ctx context.Context, id int64) (domain.User, error) {
|
func (s *Service) GetUserById(ctx context.Context, id int64) (domain.User, error) {
|
||||||
|
|
||||||
return s.userStore.GetUserByID(ctx, id)
|
return s.userStore.GetUserByID(ctx, id)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
tokenStore ports.TokenStore
|
||||||
userStore ports.UserStore
|
userStore ports.UserStore
|
||||||
otpStore ports.OtpStore
|
otpStore ports.OtpStore
|
||||||
messengerSvc *messenger.Service
|
messengerSvc *messenger.Service
|
||||||
|
|
@ -19,12 +20,14 @@ type Service struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
|
tokenStore ports.TokenStore,
|
||||||
userStore ports.UserStore,
|
userStore ports.UserStore,
|
||||||
otpStore ports.OtpStore,
|
otpStore ports.OtpStore,
|
||||||
messengerSvc *messenger.Service,
|
messengerSvc *messenger.Service,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
|
tokenStore: tokenStore,
|
||||||
userStore: userStore,
|
userStore: userStore,
|
||||||
otpStore: otpStore,
|
otpStore: otpStore,
|
||||||
messengerSvc: messengerSvc,
|
messengerSvc: messengerSvc,
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,14 @@ type loginUserRes struct {
|
||||||
// @Tags auth
|
// @Tags auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param login body authentication.LoginRequest true "Login user"
|
// @Param login body domain.LoginRequest true "Login user"
|
||||||
// @Success 200 {object} loginUserRes
|
// @Success 200 {object} loginUserRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /api/v1/{tenant_slug}/user-login [post]
|
// @Router /api/v1/{tenant_slug}/user-login [post]
|
||||||
func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
var req authentication.LoginRequest
|
var req domain.LoginRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.mongoLoggerSvc.Info("Failed to parse LoginUser request",
|
h.mongoLoggerSvc.Info("Failed to parse LoginUser request",
|
||||||
zap.Int("status_code", fiber.StatusBadRequest),
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
|
@ -180,14 +180,14 @@ type LoginAdminRes struct {
|
||||||
// @Tags auth
|
// @Tags auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param login body authentication.LoginRequest true "Login admin"
|
// @Param login body domain.LoginRequest true "Login admin"
|
||||||
// @Success 200 {object} LoginAdminRes
|
// @Success 200 {object} LoginAdminRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /api/v1/{tenant_slug}/admin-login [post]
|
// @Router /api/v1/{tenant_slug}/admin-login [post]
|
||||||
func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
||||||
var req authentication.LoginRequest
|
var req domain.LoginRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
|
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
|
||||||
zap.Int("status_code", fiber.StatusBadRequest),
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
|
@ -205,7 +205,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, errMsg)
|
return fiber.NewError(fiber.StatusBadRequest, errMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
successRes, err := h.authSvc.Login(c.Context(), authentication.LoginRequest(req))
|
successRes, err := h.authSvc.Login(c.Context(), domain.LoginRequest(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
||||||
|
|
@ -279,14 +279,14 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
||||||
// @Tags auth
|
// @Tags auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param login body authentication.LoginRequest true "Login super-admin"
|
// @Param login body domain.LoginRequest true "Login super-admin"
|
||||||
// @Success 200 {object} LoginAdminRes
|
// @Success 200 {object} LoginAdminRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /api/v1/super-login [post]
|
// @Router /api/v1/super-login [post]
|
||||||
func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
||||||
var req authentication.LoginRequest
|
var req domain.LoginRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
|
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
|
||||||
zap.Int("status_code", fiber.StatusBadRequest),
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
|
@ -304,7 +304,7 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, errMsg)
|
return fiber.NewError(fiber.StatusBadRequest, errMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
successRes, err := h.authSvc.Login(c.Context(), authentication.LoginRequest(req))
|
successRes, err := h.authSvc.Login(c.Context(), domain.LoginRequest(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
||||||
|
|
|
||||||
|
|
@ -390,7 +390,7 @@ func (h *Handler) CheckUserPending(c *fiber.Ctx) error {
|
||||||
// @Success 200 {object} response.APIResponse
|
// @Success 200 {object} response.APIResponse
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /api/v1/{tenant_slug}/users [get]
|
// @Router /api/v1/users [get]
|
||||||
func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
|
func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
|
||||||
searchQuery := c.Query("query")
|
searchQuery := c.Query("query")
|
||||||
searchString := domain.ValidString{
|
searchString := domain.ValidString{
|
||||||
|
|
@ -452,38 +452,38 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map to profile response to avoid leaking sensitive fields
|
// Map to profile response to avoid leaking sensitive fields
|
||||||
result := make([]domain.UserProfileResponse, len(users))
|
// result := make([]domain.UserProfileResponse, len(users))
|
||||||
for i, u := range users {
|
// for i, u := range users {
|
||||||
result[i] = domain.UserProfileResponse{
|
// result[i] = domain{
|
||||||
ID: u.ID,
|
// ID: u.ID,
|
||||||
FirstName: u.FirstName,
|
// FirstName: u.FirstName,
|
||||||
LastName: u.LastName,
|
// LastName: u.LastName,
|
||||||
// UserName: u.UserName,
|
// Gender: u.Gender,
|
||||||
Email: u.Email,
|
// Email: u.Email,
|
||||||
PhoneNumber: u.PhoneNumber,
|
// PhoneNumber: u.PhoneNumber,
|
||||||
Role: u.Role,
|
// Role: u.Role,
|
||||||
Age: u.Age,
|
// Age: u.Age,
|
||||||
EducationLevel: u.EducationLevel,
|
// EducationLevel: u.EducationLevel,
|
||||||
Country: u.Country,
|
// Country: u.Country,
|
||||||
Region: u.Region,
|
// Region: u.Region,
|
||||||
NickName: u.NickName,
|
// NickName: u.NickName,
|
||||||
Occupation: u.Occupation,
|
// Occupation: u.Occupation,
|
||||||
LearningGoal: u.LearningGoal,
|
// LearningGoal: u.LearningGoal,
|
||||||
LanguageGoal: u.LanguageGoal,
|
// LanguageGoal: u.LanguageGoal,
|
||||||
LanguageChallange: u.LanguageChallange,
|
// LanguageChallange: u.LanguageChallange,
|
||||||
FavouriteTopic: u.FavouriteTopic,
|
// FavouriteTopic: u.FavouriteTopic,
|
||||||
EmailVerified: u.EmailVerified,
|
// EmailVerified: u.EmailVerified,
|
||||||
PhoneVerified: u.PhoneVerified,
|
// PhoneVerified: u.PhoneVerified,
|
||||||
LastLogin: u.LastLogin,
|
// LastLogin: u.LastLogin,
|
||||||
ProfileCompleted: u.ProfileCompleted,
|
// ProfileCompleted: u.ProfileCompleted,
|
||||||
ProfilePictureURL: u.ProfilePictureURL,
|
// ProfilePictureURL: u.ProfilePictureURL,
|
||||||
PreferredLanguage: u.PreferredLanguage,
|
// PreferredLanguage: u.PreferredLanguage,
|
||||||
CreatedAt: u.CreatedAt,
|
// CreatedAt: u.CreatedAt,
|
||||||
UpdatedAt: u.UpdatedAt,
|
// UpdatedAt: u.UpdatedAt,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Users fetched successfully", map[string]interface{}{"users": result, "total": total}, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "Users fetched successfully", map[string]interface{}{"users": users, "total": total}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyOtp godoc
|
// VerifyOtp godoc
|
||||||
|
|
@ -523,7 +523,7 @@ func (h *Handler) VerifyOtp(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call service to verify OTP
|
// Call service to verify OTP
|
||||||
err := h.userSvc.VerifyOtp(c.Context(), req.Email, req.PhoneNumber, req.Otp)
|
loginSuccess, err := h.authSvc.VerifyOtp(c.Context(), req.Email, req.PhoneNumber, req.Otp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var errMsg string
|
var errMsg string
|
||||||
switch {
|
switch {
|
||||||
|
|
@ -555,7 +555,7 @@ func (h *Handler) VerifyOtp(c *fiber.Ctx) error {
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
Message: "OTP verified successfully",
|
Message: "OTP verified successfully",
|
||||||
Data: nil,
|
Data: loginSuccess,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1214,7 +1214,7 @@ func (h *Handler) GetUserProfile(c *fiber.Ctx) error {
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
PhoneNumber: user.PhoneNumber,
|
PhoneNumber: user.PhoneNumber,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
Age: user.Age,
|
AgeGroup: user.AgeGroup,
|
||||||
EducationLevel: user.EducationLevel,
|
EducationLevel: user.EducationLevel,
|
||||||
Country: user.Country,
|
Country: user.Country,
|
||||||
Region: user.Region,
|
Region: user.Region,
|
||||||
|
|
@ -1303,7 +1303,7 @@ func (h *Handler) AdminProfile(c *fiber.Ctx) error {
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
PhoneNumber: user.PhoneNumber,
|
PhoneNumber: user.PhoneNumber,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
Age: user.Age,
|
AgeGroup: user.AgeGroup,
|
||||||
EducationLevel: user.EducationLevel,
|
EducationLevel: user.EducationLevel,
|
||||||
Country: user.Country,
|
Country: user.Country,
|
||||||
Region: user.Region,
|
Region: user.Region,
|
||||||
|
|
@ -1425,7 +1425,7 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
PhoneNumber: user.PhoneNumber,
|
PhoneNumber: user.PhoneNumber,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
Age: user.Age,
|
AgeGroup: user.AgeGroup,
|
||||||
EducationLevel: user.EducationLevel,
|
EducationLevel: user.EducationLevel,
|
||||||
Country: user.Country,
|
Country: user.Country,
|
||||||
Region: user.Region,
|
Region: user.Region,
|
||||||
|
|
@ -1503,11 +1503,19 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error {
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
FirstName: user.FirstName,
|
FirstName: user.FirstName,
|
||||||
LastName: user.LastName,
|
LastName: user.LastName,
|
||||||
|
LearningGoal: user.LearningGoal,
|
||||||
|
LanguageGoal: user.LanguageGoal,
|
||||||
|
LanguageChallange: user.LanguageChallange,
|
||||||
|
Gender: user.Gender,
|
||||||
|
InitialAssessmentCompleted: user.InitialAssessmentCompleted,
|
||||||
|
NickName: user.NickName,
|
||||||
|
Occupation: user.Occupation,
|
||||||
|
FavouriteTopic: user.FavouriteTopic,
|
||||||
// UserName: user.UserName,
|
// UserName: user.UserName,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
PhoneNumber: user.PhoneNumber,
|
PhoneNumber: user.PhoneNumber,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
Age: user.Age,
|
AgeGroup: user.AgeGroup,
|
||||||
EducationLevel: user.EducationLevel,
|
EducationLevel: user.EducationLevel,
|
||||||
Country: user.Country,
|
Country: user.Country,
|
||||||
Region: user.Region,
|
Region: user.Region,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user