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
|
||||
├── .env # Environment configuration file
|
||||
└── 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"
|
||||
_ "Yimaru-Backend/docs"
|
||||
"Yimaru-Backend/internal/config"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
customlogger "Yimaru-Backend/internal/logger"
|
||||
|
|
@ -91,6 +92,7 @@ func main() {
|
|||
// )
|
||||
|
||||
userSvc := user.NewService(
|
||||
repository.NewTokenStore(store),
|
||||
repository.NewUserStore(store),
|
||||
repository.NewOTPStore(store),
|
||||
messengerSvc,
|
||||
|
|
@ -98,6 +100,7 @@ func main() {
|
|||
)
|
||||
|
||||
authSvc := authentication.NewService(
|
||||
repository.NewOTPStore(store),
|
||||
repository.NewUserStore(store),
|
||||
*userSvc,
|
||||
repository.NewTokenStore(store),
|
||||
|
|
|
|||
|
|
@ -5,70 +5,133 @@ INSERT INTO users (
|
|||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
-- user_name,
|
||||
gender,
|
||||
birth_day,
|
||||
email,
|
||||
phone_number,
|
||||
role,
|
||||
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,
|
||||
phone_verified,
|
||||
status,
|
||||
last_login,
|
||||
profile_completed,
|
||||
profile_picture_url,
|
||||
preferred_language,
|
||||
created_at
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
10,
|
||||
'Demo',
|
||||
'Student',
|
||||
-- 'demo_student',
|
||||
'Male',
|
||||
'2000-01-01',
|
||||
'student10@yimaru.com',
|
||||
NULL,
|
||||
'USER',
|
||||
crypt('password@123', gen_salt('bf'))::bytea,
|
||||
'ACTIVE',
|
||||
22,
|
||||
'Bachelor',
|
||||
'Ethiopia',
|
||||
'Addis Ababa',
|
||||
'BEGINNER',
|
||||
'Demo',
|
||||
'Student',
|
||||
'Learn programming',
|
||||
'English',
|
||||
'Grammar',
|
||||
'Technology',
|
||||
FALSE,
|
||||
TRUE,
|
||||
FALSE,
|
||||
'ACTIVE',
|
||||
NULL,
|
||||
FALSE,
|
||||
NULL,
|
||||
'en',
|
||||
CURRENT_TIMESTAMP
|
||||
CURRENT_TIMESTAMP,
|
||||
NULL
|
||||
),
|
||||
(
|
||||
11,
|
||||
'System',
|
||||
'Admin',
|
||||
-- 'sys_admin',
|
||||
'Female',
|
||||
'1995-01-01',
|
||||
'admin@yimaru.com',
|
||||
'0911001100',
|
||||
'ADMIN',
|
||||
crypt('password@123', gen_salt('bf'))::bytea,
|
||||
28,
|
||||
'Master',
|
||||
'Ethiopia',
|
||||
'Addis Ababa',
|
||||
'ADVANCED',
|
||||
'SysAdmin',
|
||||
'Administrator',
|
||||
'Manage system',
|
||||
'English',
|
||||
'Writing',
|
||||
'Management',
|
||||
TRUE,
|
||||
TRUE,
|
||||
TRUE,
|
||||
'ACTIVE',
|
||||
NULL,
|
||||
TRUE,
|
||||
TRUE,
|
||||
TRUE,
|
||||
NULL,
|
||||
'en',
|
||||
CURRENT_TIMESTAMP
|
||||
CURRENT_TIMESTAMP,
|
||||
NULL
|
||||
),
|
||||
(
|
||||
12,
|
||||
'Support',
|
||||
'Agent',
|
||||
-- 'support_agent',
|
||||
'Female',
|
||||
'1998-01-01',
|
||||
'support@yimaru.com',
|
||||
'0911223344',
|
||||
'SUPPORT',
|
||||
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',
|
||||
NULL,
|
||||
TRUE,
|
||||
TRUE,
|
||||
TRUE,
|
||||
NULL,
|
||||
'en',
|
||||
CURRENT_TIMESTAMP
|
||||
CURRENT_TIMESTAMP,
|
||||
NULL
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
|
||||
-- ======================================================
|
||||
-- Global Settings (LMS)
|
||||
-- ======================================================
|
||||
|
|
|
|||
|
|
@ -1,244 +1,267 @@
|
|||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
first_name VARCHAR(255),
|
||||
last_name VARCHAR(255),
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
first_name VARCHAR(255),
|
||||
last_name VARCHAR(255),
|
||||
|
||||
gender VARCHAR(255),
|
||||
birth_day DATE,
|
||||
gender VARCHAR(255),
|
||||
birth_day DATE,
|
||||
|
||||
email VARCHAR(255),
|
||||
phone_number VARCHAR(20),
|
||||
email VARCHAR(255),
|
||||
phone_number VARCHAR(20),
|
||||
|
||||
role VARCHAR(50) NOT NULL, -- SUPER_ADMIN, INSTRUCTOR, STUDENT, SUPPORT
|
||||
password BYTEA NOT NULL,
|
||||
age INT,
|
||||
education_level VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
region VARCHAR(100),
|
||||
role VARCHAR(50) NOT NULL, -- SUPER_ADMIN, INSTRUCTOR, STUDENT, SUPPORT
|
||||
password BYTEA NOT NULL,
|
||||
age INT,
|
||||
education_level VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
region VARCHAR(100),
|
||||
|
||||
knowledge_level VARCHAR(50), -- BEGINNER, INTERMEDIATE, ADVANCED
|
||||
nick_name VARCHAR(100),
|
||||
occupation VARCHAR(150),
|
||||
learning_goal TEXT,
|
||||
language_goal TEXT,
|
||||
language_challange TEXT,
|
||||
favourite_topic TEXT,
|
||||
knowledge_level VARCHAR(50), -- BEGINNER, INTERMEDIATE, ADVANCED
|
||||
nick_name VARCHAR(100),
|
||||
occupation VARCHAR(150),
|
||||
learning_goal TEXT,
|
||||
language_goal TEXT,
|
||||
language_challange TEXT,
|
||||
favourite_topic TEXT,
|
||||
|
||||
initial_assessment_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
status VARCHAR(50) NOT NULL, -- PENDING, ACTIVE, SUSPENDED, DEACTIVATED
|
||||
last_login TIMESTAMPTZ,
|
||||
profile_completed BOOLEAN,
|
||||
profile_picture_url TEXT,
|
||||
preferred_language VARCHAR(50),
|
||||
initial_assessment_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
status VARCHAR(50) NOT NULL, -- PENDING, ACTIVE, SUSPENDED, DEACTIVATED
|
||||
last_login TIMESTAMPTZ,
|
||||
profile_completed BOOLEAN,
|
||||
profile_picture_url TEXT,
|
||||
preferred_language VARCHAR(50),
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ,
|
||||
|
||||
-- Enforce: at least one contact method must be provided
|
||||
CONSTRAINT users_email_or_phone_required
|
||||
CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
|
||||
);
|
||||
-- Enforce: at least one contact method must be provided
|
||||
CONSTRAINT users_email_or_phone_required
|
||||
CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assessment_questions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
-- Remove the old column
|
||||
ALTER TABLE users
|
||||
DROP COLUMN age;
|
||||
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
-- Add age_group with constrained values
|
||||
ALTER TABLE users
|
||||
ADD COLUMN age_group VARCHAR(20);
|
||||
|
||||
question_type VARCHAR(50) NOT NULL,
|
||||
-- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
|
||||
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'
|
||||
)
|
||||
);
|
||||
|
||||
difficulty_level VARCHAR(50),
|
||||
-- EASY, MEDIUM, HARD
|
||||
CREATE TABLE IF NOT EXISTS assessment_questions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
points INT NOT NULL DEFAULT 1,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
question_type VARCHAR(50) NOT NULL,
|
||||
-- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ
|
||||
);
|
||||
difficulty_level VARCHAR(50),
|
||||
-- EASY, MEDIUM, HARD
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assessment_question_options (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
points INT NOT NULL DEFAULT 1,
|
||||
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
option_text TEXT NOT NULL,
|
||||
option_order INT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
is_correct BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
CREATE TABLE IF NOT EXISTS assessment_question_options (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
||||
|
||||
UNIQUE (question_id, option_order)
|
||||
);
|
||||
option_text TEXT NOT NULL,
|
||||
option_order INT NOT NULL,
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assessment_short_answers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
is_correct BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
correct_answer TEXT NOT NULL,
|
||||
UNIQUE (question_id, option_order)
|
||||
);
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS assessment_short_answers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
ALTER TABLE assessment_questions
|
||||
ADD CONSTRAINT chk_question_type
|
||||
CHECK (question_type IN ('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER'));
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assessment_attempts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
correct_answer TEXT NOT NULL,
|
||||
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
total_questions INT NOT NULL,
|
||||
total_points INT NOT NULL,
|
||||
ALTER TABLE assessment_questions
|
||||
ADD CONSTRAINT chk_question_type
|
||||
CHECK (question_type IN ('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER'));
|
||||
|
||||
score INT,
|
||||
percentage NUMERIC(5,2),
|
||||
CREATE TABLE IF NOT EXISTS assessment_attempts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
status VARCHAR(50) NOT NULL,
|
||||
-- IN_PROGRESS, SUBMITTED, EVALUATED
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
submitted_at TIMESTAMPTZ,
|
||||
evaluated_at TIMESTAMPTZ,
|
||||
total_questions INT NOT NULL,
|
||||
total_points INT NOT NULL,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ
|
||||
);
|
||||
score INT,
|
||||
percentage NUMERIC(5,2),
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assessment_attempt_questions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
-- IN_PROGRESS, SUBMITTED, EVALUATED
|
||||
|
||||
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
submitted_at TIMESTAMPTZ,
|
||||
evaluated_at TIMESTAMPTZ,
|
||||
|
||||
question_type VARCHAR(50) NOT NULL,
|
||||
points INT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CREATE TABLE IF NOT EXISTS assessment_attempt_questions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
UNIQUE (attempt_id, question_id)
|
||||
);
|
||||
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assessment_attempt_answers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
question_type VARCHAR(50) NOT NULL,
|
||||
points INT NOT NULL,
|
||||
|
||||
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- For MCQ / TRUE_FALSE
|
||||
selected_option_id BIGINT
|
||||
REFERENCES assessment_question_options(id),
|
||||
UNIQUE (attempt_id, question_id)
|
||||
);
|
||||
|
||||
-- For SHORT_ANSWER
|
||||
submitted_text TEXT,
|
||||
CREATE TABLE IF NOT EXISTS assessment_attempt_answers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
is_correct BOOLEAN,
|
||||
awarded_points INT NOT NULL DEFAULT 0,
|
||||
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
-- For MCQ / TRUE_FALSE
|
||||
selected_option_id BIGINT
|
||||
REFERENCES assessment_question_options(id),
|
||||
|
||||
UNIQUE (attempt_id, question_id),
|
||||
-- For SHORT_ANSWER
|
||||
submitted_text TEXT,
|
||||
|
||||
CHECK (
|
||||
(selected_option_id IS NOT NULL AND submitted_text IS NULL)
|
||||
OR
|
||||
(selected_option_id IS NULL AND submitted_text IS NOT NULL)
|
||||
)
|
||||
);
|
||||
is_correct BOOLEAN,
|
||||
awarded_points INT NOT NULL DEFAULT 0,
|
||||
|
||||
ALTER TABLE assessment_attempts
|
||||
ADD CONSTRAINT chk_attempt_status
|
||||
CHECK (status IN ('IN_PROGRESS', 'SUBMITTED', 'EVALUATED'));
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
ALTER TABLE assessment_attempt_questions
|
||||
ADD CONSTRAINT chk_attempt_question_type
|
||||
CHECK (question_type IN ('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER'));
|
||||
UNIQUE (attempt_id, question_id),
|
||||
|
||||
CREATE TABLE refresh_tokens (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CHECK (
|
||||
(selected_option_id IS NOT NULL AND submitted_text IS NULL)
|
||||
OR
|
||||
(selected_option_id IS NULL AND submitted_text IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE otps (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGSERIAL NOT NULL,
|
||||
sent_to VARCHAR(255) NOT NULL,
|
||||
medium VARCHAR(50) NOT NULL, -- email, sms
|
||||
otp_for VARCHAR(50) NOT NULL, -- register, reset
|
||||
otp VARCHAR(10) NOT NULL,
|
||||
used BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
used_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
ALTER TABLE assessment_attempts
|
||||
ADD CONSTRAINT chk_attempt_status
|
||||
CHECK (status IN ('IN_PROGRESS', 'SUBMITTED', 'EVALUATED'));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
ALTER TABLE assessment_attempt_questions
|
||||
ADD CONSTRAINT chk_attempt_question_type
|
||||
CHECK (question_type IN ('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER'));
|
||||
|
||||
type TEXT NOT NULL CHECK (
|
||||
type IN (
|
||||
'course_enrolled',
|
||||
'lesson_completed',
|
||||
'assessment_assigned',
|
||||
'assessment_submitted',
|
||||
'assessment_graded',
|
||||
'course_completed',
|
||||
'certificate_issued',
|
||||
'announcement',
|
||||
'otp_sent',
|
||||
'signup_welcome',
|
||||
'system_alert'
|
||||
)
|
||||
),
|
||||
CREATE TABLE refresh_tokens (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
level TEXT NOT NULL CHECK (
|
||||
level IN ('info', 'warning', 'success', 'error')
|
||||
),
|
||||
CREATE TABLE otps (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGSERIAL NOT NULL,
|
||||
sent_to VARCHAR(255) NOT NULL,
|
||||
medium VARCHAR(50) NOT NULL, -- email, sms
|
||||
otp_for VARCHAR(50) NOT NULL, -- register, reset
|
||||
otp VARCHAR(10) NOT NULL,
|
||||
used BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
used_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
channel TEXT CHECK (
|
||||
channel IN ('email', 'sms', 'push', 'in_app')
|
||||
),
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
title TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK (
|
||||
type IN (
|
||||
'course_enrolled',
|
||||
'lesson_completed',
|
||||
'assessment_assigned',
|
||||
'assessment_submitted',
|
||||
'assessment_graded',
|
||||
'course_completed',
|
||||
'certificate_issued',
|
||||
'announcement',
|
||||
'otp_sent',
|
||||
'signup_welcome',
|
||||
'system_alert'
|
||||
)
|
||||
),
|
||||
|
||||
payload JSONB,
|
||||
is_read BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
level TEXT NOT NULL CHECK (
|
||||
level IN ('info', 'warning', 'success', 'error')
|
||||
),
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
read_at TIMESTAMPTZ
|
||||
);
|
||||
channel TEXT CHECK (
|
||||
channel IN ('email', 'sms', 'push', 'in_app')
|
||||
),
|
||||
|
||||
CREATE TABLE global_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
title TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
|
||||
payload JSONB,
|
||||
is_read BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
read_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE global_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reported_issues (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
user_role VARCHAR(255) NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
issue_type TEXT NOT NULL,
|
||||
-- e.g., "deposit", "withdrawal", "bet", "technical"
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
-- pending, in_progress, resolved, rejected
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reported_issues (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
user_role VARCHAR(255) NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
issue_type TEXT NOT NULL,
|
||||
-- e.g., "deposit", "withdrawal", "bet", "technical"
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
-- pending, in_progress, resolved, rejected
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ FROM users
|
|||
WHERE id = $1
|
||||
LIMIT 1;
|
||||
|
||||
|
||||
-- name: IsProfileCompleted :one
|
||||
SELECT
|
||||
CASE WHEN profile_completed = true THEN true ELSE false END AS is_pending
|
||||
|
|
@ -12,6 +13,7 @@ FROM users
|
|||
WHERE id = $1
|
||||
LIMIT 1;
|
||||
|
||||
|
||||
-- name: IsUserNameUnique :one
|
||||
SELECT
|
||||
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique
|
||||
|
|
@ -19,6 +21,7 @@ FROM users
|
|||
WHERE id = $1;
|
||||
|
||||
|
||||
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO users (
|
||||
first_name,
|
||||
|
|
@ -29,7 +32,7 @@ INSERT INTO users (
|
|||
phone_number,
|
||||
role,
|
||||
password,
|
||||
age,
|
||||
age_group,
|
||||
education_level,
|
||||
country,
|
||||
region,
|
||||
|
|
@ -51,33 +54,33 @@ INSERT INTO users (
|
|||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1, -- first_name
|
||||
$2, -- last_name
|
||||
$3, -- gender
|
||||
$4, -- birth_day
|
||||
$5, -- email
|
||||
$6, -- phone_number
|
||||
$7, -- role
|
||||
$8, -- password
|
||||
$9, -- age
|
||||
$10, -- education_level
|
||||
$11, -- country
|
||||
$12, -- region
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
$9, -- age_group
|
||||
$10,
|
||||
$11,
|
||||
$12,
|
||||
|
||||
$13, -- nick_name
|
||||
$14, -- occupation
|
||||
$15, -- learning_goal
|
||||
$16, -- language_goal
|
||||
$17, -- language_challange
|
||||
$18, -- favourite_topic
|
||||
$13,
|
||||
$14,
|
||||
$15,
|
||||
$16,
|
||||
$17,
|
||||
$18,
|
||||
|
||||
$19, -- initial_assessment_completed
|
||||
$20, -- email_verified
|
||||
$21, -- phone_verified
|
||||
$22, -- status
|
||||
$23, -- profile_completed
|
||||
$24, -- profile_picture_url
|
||||
$25, -- preferred_language
|
||||
$19,
|
||||
$20,
|
||||
$21,
|
||||
$22,
|
||||
$23,
|
||||
$24,
|
||||
$25,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
RETURNING
|
||||
|
|
@ -89,7 +92,7 @@ RETURNING
|
|||
email,
|
||||
phone_number,
|
||||
role,
|
||||
age,
|
||||
age_group,
|
||||
education_level,
|
||||
country,
|
||||
region,
|
||||
|
|
@ -111,11 +114,13 @@ RETURNING
|
|||
created_at,
|
||||
updated_at;
|
||||
|
||||
|
||||
-- name: GetUserByID :one
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE id = $1;
|
||||
|
||||
|
||||
-- name: GetAllUsers :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
|
|
@ -127,10 +132,11 @@ SELECT
|
|||
email,
|
||||
phone_number,
|
||||
role,
|
||||
age,
|
||||
age_group,
|
||||
education_level,
|
||||
country,
|
||||
region,
|
||||
knowledge_level,
|
||||
nick_name,
|
||||
occupation,
|
||||
learning_goal,
|
||||
|
|
@ -138,30 +144,26 @@ SELECT
|
|||
language_challange,
|
||||
favourite_topic,
|
||||
initial_assessment_completed,
|
||||
profile_picture_url,
|
||||
preferred_language,
|
||||
email_verified,
|
||||
phone_verified,
|
||||
status,
|
||||
last_login,
|
||||
profile_completed,
|
||||
profile_picture_url,
|
||||
preferred_language,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM users
|
||||
WHERE ($1 IS NULL OR role = $1)
|
||||
AND ($2 IS NULL OR first_name ILIKE '%' || $2 || '%'
|
||||
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;
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
||||
|
||||
-- name: GetTotalUsers :one
|
||||
SELECT COUNT(*)
|
||||
FROM users
|
||||
WHERE (role = $1 OR $1 IS NULL);
|
||||
|
||||
|
||||
-- name: SearchUserByNameOrPhone :many
|
||||
SELECT
|
||||
id,
|
||||
|
|
@ -172,7 +174,7 @@ SELECT
|
|||
email,
|
||||
phone_number,
|
||||
role,
|
||||
age,
|
||||
age_group,
|
||||
education_level,
|
||||
country,
|
||||
region,
|
||||
|
|
@ -205,17 +207,14 @@ WHERE (
|
|||
OR sqlc.narg('role') IS NULL
|
||||
);
|
||||
|
||||
|
||||
-- name: UpdateUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
first_name = COALESCE($1, first_name),
|
||||
last_name = COALESCE($2, last_name),
|
||||
|
||||
-- email = COALESCE($3, email),
|
||||
-- phone_number = COALESCE($4, phone_number),
|
||||
|
||||
knowledge_level = COALESCE($3, knowledge_level),
|
||||
age = COALESCE($4, age),
|
||||
age_group = COALESCE($4, age_group),
|
||||
education_level = COALESCE($5, education_level),
|
||||
country = COALESCE($6, country),
|
||||
region = COALESCE($7, region),
|
||||
|
|
@ -226,21 +225,19 @@ SET
|
|||
language_challange = COALESCE($12, language_challange),
|
||||
favourite_topic = COALESCE($13, favourite_topic),
|
||||
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_picture_url = COALESCE($16, profile_picture_url),
|
||||
preferred_language = COALESCE($17, preferred_language),
|
||||
gender = COALESCE($18, gender),
|
||||
birth_day = COALESCE($19, gender),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
birth_day = COALESCE($19, birth_day),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $20;
|
||||
|
||||
-- name: DeleteUser :exec
|
||||
DELETE FROM users
|
||||
WHERE id = $1;
|
||||
|
||||
|
||||
-- name: CheckPhoneEmailExist :one
|
||||
SELECT
|
||||
EXISTS (
|
||||
|
|
@ -250,6 +247,7 @@ SELECT
|
|||
SELECT 1 FROM users u2 WHERE u2.email = $2
|
||||
) AS email_exists;
|
||||
|
||||
|
||||
-- -- name: GetUserByUserName :one
|
||||
-- SELECT
|
||||
-- id,
|
||||
|
|
@ -295,7 +293,7 @@ SELECT
|
|||
phone_number,
|
||||
role,
|
||||
password,
|
||||
age,
|
||||
age_group,
|
||||
education_level,
|
||||
country,
|
||||
region,
|
||||
|
|
@ -341,3 +339,4 @@ SET
|
|||
knowledge_level = $1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2;
|
||||
|
||||
|
|
|
|||
|
|
@ -221,7 +221,6 @@ type User struct {
|
|||
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"`
|
||||
|
|
@ -242,4 +241,5 @@ type User struct {
|
|||
PreferredLanguage pgtype.Text `json:"preferred_language"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
AgeGroup pgtype.Text `json:"age_group"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ INSERT INTO users (
|
|||
phone_number,
|
||||
role,
|
||||
password,
|
||||
age,
|
||||
age_group,
|
||||
education_level,
|
||||
country,
|
||||
region,
|
||||
|
|
@ -70,33 +70,33 @@ INSERT INTO users (
|
|||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1, -- first_name
|
||||
$2, -- last_name
|
||||
$3, -- gender
|
||||
$4, -- birth_day
|
||||
$5, -- email
|
||||
$6, -- phone_number
|
||||
$7, -- role
|
||||
$8, -- password
|
||||
$9, -- age
|
||||
$10, -- education_level
|
||||
$11, -- country
|
||||
$12, -- region
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
$9, -- age_group
|
||||
$10,
|
||||
$11,
|
||||
$12,
|
||||
|
||||
$13, -- nick_name
|
||||
$14, -- occupation
|
||||
$15, -- learning_goal
|
||||
$16, -- language_goal
|
||||
$17, -- language_challange
|
||||
$18, -- favourite_topic
|
||||
$13,
|
||||
$14,
|
||||
$15,
|
||||
$16,
|
||||
$17,
|
||||
$18,
|
||||
|
||||
$19, -- initial_assessment_completed
|
||||
$20, -- email_verified
|
||||
$21, -- phone_verified
|
||||
$22, -- status
|
||||
$23, -- profile_completed
|
||||
$24, -- profile_picture_url
|
||||
$25, -- preferred_language
|
||||
$19,
|
||||
$20,
|
||||
$21,
|
||||
$22,
|
||||
$23,
|
||||
$24,
|
||||
$25,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
RETURNING
|
||||
|
|
@ -108,7 +108,7 @@ RETURNING
|
|||
email,
|
||||
phone_number,
|
||||
role,
|
||||
age,
|
||||
age_group,
|
||||
education_level,
|
||||
country,
|
||||
region,
|
||||
|
|
@ -140,7 +140,7 @@ type CreateUserParams struct {
|
|||
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||
Role string `json:"role"`
|
||||
Password []byte `json:"password"`
|
||||
Age pgtype.Int4 `json:"age"`
|
||||
AgeGroup pgtype.Text `json:"age_group"`
|
||||
EducationLevel pgtype.Text `json:"education_level"`
|
||||
Country pgtype.Text `json:"country"`
|
||||
Region pgtype.Text `json:"region"`
|
||||
|
|
@ -168,7 +168,7 @@ type CreateUserRow struct {
|
|||
Email pgtype.Text `json:"email"`
|
||||
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||
Role string `json:"role"`
|
||||
Age pgtype.Int4 `json:"age"`
|
||||
AgeGroup pgtype.Text `json:"age_group"`
|
||||
EducationLevel pgtype.Text `json:"education_level"`
|
||||
Country pgtype.Text `json:"country"`
|
||||
Region pgtype.Text `json:"region"`
|
||||
|
|
@ -199,7 +199,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
|
|||
arg.PhoneNumber,
|
||||
arg.Role,
|
||||
arg.Password,
|
||||
arg.Age,
|
||||
arg.AgeGroup,
|
||||
arg.EducationLevel,
|
||||
arg.Country,
|
||||
arg.Region,
|
||||
|
|
@ -227,7 +227,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
|
|||
&i.Email,
|
||||
&i.PhoneNumber,
|
||||
&i.Role,
|
||||
&i.Age,
|
||||
&i.AgeGroup,
|
||||
&i.EducationLevel,
|
||||
&i.Country,
|
||||
&i.Region,
|
||||
|
|
@ -271,10 +271,11 @@ SELECT
|
|||
email,
|
||||
phone_number,
|
||||
role,
|
||||
age,
|
||||
age_group,
|
||||
education_level,
|
||||
country,
|
||||
region,
|
||||
knowledge_level,
|
||||
nick_name,
|
||||
occupation,
|
||||
learning_goal,
|
||||
|
|
@ -282,33 +283,23 @@ SELECT
|
|||
language_challange,
|
||||
favourite_topic,
|
||||
initial_assessment_completed,
|
||||
profile_picture_url,
|
||||
preferred_language,
|
||||
email_verified,
|
||||
phone_verified,
|
||||
status,
|
||||
last_login,
|
||||
profile_completed,
|
||||
profile_picture_url,
|
||||
preferred_language,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM users
|
||||
WHERE ($1 IS NULL OR role = $1)
|
||||
AND ($2 IS NULL OR first_name ILIKE '%' || $2 || '%'
|
||||
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
|
||||
LIMIT $2::INT
|
||||
OFFSET $1::INT
|
||||
`
|
||||
|
||||
type GetAllUsersParams struct {
|
||||
Column1 interface{} `json:"column_1"`
|
||||
Column2 interface{} `json:"column_2"`
|
||||
Column3 interface{} `json:"column_3"`
|
||||
Column4 interface{} `json:"column_4"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
Offset pgtype.Int4 `json:"offset"`
|
||||
Limit pgtype.Int4 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetAllUsersRow struct {
|
||||
|
|
@ -321,10 +312,11 @@ type GetAllUsersRow struct {
|
|||
Email pgtype.Text `json:"email"`
|
||||
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||
Role string `json:"role"`
|
||||
Age pgtype.Int4 `json:"age"`
|
||||
AgeGroup pgtype.Text `json:"age_group"`
|
||||
EducationLevel pgtype.Text `json:"education_level"`
|
||||
Country pgtype.Text `json:"country"`
|
||||
Region pgtype.Text `json:"region"`
|
||||
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
|
||||
NickName pgtype.Text `json:"nick_name"`
|
||||
Occupation pgtype.Text `json:"occupation"`
|
||||
LearningGoal pgtype.Text `json:"learning_goal"`
|
||||
|
|
@ -332,25 +324,19 @@ type GetAllUsersRow struct {
|
|||
LanguageChallange pgtype.Text `json:"language_challange"`
|
||||
FavouriteTopic pgtype.Text `json:"favourite_topic"`
|
||||
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
|
||||
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
|
||||
PreferredLanguage pgtype.Text `json:"preferred_language"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
PhoneVerified bool `json:"phone_verified"`
|
||||
Status string `json:"status"`
|
||||
LastLogin pgtype.Timestamptz `json:"last_login"`
|
||||
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"`
|
||||
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.Column1,
|
||||
arg.Column2,
|
||||
arg.Column3,
|
||||
arg.Column4,
|
||||
arg.Limit,
|
||||
arg.Offset,
|
||||
)
|
||||
rows, err := q.db.Query(ctx, GetAllUsers, arg.Offset, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -368,10 +354,11 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
|
|||
&i.Email,
|
||||
&i.PhoneNumber,
|
||||
&i.Role,
|
||||
&i.Age,
|
||||
&i.AgeGroup,
|
||||
&i.EducationLevel,
|
||||
&i.Country,
|
||||
&i.Region,
|
||||
&i.KnowledgeLevel,
|
||||
&i.NickName,
|
||||
&i.Occupation,
|
||||
&i.LearningGoal,
|
||||
|
|
@ -379,12 +366,13 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
|
|||
&i.LanguageChallange,
|
||||
&i.FavouriteTopic,
|
||||
&i.InitialAssessmentCompleted,
|
||||
&i.ProfilePictureUrl,
|
||||
&i.PreferredLanguage,
|
||||
&i.EmailVerified,
|
||||
&i.PhoneVerified,
|
||||
&i.Status,
|
||||
&i.LastLogin,
|
||||
&i.ProfileCompleted,
|
||||
&i.ProfilePictureUrl,
|
||||
&i.PreferredLanguage,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
|
|
@ -425,7 +413,7 @@ SELECT
|
|||
phone_number,
|
||||
role,
|
||||
password,
|
||||
age,
|
||||
age_group,
|
||||
education_level,
|
||||
country,
|
||||
region,
|
||||
|
|
@ -467,7 +455,7 @@ type GetUserByEmailPhoneRow struct {
|
|||
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||
Role string `json:"role"`
|
||||
Password []byte `json:"password"`
|
||||
Age pgtype.Int4 `json:"age"`
|
||||
AgeGroup pgtype.Text `json:"age_group"`
|
||||
EducationLevel pgtype.Text `json:"education_level"`
|
||||
Country pgtype.Text `json:"country"`
|
||||
Region pgtype.Text `json:"region"`
|
||||
|
|
@ -534,7 +522,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
|||
&i.PhoneNumber,
|
||||
&i.Role,
|
||||
&i.Password,
|
||||
&i.Age,
|
||||
&i.AgeGroup,
|
||||
&i.EducationLevel,
|
||||
&i.Country,
|
||||
&i.Region,
|
||||
|
|
@ -558,7 +546,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
|||
}
|
||||
|
||||
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
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -576,7 +564,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
|||
&i.PhoneNumber,
|
||||
&i.Role,
|
||||
&i.Password,
|
||||
&i.Age,
|
||||
&i.EducationLevel,
|
||||
&i.Country,
|
||||
&i.Region,
|
||||
|
|
@ -597,6 +584,7 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
|||
&i.PreferredLanguage,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.AgeGroup,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -655,7 +643,7 @@ SELECT
|
|||
email,
|
||||
phone_number,
|
||||
role,
|
||||
age,
|
||||
age_group,
|
||||
education_level,
|
||||
country,
|
||||
region,
|
||||
|
|
@ -703,7 +691,7 @@ type SearchUserByNameOrPhoneRow struct {
|
|||
Email pgtype.Text `json:"email"`
|
||||
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||
Role string `json:"role"`
|
||||
Age pgtype.Int4 `json:"age"`
|
||||
AgeGroup pgtype.Text `json:"age_group"`
|
||||
EducationLevel pgtype.Text `json:"education_level"`
|
||||
Country pgtype.Text `json:"country"`
|
||||
Region pgtype.Text `json:"region"`
|
||||
|
|
@ -742,7 +730,7 @@ func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByN
|
|||
&i.Email,
|
||||
&i.PhoneNumber,
|
||||
&i.Role,
|
||||
&i.Age,
|
||||
&i.AgeGroup,
|
||||
&i.EducationLevel,
|
||||
&i.Country,
|
||||
&i.Region,
|
||||
|
|
@ -795,12 +783,8 @@ UPDATE users
|
|||
SET
|
||||
first_name = COALESCE($1, first_name),
|
||||
last_name = COALESCE($2, last_name),
|
||||
|
||||
-- email = COALESCE($3, email),
|
||||
-- phone_number = COALESCE($4, phone_number),
|
||||
|
||||
knowledge_level = COALESCE($3, knowledge_level),
|
||||
age = COALESCE($4, age),
|
||||
age_group = COALESCE($4, age_group),
|
||||
education_level = COALESCE($5, education_level),
|
||||
country = COALESCE($6, country),
|
||||
region = COALESCE($7, region),
|
||||
|
|
@ -811,15 +795,12 @@ SET
|
|||
language_challange = COALESCE($12, language_challange),
|
||||
favourite_topic = COALESCE($13, favourite_topic),
|
||||
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_picture_url = COALESCE($16, profile_picture_url),
|
||||
preferred_language = COALESCE($17, preferred_language),
|
||||
gender = COALESCE($18, gender),
|
||||
birth_day = COALESCE($19, gender),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
birth_day = COALESCE($19, birth_day),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $20
|
||||
`
|
||||
|
||||
|
|
@ -827,7 +808,7 @@ type UpdateUserParams struct {
|
|||
FirstName pgtype.Text `json:"first_name"`
|
||||
LastName pgtype.Text `json:"last_name"`
|
||||
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
|
||||
Age pgtype.Int4 `json:"age"`
|
||||
AgeGroup pgtype.Text `json:"age_group"`
|
||||
EducationLevel pgtype.Text `json:"education_level"`
|
||||
Country pgtype.Text `json:"country"`
|
||||
Region pgtype.Text `json:"region"`
|
||||
|
|
@ -851,7 +832,7 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
|||
arg.FirstName,
|
||||
arg.LastName,
|
||||
arg.KnowledgeLevel,
|
||||
arg.Age,
|
||||
arg.AgeGroup,
|
||||
arg.EducationLevel,
|
||||
arg.Country,
|
||||
arg.Region,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,21 @@
|
|||
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 {
|
||||
ID int64
|
||||
|
|
|
|||
|
|
@ -5,6 +5,18 @@ import (
|
|||
"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 (
|
||||
ErrUserNotVerified = errors.New("user not verified")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
|
|
@ -42,7 +54,7 @@ type User struct {
|
|||
Password []byte
|
||||
Role Role
|
||||
|
||||
Age int
|
||||
AgeGroup string
|
||||
EducationLevel string
|
||||
Country string
|
||||
Region string
|
||||
|
|
@ -81,7 +93,7 @@ type UserProfileResponse struct {
|
|||
PhoneNumber string `json:"phone_number,omitempty"`
|
||||
Role Role `json:"role"`
|
||||
|
||||
Age int `json:"age,omitempty"`
|
||||
AgeGroup string `json:"age_group,omitempty"`
|
||||
EducationLevel string `json:"education_level,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
|
|
@ -138,7 +150,7 @@ type CreateUserReq struct {
|
|||
|
||||
Status UserStatus
|
||||
|
||||
Age int
|
||||
AgeGroup string
|
||||
EducationLevel string
|
||||
Country string
|
||||
Region string
|
||||
|
|
@ -166,27 +178,20 @@ type UpdateUserStatusReq struct {
|
|||
}
|
||||
|
||||
type UpdateUserReq struct {
|
||||
// Identity (enforced from auth context, not request body)
|
||||
UserID int64 `json:"-"`
|
||||
|
||||
// Basic profile
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
|
||||
Gender string `json:"gender"`
|
||||
BirthDay time.Time `json:"birth_day"`
|
||||
Gender string `json:"gender"`
|
||||
BirthDay *string `json:"birth_day"` // YYYY-MM-DD
|
||||
|
||||
// Contact (optional – at least one must exist at DB level)
|
||||
// Email string `json:"email"`
|
||||
// PhoneNumber string `json:"phone_number"`
|
||||
AgeGroup *AgeGroup `json:"age_group"`
|
||||
|
||||
// Personal details
|
||||
Age int64 `json:"age"`
|
||||
EducationLevel string `json:"education_level"`
|
||||
Country string `json:"country"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Learning / profile
|
||||
KnowledgeLevel string `json:"knowledge_level"`
|
||||
NickName string `json:"nick_name"`
|
||||
Occupation string `json:"occupation"`
|
||||
|
|
@ -196,11 +201,8 @@ type UpdateUserReq struct {
|
|||
FavouriteTopic string `json:"favourite_topic"`
|
||||
|
||||
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"`
|
||||
PreferredLanguage string `json:"preferred_language"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ func (s *Store) GetUserByEmailOrPhone(
|
|||
Password: u.Password,
|
||||
Role: domain.Role(u.Role),
|
||||
|
||||
Age: int(u.Age.Int32),
|
||||
AgeGroup: u.AgeGroup.String,
|
||||
EducationLevel: u.EducationLevel.String,
|
||||
Country: u.Country.String,
|
||||
Region: u.Region.String,
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ func (s *Store) CreateUserWithoutOtp(
|
|||
Role: string(user.Role),
|
||||
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 != ""},
|
||||
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
|
||||
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""},
|
||||
|
|
@ -178,7 +178,7 @@ func (s *Store) CreateUser(
|
|||
Role: string(user.Role),
|
||||
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 != ""},
|
||||
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
|
||||
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""},
|
||||
|
|
@ -254,7 +254,7 @@ func (s *Store) GetUserByID(
|
|||
PhoneNumber: u.PhoneNumber.String,
|
||||
Role: domain.Role(u.Role),
|
||||
|
||||
Age: int(u.Age.Int32),
|
||||
AgeGroup: u.AgeGroup.String,
|
||||
EducationLevel: u.EducationLevel.String,
|
||||
Country: u.Country.String,
|
||||
Region: u.Region.String,
|
||||
|
|
@ -289,41 +289,56 @@ func (s *Store) GetAllUsers(
|
|||
limit, offset int32,
|
||||
) ([]domain.User, int64, error) {
|
||||
|
||||
var roleParam sql.NullString
|
||||
if role != nil && *role != "" {
|
||||
roleParam = sql.NullString{String: *role, Valid: true}
|
||||
} else {
|
||||
roleParam = sql.NullString{Valid: false} // This will make $1 IS NULL work
|
||||
}
|
||||
// var roleParam sql.NullString
|
||||
// if role != nil && *role != "" {
|
||||
// roleParam = sql.NullString{String: *role, Valid: true}
|
||||
// } else {
|
||||
// roleParam = sql.NullString{Valid: false} // This will make $1 IS NULL work
|
||||
// }
|
||||
|
||||
var queryParam sql.NullString
|
||||
if query != nil && *query != "" {
|
||||
queryParam = sql.NullString{String: *query, Valid: true}
|
||||
} else {
|
||||
queryParam = sql.NullString{Valid: false}
|
||||
}
|
||||
// var queryParam sql.NullString
|
||||
// if query != nil && *query != "" {
|
||||
// queryParam = sql.NullString{String: *query, Valid: true}
|
||||
// } else {
|
||||
// queryParam = sql.NullString{Valid: false}
|
||||
// }
|
||||
|
||||
var createdAfterParam sql.NullTime
|
||||
if createdAfter != nil {
|
||||
createdAfterParam = sql.NullTime{Time: *createdAfter, Valid: true}
|
||||
} else {
|
||||
createdAfterParam = sql.NullTime{Valid: false}
|
||||
}
|
||||
// var createdAfterParam sql.NullTime
|
||||
// if createdAfter != nil {
|
||||
// createdAfterParam = sql.NullTime{Time: *createdAfter, Valid: true}
|
||||
// } else {
|
||||
// createdAfterParam = sql.NullTime{Valid: false}
|
||||
// }
|
||||
|
||||
var createdBeforeParam sql.NullTime
|
||||
if createdBefore != nil {
|
||||
createdBeforeParam = sql.NullTime{Time: *createdBefore, Valid: true}
|
||||
} else {
|
||||
createdBeforeParam = sql.NullTime{Valid: false}
|
||||
}
|
||||
// var createdBeforeParam sql.NullTime
|
||||
// if createdBefore != nil {
|
||||
// createdBeforeParam = sql.NullTime{Time: *createdBefore, Valid: true}
|
||||
// } else {
|
||||
// createdBeforeParam = sql.NullTime{Valid: false}
|
||||
// }
|
||||
|
||||
params := dbgen.GetAllUsersParams{
|
||||
Column1: roleParam.String,
|
||||
Column2: pgtype.Text{String: queryParam.String, Valid: queryParam.Valid},
|
||||
Column3: pgtype.Timestamptz{Time: createdAfterParam.Time, Valid: createdAfterParam.Valid},
|
||||
Column4: pgtype.Timestamptz{Time: createdBeforeParam.Time, Valid: createdBeforeParam.Valid},
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
// Role: pgtype.Text{
|
||||
// String: roleParam.String,
|
||||
// Valid: roleParam.String != "",
|
||||
// },
|
||||
// Query: queryParam.String,
|
||||
// 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)
|
||||
|
|
@ -356,7 +371,7 @@ func (s *Store) GetAllUsers(
|
|||
PhoneNumber: u.PhoneNumber.String,
|
||||
Role: domain.Role(u.Role),
|
||||
|
||||
Age: int(u.Age.Int32),
|
||||
AgeGroup: u.AgeGroup.String,
|
||||
EducationLevel: u.EducationLevel.String,
|
||||
Country: u.Country.String,
|
||||
Region: u.Region.String,
|
||||
|
|
@ -443,7 +458,7 @@ func (s *Store) SearchUserByNameOrPhone(
|
|||
PhoneNumber: u.PhoneNumber.String,
|
||||
Role: domain.Role(u.Role),
|
||||
|
||||
Age: int(u.Age.Int32),
|
||||
AgeGroup: u.AgeGroup.String,
|
||||
EducationLevel: u.EducationLevel.String,
|
||||
Country: u.Country.String,
|
||||
Region: u.Region.String,
|
||||
|
|
@ -477,19 +492,27 @@ func (s *Store) UpdateUser(
|
|||
req domain.UpdateUserReq,
|
||||
) 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{
|
||||
FirstName: pgtype.Text{String: req.FirstName, Valid: req.FirstName != ""},
|
||||
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 != ""},
|
||||
Country: pgtype.Text{String: req.Country, Valid: req.Country != ""},
|
||||
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 != ""},
|
||||
FavouriteTopic: pgtype.Text{String: req.FavouriteTopic, Valid: req.FavouriteTopic != ""},
|
||||
|
||||
ProfileCompleted: pgtype.Bool{
|
||||
Bool: req.ProfileCompleted,
|
||||
Valid: true,
|
||||
},
|
||||
InitialAssessmentCompleted: req.InitialAssessmentCompleted,
|
||||
ProfileCompleted: pgtype.Bool{Bool: req.ProfileCompleted, Valid: true},
|
||||
|
||||
ProfilePictureUrl: pgtype.Text{String: req.ProfilePictureURL, Valid: req.ProfilePictureURL != ""},
|
||||
PreferredLanguage: pgtype.Text{String: req.PreferredLanguage, Valid: req.PreferredLanguage != ""},
|
||||
|
||||
Gender: pgtype.Text{String: req.Gender, Valid: req.Gender != ""},
|
||||
BirthDay: birthDate,
|
||||
|
||||
ID: req.UserID,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// DeleteUser removes a user
|
||||
|
|
@ -637,7 +660,7 @@ func (s *Store) GetUserByEmailPhone(
|
|||
Password: u.Password,
|
||||
Role: domain.Role(u.Role),
|
||||
|
||||
Age: int(u.Age.Int32),
|
||||
AgeGroup: u.AgeGroup.String,
|
||||
EducationLevel: u.EducationLevel.String,
|
||||
Country: u.Country.String,
|
||||
Region: u.Region.String,
|
||||
|
|
@ -690,7 +713,7 @@ func mapCreateUserResult(
|
|||
Role: domain.Role(userRes.Role),
|
||||
Password: password,
|
||||
|
||||
Age: int(userRes.Age.Int32),
|
||||
AgeGroup: userRes.AgeGroup.String,
|
||||
EducationLevel: userRes.EducationLevel.String,
|
||||
Country: userRes.Country.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) {
|
||||
// Generate unique nonce
|
||||
nonce := uuid.NewString()
|
||||
|
|
@ -42,6 +43,7 @@ func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientR
|
|||
NotifyURL = s.cfg.ARIFPAY.B2CNotifyUrl
|
||||
}
|
||||
|
||||
|
||||
// Construct full checkout request
|
||||
checkoutReq := domain.CheckoutSessionRequest{
|
||||
CancelURL: s.cfg.ARIFPAY.CancelUrl,
|
||||
|
|
|
|||
|
|
@ -20,67 +20,136 @@ var (
|
|||
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(
|
||||
ctx context.Context,
|
||||
req LoginRequest,
|
||||
) (LoginSuccess, error) {
|
||||
req domain.LoginRequest,
|
||||
) (domain.LoginSuccess, error) {
|
||||
|
||||
// Try to find user by username first
|
||||
user, err := s.userStore.GetUserByEmailPhone(ctx, req.Email, req.PhoneNumber)
|
||||
if err != nil {
|
||||
// If not found by username, try email or phone lookup using the same identifier
|
||||
return LoginSuccess{}, err
|
||||
return domain.LoginSuccess{}, err
|
||||
}
|
||||
|
||||
if user.Status == domain.UserStatusPending {
|
||||
return LoginSuccess{}, domain.ErrUserNotVerified
|
||||
return domain.LoginSuccess{}, domain.ErrUserNotVerified
|
||||
}
|
||||
|
||||
// Status check instead of Suspended
|
||||
if user.Status == domain.UserStatusSuspended {
|
||||
return LoginSuccess{}, ErrUserSuspended
|
||||
return domain.LoginSuccess{}, ErrUserSuspended
|
||||
}
|
||||
|
||||
// Email + password login
|
||||
if req.Email != "" {
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return domain.LoginSuccess{
|
||||
UserId: user.ID,
|
||||
Role: user.Role,
|
||||
RfToken: refreshToken,
|
||||
}, 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
|
||||
}
|
||||
}
|
||||
|
||||
// Handle existing refresh token
|
||||
// 7. Handle existing refresh token
|
||||
oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
|
||||
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 := s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token); err != nil {
|
||||
return LoginSuccess{}, err
|
||||
return domain.LoginSuccess{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new refresh token
|
||||
// 8. Generate new refresh token
|
||||
refreshToken, err := generateRefreshToken()
|
||||
if err != nil {
|
||||
return LoginSuccess{}, err
|
||||
return domain.LoginSuccess{}, err
|
||||
}
|
||||
|
||||
if err := s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
|
||||
|
|
@ -89,17 +158,22 @@ func (s *Service) Login(
|
|||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
|
||||
}); err != nil {
|
||||
return LoginSuccess{}, err
|
||||
return domain.LoginSuccess{}, err
|
||||
}
|
||||
|
||||
// Return login success payload
|
||||
return LoginSuccess{
|
||||
// 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) {
|
||||
|
||||
token, err := s.tokenStore.GetRefreshToken(ctx, refToken)
|
||||
|
|
|
|||
|
|
@ -19,14 +19,16 @@ type Tokens struct {
|
|||
RefreshToken string
|
||||
}
|
||||
type Service struct {
|
||||
otpStore ports.OtpStore
|
||||
userStore ports.UserStore
|
||||
UserSvc user.Service
|
||||
tokenStore ports.TokenStore
|
||||
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{
|
||||
otpStore: otpStore,
|
||||
userStore: userStore,
|
||||
UserSvc: userSvc,
|
||||
tokenStore: tokenStore,
|
||||
|
|
|
|||
|
|
@ -10,56 +10,6 @@ import (
|
|||
"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(
|
||||
ctx context.Context,
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package user
|
|||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Service) UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error {
|
||||
|
|
@ -23,8 +24,8 @@ func (s *Service) CreateUser(
|
|||
|
||||
// Create the user
|
||||
return s.userStore.CreateUserWithoutOtp(ctx, domain.User{
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
// UserName: req.UserName,
|
||||
Email: req.Email,
|
||||
PhoneNumber: req.PhoneNumber,
|
||||
|
|
@ -33,7 +34,7 @@ func (s *Service) CreateUser(
|
|||
EmailVerified: true, // assuming auto-verified on creation
|
||||
PhoneVerified: true,
|
||||
Status: domain.UserStatusActive,
|
||||
Age: req.Age,
|
||||
AgeGroup: req.AgeGroup,
|
||||
EducationLevel: req.EducationLevel,
|
||||
Country: req.Country,
|
||||
Region: req.Region,
|
||||
|
|
@ -46,10 +47,44 @@ func (s *Service) DeleteUser(ctx context.Context, id int64) error {
|
|||
return s.userStore.DeleteUser(ctx, id)
|
||||
}
|
||||
|
||||
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.Query, &filter.CreatedBefore.Value, &filter.CreatedAfter.Value, int32(filter.PageSize), int32(filter.Page))
|
||||
func (s *Service) GetAllUsers(
|
||||
ctx context.Context,
|
||||
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) {
|
||||
|
||||
return s.userStore.GetUserByID(ctx, id)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const (
|
|||
)
|
||||
|
||||
type Service struct {
|
||||
tokenStore ports.TokenStore
|
||||
userStore ports.UserStore
|
||||
otpStore ports.OtpStore
|
||||
messengerSvc *messenger.Service
|
||||
|
|
@ -19,12 +20,14 @@ type Service struct {
|
|||
}
|
||||
|
||||
func NewService(
|
||||
tokenStore ports.TokenStore,
|
||||
userStore ports.UserStore,
|
||||
otpStore ports.OtpStore,
|
||||
messengerSvc *messenger.Service,
|
||||
cfg *config.Config,
|
||||
) *Service {
|
||||
return &Service{
|
||||
tokenStore: tokenStore,
|
||||
userStore: userStore,
|
||||
otpStore: otpStore,
|
||||
messengerSvc: messengerSvc,
|
||||
|
|
|
|||
|
|
@ -37,14 +37,14 @@ type loginUserRes struct {
|
|||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param login body authentication.LoginRequest true "Login user"
|
||||
// @Param login body domain.LoginRequest 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}/user-login [post]
|
||||
func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||
var req authentication.LoginRequest
|
||||
var req domain.LoginRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
h.mongoLoggerSvc.Info("Failed to parse LoginUser request",
|
||||
zap.Int("status_code", fiber.StatusBadRequest),
|
||||
|
|
@ -180,14 +180,14 @@ type LoginAdminRes struct {
|
|||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param login body authentication.LoginRequest true "Login admin"
|
||||
// @Param login body domain.LoginRequest true "Login admin"
|
||||
// @Success 200 {object} LoginAdminRes
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/v1/{tenant_slug}/admin-login [post]
|
||||
func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
||||
var req authentication.LoginRequest
|
||||
var req domain.LoginRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
|
||||
zap.Int("status_code", fiber.StatusBadRequest),
|
||||
|
|
@ -205,7 +205,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
|||
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 {
|
||||
switch {
|
||||
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
|
||||
// @Accept 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
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/v1/super-login [post]
|
||||
func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
||||
var req authentication.LoginRequest
|
||||
var req domain.LoginRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
|
||||
zap.Int("status_code", fiber.StatusBadRequest),
|
||||
|
|
@ -304,7 +304,7 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
|||
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 {
|
||||
switch {
|
||||
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
|
||||
// @Failure 400 {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 {
|
||||
searchQuery := c.Query("query")
|
||||
searchString := domain.ValidString{
|
||||
|
|
@ -452,38 +452,38 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
|
|||
}
|
||||
|
||||
// Map to profile response to avoid leaking sensitive fields
|
||||
result := make([]domain.UserProfileResponse, len(users))
|
||||
for i, u := range users {
|
||||
result[i] = domain.UserProfileResponse{
|
||||
ID: u.ID,
|
||||
FirstName: u.FirstName,
|
||||
LastName: u.LastName,
|
||||
// UserName: u.UserName,
|
||||
Email: u.Email,
|
||||
PhoneNumber: u.PhoneNumber,
|
||||
Role: u.Role,
|
||||
Age: u.Age,
|
||||
EducationLevel: u.EducationLevel,
|
||||
Country: u.Country,
|
||||
Region: u.Region,
|
||||
NickName: u.NickName,
|
||||
Occupation: u.Occupation,
|
||||
LearningGoal: u.LearningGoal,
|
||||
LanguageGoal: u.LanguageGoal,
|
||||
LanguageChallange: u.LanguageChallange,
|
||||
FavouriteTopic: u.FavouriteTopic,
|
||||
EmailVerified: u.EmailVerified,
|
||||
PhoneVerified: u.PhoneVerified,
|
||||
LastLogin: u.LastLogin,
|
||||
ProfileCompleted: u.ProfileCompleted,
|
||||
ProfilePictureURL: u.ProfilePictureURL,
|
||||
PreferredLanguage: u.PreferredLanguage,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
}
|
||||
}
|
||||
// result := make([]domain.UserProfileResponse, len(users))
|
||||
// for i, u := range users {
|
||||
// result[i] = domain{
|
||||
// ID: u.ID,
|
||||
// FirstName: u.FirstName,
|
||||
// LastName: u.LastName,
|
||||
// Gender: u.Gender,
|
||||
// Email: u.Email,
|
||||
// PhoneNumber: u.PhoneNumber,
|
||||
// Role: u.Role,
|
||||
// Age: u.Age,
|
||||
// EducationLevel: u.EducationLevel,
|
||||
// Country: u.Country,
|
||||
// Region: u.Region,
|
||||
// NickName: u.NickName,
|
||||
// Occupation: u.Occupation,
|
||||
// LearningGoal: u.LearningGoal,
|
||||
// LanguageGoal: u.LanguageGoal,
|
||||
// LanguageChallange: u.LanguageChallange,
|
||||
// FavouriteTopic: u.FavouriteTopic,
|
||||
// EmailVerified: u.EmailVerified,
|
||||
// PhoneVerified: u.PhoneVerified,
|
||||
// LastLogin: u.LastLogin,
|
||||
// ProfileCompleted: u.ProfileCompleted,
|
||||
// ProfilePictureURL: u.ProfilePictureURL,
|
||||
// PreferredLanguage: u.PreferredLanguage,
|
||||
// CreatedAt: u.CreatedAt,
|
||||
// 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
|
||||
|
|
@ -523,7 +523,7 @@ func (h *Handler) VerifyOtp(c *fiber.Ctx) error {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
var errMsg string
|
||||
switch {
|
||||
|
|
@ -555,7 +555,7 @@ func (h *Handler) VerifyOtp(c *fiber.Ctx) error {
|
|||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "OTP verified successfully",
|
||||
Data: nil,
|
||||
Data: loginSuccess,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1214,7 +1214,7 @@ func (h *Handler) GetUserProfile(c *fiber.Ctx) error {
|
|||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Role: user.Role,
|
||||
Age: user.Age,
|
||||
AgeGroup: user.AgeGroup,
|
||||
EducationLevel: user.EducationLevel,
|
||||
Country: user.Country,
|
||||
Region: user.Region,
|
||||
|
|
@ -1303,7 +1303,7 @@ func (h *Handler) AdminProfile(c *fiber.Ctx) error {
|
|||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Role: user.Role,
|
||||
Age: user.Age,
|
||||
AgeGroup: user.AgeGroup,
|
||||
EducationLevel: user.EducationLevel,
|
||||
Country: user.Country,
|
||||
Region: user.Region,
|
||||
|
|
@ -1425,7 +1425,7 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
|
|||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Role: user.Role,
|
||||
Age: user.Age,
|
||||
AgeGroup: user.AgeGroup,
|
||||
EducationLevel: user.EducationLevel,
|
||||
Country: user.Country,
|
||||
Region: user.Region,
|
||||
|
|
@ -1500,14 +1500,22 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error {
|
|||
// }
|
||||
|
||||
res := domain.UserProfileResponse{
|
||||
ID: user.ID,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
ID: user.ID,
|
||||
FirstName: user.FirstName,
|
||||
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,
|
||||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Role: user.Role,
|
||||
Age: user.Age,
|
||||
AgeGroup: user.AgeGroup,
|
||||
EducationLevel: user.EducationLevel,
|
||||
Country: user.Country,
|
||||
Region: user.Region,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user