schema adjustment and profile management fixes
This commit is contained in:
parent
8ed0a5f1c6
commit
d94774c138
|
|
@ -1,40 +1,3 @@
|
||||||
CREATE TABLE assessment_questions (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
question_type VARCHAR(50) NOT NULL, -- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
|
|
||||||
difficulty_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE assessment_question_options (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
|
||||||
option_text TEXT NOT NULL,
|
|
||||||
is_correct BOOLEAN NOT NULL DEFAULT FALSE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE assessment_attempts (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
total_questions INT NOT NULL,
|
|
||||||
correct_answers INT NOT NULL,
|
|
||||||
score_percentage NUMERIC(5,2) NOT NULL,
|
|
||||||
knowledge_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
|
|
||||||
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE assessment_answers (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
|
|
||||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
|
|
||||||
selected_option_id BIGINT REFERENCES assessment_question_options(id),
|
|
||||||
short_answer TEXT,
|
|
||||||
is_correct BOOLEAN NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
first_name VARCHAR(255) NOT NULL,
|
first_name VARCHAR(255) NOT NULL,
|
||||||
|
|
@ -73,6 +36,43 @@ 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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE assessment_questions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
question_type VARCHAR(50) NOT NULL, -- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
|
||||||
|
difficulty_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE assessment_question_options (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
|
||||||
|
option_text TEXT NOT NULL,
|
||||||
|
is_correct BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE assessment_attempts (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
total_questions INT NOT NULL,
|
||||||
|
correct_answers INT NOT NULL,
|
||||||
|
score_percentage NUMERIC(5,2) NOT NULL,
|
||||||
|
knowledge_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
|
||||||
|
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE assessment_answers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
|
||||||
|
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
|
||||||
|
selected_option_id BIGINT REFERENCES assessment_question_options(id),
|
||||||
|
short_answer TEXT,
|
||||||
|
is_correct BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE refresh_tokens (
|
CREATE TABLE refresh_tokens (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,18 @@ SET
|
||||||
used = FALSE,
|
used = FALSE,
|
||||||
used_at = NULL,
|
used_at = NULL,
|
||||||
expires_at = $3
|
expires_at = $3
|
||||||
WHERE
|
WHERE user_name = $1;
|
||||||
user_name = $1
|
|
||||||
AND expires_at <= NOW();
|
|
||||||
|
|
||||||
-- name: CreateOtp :exec
|
-- name: CreateOtp :exec
|
||||||
INSERT INTO otps (user_name, sent_to, medium, otp_for, otp, used, created_at, expires_at)
|
INSERT INTO otps (
|
||||||
VALUES ($1, $2, $3, $4, $5, FALSE, $6, $7);
|
user_name,
|
||||||
|
sent_to,
|
||||||
|
medium,
|
||||||
|
otp_for,
|
||||||
|
otp,
|
||||||
|
expires_at
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6);
|
||||||
|
|
||||||
-- name: GetOtp :one
|
-- name: GetOtp :one
|
||||||
SELECT id, user_name, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
|
SELECT id, user_name, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
|
||||||
|
|
|
||||||
|
|
@ -210,30 +210,29 @@ WHERE (
|
||||||
-- name: UpdateUser :exec
|
-- name: UpdateUser :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
first_name = $1,
|
first_name = COALESCE($1, first_name),
|
||||||
last_name = $2,
|
last_name = COALESCE($2, last_name),
|
||||||
user_name = $3,
|
user_name = COALESCE($3, user_name),
|
||||||
age = $4,
|
knowledge_level = COALESCE($4, knowledge_level),
|
||||||
education_level = $5,
|
age = COALESCE($5, age),
|
||||||
country = $6,
|
education_level = COALESCE($6, education_level),
|
||||||
region = $7,
|
country = COALESCE($7, country),
|
||||||
|
region = COALESCE($8, region),
|
||||||
nick_name = $8,
|
nick_name = COALESCE($9, nick_name),
|
||||||
occupation = $9,
|
occupation = COALESCE($10, occupation),
|
||||||
learning_goal = $10,
|
learning_goal = COALESCE($11, learning_goal),
|
||||||
language_goal = $11,
|
language_goal = COALESCE($12, language_goal),
|
||||||
language_challange = $12,
|
language_challange = COALESCE($13, language_challange),
|
||||||
favoutite_topic = $13,
|
favoutite_topic = COALESCE($14, favoutite_topic),
|
||||||
|
initial_assessment_completed = COALESCE($15, initial_assessment_completed),
|
||||||
initial_assessment_completed = $14,
|
email_verified = COALESCE($16, email_verified),
|
||||||
email_verified = $15,
|
phone_verified = COALESCE($17, phone_verified),
|
||||||
phone_verified = $16,
|
status = COALESCE($18, status),
|
||||||
status = $17,
|
profile_completed = COALESCE($19, profile_completed),
|
||||||
profile_completed = $18,
|
profile_picture_url = COALESCE($20, profile_picture_url),
|
||||||
profile_picture_url = $19,
|
preferred_language = COALESCE($21, preferred_language),
|
||||||
preferred_language = $20,
|
updated_at = CURRENT_TIMESTAMP
|
||||||
updated_at = CURRENT_TIMESTAMP
|
WHERE id = $22;
|
||||||
WHERE id = $21;
|
|
||||||
|
|
||||||
-- name: DeleteUser :exec
|
-- name: DeleteUser :exec
|
||||||
DELETE FROM users
|
DELETE FROM users
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,23 @@ services:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
- ./exports:/exports
|
- ./exports:/exports
|
||||||
|
|
||||||
|
pgadmin:
|
||||||
|
container_name: yimaru-pgadmin
|
||||||
|
image: dpage/pgadmin4:latest
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "5050:80"
|
||||||
|
environment:
|
||||||
|
PGADMIN_DEFAULT_EMAIL: admin@local.dev
|
||||||
|
PGADMIN_DEFAULT_PASSWORD: admin
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- app
|
||||||
|
volumes:
|
||||||
|
- pgadmin_data:/var/lib/pgadmin
|
||||||
|
|
||||||
mongo:
|
mongo:
|
||||||
container_name: yimaru-mongo
|
container_name: yimaru-mongo
|
||||||
image: mongo:7.0.11
|
image: mongo:7.0.11
|
||||||
|
|
@ -53,21 +70,11 @@ services:
|
||||||
"/migrations",
|
"/migrations",
|
||||||
"-database",
|
"-database",
|
||||||
"postgresql://root:secret@postgres:5432/gh?sslmode=disable",
|
"postgresql://root:secret@postgres:5432/gh?sslmode=disable",
|
||||||
"up",
|
"up"
|
||||||
]
|
]
|
||||||
networks:
|
networks:
|
||||||
- app
|
- app
|
||||||
# redis:
|
|
||||||
# image: redis:7-alpine
|
|
||||||
# ports:
|
|
||||||
# - "6379:6379"
|
|
||||||
# networks:
|
|
||||||
# - app
|
|
||||||
# healthcheck:
|
|
||||||
# test: ["CMD", "redis-cli", "ping"]
|
|
||||||
# interval: 10s
|
|
||||||
# timeout: 5s
|
|
||||||
# retries: 5
|
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
|
@ -84,8 +91,6 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- app
|
- app
|
||||||
command: ["/app/bin/web"]
|
command: ["/app/bin/web"]
|
||||||
# volumes:
|
|
||||||
# - "C:/Users/User/Desktop:/host-desktop"
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
build:
|
build:
|
||||||
|
|
@ -105,3 +110,4 @@ networks:
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
mongo_data:
|
mongo_data:
|
||||||
|
pgadmin_data:
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const CreateOtp = `-- name: CreateOtp :exec
|
const CreateOtp = `-- name: CreateOtp :exec
|
||||||
INSERT INTO otps (user_name, sent_to, medium, otp_for, otp, used, created_at, expires_at)
|
INSERT INTO otps (
|
||||||
VALUES ($1, $2, $3, $4, $5, FALSE, $6, $7)
|
user_name,
|
||||||
|
sent_to,
|
||||||
|
medium,
|
||||||
|
otp_for,
|
||||||
|
otp,
|
||||||
|
expires_at
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateOtpParams struct {
|
type CreateOtpParams struct {
|
||||||
|
|
@ -22,7 +29,6 @@ type CreateOtpParams struct {
|
||||||
Medium string `json:"medium"`
|
Medium string `json:"medium"`
|
||||||
OtpFor string `json:"otp_for"`
|
OtpFor string `json:"otp_for"`
|
||||||
Otp string `json:"otp"`
|
Otp string `json:"otp"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,7 +39,6 @@ func (q *Queries) CreateOtp(ctx context.Context, arg CreateOtpParams) error {
|
||||||
arg.Medium,
|
arg.Medium,
|
||||||
arg.OtpFor,
|
arg.OtpFor,
|
||||||
arg.Otp,
|
arg.Otp,
|
||||||
arg.CreatedAt,
|
|
||||||
arg.ExpiresAt,
|
arg.ExpiresAt,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
|
|
@ -100,9 +105,7 @@ SET
|
||||||
used = FALSE,
|
used = FALSE,
|
||||||
used_at = NULL,
|
used_at = NULL,
|
||||||
expires_at = $3
|
expires_at = $3
|
||||||
WHERE
|
WHERE user_name = $1
|
||||||
user_name = $1
|
|
||||||
AND expires_at <= NOW()
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateExpiredOtpParams struct {
|
type UpdateExpiredOtpParams struct {
|
||||||
|
|
|
||||||
|
|
@ -842,36 +842,36 @@ func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams)
|
||||||
const UpdateUser = `-- name: UpdateUser :exec
|
const UpdateUser = `-- name: UpdateUser :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
first_name = $1,
|
first_name = COALESCE($1, first_name),
|
||||||
last_name = $2,
|
last_name = COALESCE($2, last_name),
|
||||||
user_name = $3,
|
user_name = COALESCE($3, user_name),
|
||||||
age = $4,
|
knowledge_level = COALESCE($4, knowledge_level),
|
||||||
education_level = $5,
|
age = COALESCE($5, age),
|
||||||
country = $6,
|
education_level = COALESCE($6, education_level),
|
||||||
region = $7,
|
country = COALESCE($7, country),
|
||||||
|
region = COALESCE($8, region),
|
||||||
nick_name = $8,
|
nick_name = COALESCE($9, nick_name),
|
||||||
occupation = $9,
|
occupation = COALESCE($10, occupation),
|
||||||
learning_goal = $10,
|
learning_goal = COALESCE($11, learning_goal),
|
||||||
language_goal = $11,
|
language_goal = COALESCE($12, language_goal),
|
||||||
language_challange = $12,
|
language_challange = COALESCE($13, language_challange),
|
||||||
favoutite_topic = $13,
|
favoutite_topic = COALESCE($14, favoutite_topic),
|
||||||
|
initial_assessment_completed = COALESCE($15, initial_assessment_completed),
|
||||||
initial_assessment_completed = $14,
|
email_verified = COALESCE($16, email_verified),
|
||||||
email_verified = $15,
|
phone_verified = COALESCE($17, phone_verified),
|
||||||
phone_verified = $16,
|
status = COALESCE($18, status),
|
||||||
status = $17,
|
profile_completed = COALESCE($19, profile_completed),
|
||||||
profile_completed = $18,
|
profile_picture_url = COALESCE($20, profile_picture_url),
|
||||||
profile_picture_url = $19,
|
preferred_language = COALESCE($21, preferred_language),
|
||||||
preferred_language = $20,
|
updated_at = CURRENT_TIMESTAMP
|
||||||
updated_at = CURRENT_TIMESTAMP
|
WHERE id = $22
|
||||||
WHERE id = $21
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateUserParams struct {
|
type UpdateUserParams struct {
|
||||||
FirstName string `json:"first_name"`
|
FirstName string `json:"first_name"`
|
||||||
LastName string `json:"last_name"`
|
LastName string `json:"last_name"`
|
||||||
UserName string `json:"user_name"`
|
UserName string `json:"user_name"`
|
||||||
|
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
|
||||||
Age pgtype.Int4 `json:"age"`
|
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"`
|
||||||
|
|
@ -897,6 +897,7 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
||||||
arg.FirstName,
|
arg.FirstName,
|
||||||
arg.LastName,
|
arg.LastName,
|
||||||
arg.UserName,
|
arg.UserName,
|
||||||
|
arg.KnowledgeLevel,
|
||||||
arg.Age,
|
arg.Age,
|
||||||
arg.EducationLevel,
|
arg.EducationLevel,
|
||||||
arg.Country,
|
arg.Country,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ type User struct {
|
||||||
Region string
|
Region string
|
||||||
|
|
||||||
// Profile fields
|
// Profile fields
|
||||||
|
KnowledgeLevel string
|
||||||
initial_assessment_completed bool
|
initial_assessment_completed bool
|
||||||
NickName string
|
NickName string
|
||||||
Occupation string
|
Occupation string
|
||||||
|
|
@ -185,6 +186,7 @@ type UpdateUserReq struct {
|
||||||
Region ValidString
|
Region ValidString
|
||||||
|
|
||||||
// Profile fields
|
// Profile fields
|
||||||
|
KnowledgeLevel ValidString
|
||||||
NickName ValidString
|
NickName ValidString
|
||||||
Occupation ValidString
|
Occupation ValidString
|
||||||
LearningGoal ValidString
|
LearningGoal ValidString
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserStore interface {
|
type UserStore interface {
|
||||||
|
UpdateUserStatus(ctx context.Context, user domain.UpdateUserReq) error
|
||||||
GetCorrectOptionForQuestion(
|
GetCorrectOptionForQuestion(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
questionID int64,
|
questionID int64,
|
||||||
|
|
@ -67,7 +68,7 @@ type EmailGateway interface {
|
||||||
SendEmailOTP(ctx context.Context, email string, otp string) error
|
SendEmailOTP(ctx context.Context, email string, otp string) error
|
||||||
}
|
}
|
||||||
type OtpStore interface {
|
type OtpStore interface {
|
||||||
UpdateExpiredOtp(ctx context.Context, otp, userName string) error
|
UpdateOtp(ctx context.Context, otp, userName string) error
|
||||||
MarkOtpAsUsed(ctx context.Context, otp domain.Otp) error
|
MarkOtpAsUsed(ctx context.Context, otp domain.Otp) error
|
||||||
CreateOtp(ctx context.Context, otp domain.Otp) error
|
CreateOtp(ctx context.Context, otp domain.Otp) error
|
||||||
GetOtp(ctx context.Context, userName string) (domain.Otp, error)
|
GetOtp(ctx context.Context, userName string) (domain.Otp, error)
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,20 @@ func NewTokenStore(s *Store) ports.TokenStore {
|
||||||
|
|
||||||
// CreateRefreshToken inserts a new refresh token into the database
|
// CreateRefreshToken inserts a new refresh token into the database
|
||||||
func (s *Store) CreateRefreshToken(ctx context.Context, rt domain.RefreshToken) error {
|
func (s *Store) CreateRefreshToken(ctx context.Context, rt domain.RefreshToken) error {
|
||||||
|
rt.ExpiresAt = time.Now().Add(10 * time.Minute)
|
||||||
|
|
||||||
return s.queries.CreateRefreshToken(ctx, dbgen.CreateRefreshTokenParams{
|
return s.queries.CreateRefreshToken(ctx, dbgen.CreateRefreshTokenParams{
|
||||||
UserID: rt.UserID,
|
UserID: rt.UserID,
|
||||||
Token: rt.Token,
|
Token: rt.Token,
|
||||||
ExpiresAt: pgtype.Timestamptz{Time: rt.ExpiresAt},
|
ExpiresAt: pgtype.Timestamptz{
|
||||||
CreatedAt: pgtype.Timestamptz{Time: rt.CreatedAt},
|
Time: rt.ExpiresAt,
|
||||||
Revoked: rt.Revoked,
|
Valid: true,
|
||||||
|
},
|
||||||
|
CreatedAt: pgtype.Timestamptz{
|
||||||
|
Time: time.Now(),
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
Revoked: rt.Revoked,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import (
|
||||||
// Interface for creating new otp store
|
// Interface for creating new otp store
|
||||||
func NewOTPStore(s *Store) ports.OtpStore { return s }
|
func NewOTPStore(s *Store) ports.OtpStore { return s }
|
||||||
|
|
||||||
func (s *Store) UpdateExpiredOtp(ctx context.Context, otp, userName string) error {
|
func (s *Store) UpdateOtp(ctx context.Context, otp, userName string) error {
|
||||||
return s.queries.UpdateExpiredOtp(ctx, dbgen.UpdateExpiredOtpParams{
|
return s.queries.UpdateExpiredOtp(ctx, dbgen.UpdateExpiredOtpParams{
|
||||||
UserName: userName,
|
UserName: userName,
|
||||||
Otp: otp,
|
Otp: otp,
|
||||||
|
|
@ -29,20 +29,18 @@ func (s *Store) UpdateExpiredOtp(ctx context.Context, otp, userName string) erro
|
||||||
|
|
||||||
func (s *Store) CreateOtp(ctx context.Context, otp domain.Otp) error {
|
func (s *Store) CreateOtp(ctx context.Context, otp domain.Otp) error {
|
||||||
return s.queries.CreateOtp(ctx, dbgen.CreateOtpParams{
|
return s.queries.CreateOtp(ctx, dbgen.CreateOtpParams{
|
||||||
SentTo: otp.SentTo,
|
UserName: otp.UserName,
|
||||||
Medium: string(otp.Medium),
|
SentTo: otp.SentTo,
|
||||||
OtpFor: string(otp.For),
|
Medium: string(otp.Medium),
|
||||||
Otp: otp.Otp,
|
OtpFor: string(otp.For),
|
||||||
|
Otp: otp.Otp,
|
||||||
ExpiresAt: pgtype.Timestamptz{
|
ExpiresAt: pgtype.Timestamptz{
|
||||||
Time: otp.ExpiresAt,
|
Time: otp.ExpiresAt,
|
||||||
Valid: true,
|
Valid: true,
|
||||||
},
|
},
|
||||||
CreatedAt: pgtype.Timestamptz{
|
|
||||||
Time: otp.CreatedAt,
|
|
||||||
Valid: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetOtp(ctx context.Context, userName string) (domain.Otp, error) {
|
func (s *Store) GetOtp(ctx context.Context, userName string) (domain.Otp, error) {
|
||||||
row, err := s.queries.GetOtp(ctx, userName)
|
row, err := s.queries.GetOtp(ctx, userName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -54,6 +52,7 @@ func (s *Store) GetOtp(ctx context.Context, userName string) (domain.Otp, error)
|
||||||
}
|
}
|
||||||
return domain.Otp{
|
return domain.Otp{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
|
UserName: row.UserName,
|
||||||
SentTo: row.SentTo,
|
SentTo: row.SentTo,
|
||||||
Medium: domain.OtpMedium(row.Medium),
|
Medium: domain.OtpMedium(row.Medium),
|
||||||
For: domain.OtpFor(row.OtpFor),
|
For: domain.OtpFor(row.OtpFor),
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,13 @@ func (s *Store) IsUserNameUnique(ctx context.Context, userName string) (bool, er
|
||||||
return isUnique, nil
|
return isUnique, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateUserStatus(ctx context.Context, user domain.UpdateUserReq) error {
|
||||||
|
return s.queries.UpdateUserStatus(ctx, dbgen.UpdateUserStatusParams{
|
||||||
|
Status: user.Status.Value,
|
||||||
|
ID: user.UserID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) CreateUserWithoutOtp(
|
func (s *Store) CreateUserWithoutOtp(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
user domain.User,
|
user domain.User,
|
||||||
|
|
|
||||||
|
|
@ -48,14 +48,14 @@ func (s *Service) ResendOtp(
|
||||||
return fmt.Errorf("invalid otp medium: %s", otp.Medium)
|
return fmt.Errorf("invalid otp medium: %s", otp.Medium)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.otpStore.UpdateExpiredOtp(ctx, otp.Otp, userName); err != nil {
|
if err := s.otpStore.UpdateOtp(ctx, otpCode, userName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.SMSProvider) error {
|
func (s *Service) SendOtp(ctx context.Context, userName string, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.SMSProvider) error {
|
||||||
otpCode := helpers.GenerateOTP()
|
otpCode := helpers.GenerateOTP()
|
||||||
|
|
||||||
message := fmt.Sprintf("Welcome to Yimaru Online Learning Platform, your OTP is %s please don't share with anyone.", otpCode)
|
message := fmt.Sprintf("Welcome to Yimaru Online Learning Platform, your OTP is %s please don't share with anyone.", otpCode)
|
||||||
|
|
@ -82,6 +82,7 @@ func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpF
|
||||||
}
|
}
|
||||||
|
|
||||||
otp := domain.Otp{
|
otp := domain.Otp{
|
||||||
|
UserName: userName,
|
||||||
SentTo: sentTo,
|
SentTo: sentTo,
|
||||||
Medium: medium,
|
Medium: medium,
|
||||||
For: otpFor,
|
For: otpFor,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,21 @@ func (s *Service) VerifyOtp(ctx context.Context, userName string, otpCode string
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user, err := s.userStore.GetUserByUserName(ctx, userName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newUser := domain.UpdateUserReq{
|
||||||
|
UserID: user.ID,
|
||||||
|
Status: domain.ValidString{
|
||||||
|
Value: string(domain.UserStatusActive),
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s.userStore.UpdateUserStatus(ctx, newUser)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,7 +74,7 @@ func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium,
|
||||||
}
|
}
|
||||||
|
|
||||||
// send otp based on the medium
|
// send otp based on the medium
|
||||||
return s.SendOtp(ctx, sentTo, domain.OtpRegister, medium, provider)
|
return s.SendOtp(ctx, "", sentTo, domain.OtpRegister, medium, provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) {
|
func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) {
|
||||||
|
|
@ -94,7 +109,7 @@ func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterU
|
||||||
PhoneNumber: registerReq.PhoneNumber,
|
PhoneNumber: registerReq.PhoneNumber,
|
||||||
Password: hashedPassword,
|
Password: hashedPassword,
|
||||||
Role: domain.RoleStudent,
|
Role: domain.RoleStudent,
|
||||||
EmailVerified: false, // verification pending via OTP
|
EmailVerified: false,
|
||||||
PhoneVerified: false,
|
PhoneVerified: false,
|
||||||
EducationLevel: registerReq.EducationLevel,
|
EducationLevel: registerReq.EducationLevel,
|
||||||
Age: registerReq.Age,
|
Age: registerReq.Age,
|
||||||
|
|
@ -103,7 +118,17 @@ func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterU
|
||||||
Status: domain.UserStatusPending,
|
Status: domain.UserStatusPending,
|
||||||
ProfileCompleted: false,
|
ProfileCompleted: false,
|
||||||
PreferredLanguage: registerReq.PreferredLanguage,
|
PreferredLanguage: registerReq.PreferredLanguage,
|
||||||
CreatedAt: time.Now(),
|
|
||||||
|
// Optional fields
|
||||||
|
NickName: registerReq.NickName,
|
||||||
|
Occupation: registerReq.Occupation,
|
||||||
|
LearningGoal: registerReq.LearningGoal,
|
||||||
|
LanguageGoal: registerReq.LanguageGoal,
|
||||||
|
LanguageChallange: registerReq.LanguageChallange,
|
||||||
|
FavoutiteTopic: registerReq.FavoutiteTopic,
|
||||||
|
// ProfilePictureURL: registerReq.ProfilePictureURL,
|
||||||
|
|
||||||
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var sentTo string
|
var sentTo string
|
||||||
|
|
@ -115,7 +140,7 @@ func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterU
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send OTP to the user (email/SMS)
|
// Send OTP to the user (email/SMS)
|
||||||
if err := s.SendOtp(ctx, sentTo, domain.OtpRegister, registerReq.OtpMedium, domain.TwilioSms); err != nil {
|
if err := s.SendOtp(ctx, registerReq.UserName, sentTo, domain.OtpRegister, registerReq.OtpMedium, domain.TwilioSms); err != nil {
|
||||||
return domain.User{}, err
|
return domain.User{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error {
|
func (s *Service) SendResetCode(ctx context.Context, userName string, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error {
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
// check if user exists
|
// check if user exists
|
||||||
|
|
@ -22,7 +22,7 @@ func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, se
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.SendOtp(ctx, sentTo, domain.OtpReset, medium, provider)
|
return s.SendOtp(ctx, userName, sentTo, domain.OtpReset, medium, provider)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,27 @@ func (s *Service) SearchUserByNameOrPhone(ctx context.Context, searchString stri
|
||||||
return s.userStore.SearchUserByNameOrPhone(ctx, searchString, roleStr)
|
return s.userStore.SearchUserByNameOrPhone(ctx, searchString, roleStr)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UpdateUser(ctx context.Context, req domain.UpdateUserReq) error {
|
func (s *Service) UpdateUser(ctx context.Context, req domain.UpdateUserReq) error {
|
||||||
newUser := domain.User{
|
newUser := domain.User{
|
||||||
ID: req.UserID,
|
ID: req.UserID,
|
||||||
FirstName: req.FirstName.Value,
|
FirstName: req.FirstName.Value,
|
||||||
LastName: req.LastName.Value,
|
LastName: req.LastName.Value,
|
||||||
UserName: req.UserName.Value,
|
KnowledgeLevel: req.KnowledgeLevel.Value,
|
||||||
Age: req.Age.Value,
|
UserName: req.UserName.Value,
|
||||||
EducationLevel: req.EducationLevel.Value,
|
Age: req.Age.Value,
|
||||||
Country: req.Country.Value,
|
EducationLevel: req.EducationLevel.Value,
|
||||||
Region: req.Region.Value,
|
Country: req.Country.Value,
|
||||||
|
Region: req.Region.Value,
|
||||||
|
Status: domain.UserStatus(req.Status.Value),
|
||||||
|
NickName: req.NickName.Value,
|
||||||
|
Occupation: req.Occupation.Value,
|
||||||
|
LearningGoal: req.LearningGoal.Value,
|
||||||
|
LanguageGoal: req.LanguageGoal.Value,
|
||||||
|
LanguageChallange: req.LanguageChallange.Value,
|
||||||
|
FavoutiteTopic: req.FavoutiteTopic.Value,
|
||||||
|
PreferredLanguage: req.PreferredLanguage.Value,
|
||||||
|
ProfilePictureURL: req.ProfilePictureURL.Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user in the store
|
// Update user in the store
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,6 @@ type loginUserRes struct {
|
||||||
// @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 {
|
||||||
// OrganizationID := c.Locals("company_id").(domain.ValidInt64)
|
|
||||||
// if !OrganizationID.Valid {
|
|
||||||
// h.BadRequestLogger().Error("invalid company id")
|
|
||||||
// return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
|
|
||||||
// }
|
|
||||||
var req loginUserReq
|
var req loginUserReq
|
||||||
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",
|
||||||
|
|
@ -51,7 +46,10 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body"+err.Error())
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to login",
|
||||||
|
Error: "Invalid request body: " + err.Error(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
||||||
|
|
@ -59,11 +57,13 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
for field, msg := range valErrs {
|
for field, msg := range valErrs {
|
||||||
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
|
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
|
||||||
}
|
}
|
||||||
return fiber.NewError(fiber.StatusBadRequest, errMsg)
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to login",
|
||||||
|
Error: errMsg,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
successRes, err := h.authSvc.Login(c.Context(), req.UserName, req.Password)
|
successRes, err := h.authSvc.Login(c.Context(), req.UserName, req.Password)
|
||||||
|
|
||||||
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):
|
||||||
|
|
@ -73,7 +73,10 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials")
|
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to login",
|
||||||
|
Error: fmt.Sprintf("Invalid credentials: %v", err),
|
||||||
|
})
|
||||||
case errors.Is(err, authentication.ErrUserSuspended):
|
case errors.Is(err, authentication.ErrUserSuspended):
|
||||||
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
|
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
|
||||||
zap.Int("status_code", fiber.StatusUnauthorized),
|
zap.Int("status_code", fiber.StatusUnauthorized),
|
||||||
|
|
@ -81,14 +84,20 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "User login has been locked")
|
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to login",
|
||||||
|
Error: fmt.Sprintf("User login has been locked: %v", err),
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
h.mongoLoggerSvc.Error("Login failed",
|
h.mongoLoggerSvc.Error("Login failed",
|
||||||
zap.Int("status_code", fiber.StatusInternalServerError),
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to login",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,10 +106,12 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
zap.Int("status_code", fiber.StatusForbidden),
|
zap.Int("status_code", fiber.StatusForbidden),
|
||||||
zap.String("role", string(successRes.Role)),
|
zap.String("role", string(successRes.Role)),
|
||||||
zap.String("user_name", req.UserName),
|
zap.String("user_name", req.UserName),
|
||||||
zap.Error(err),
|
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Only users are allowed to login ")
|
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to login",
|
||||||
|
Error: "Only users are allowed to login",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken, err := jwtutil.CreateJwt(
|
accessToken, err := jwtutil.CreateJwt(
|
||||||
|
|
@ -116,7 +127,10 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token")
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to login",
|
||||||
|
Error: "Failed to generate access token",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
res := loginUserRes{
|
res := loginUserRes{
|
||||||
|
|
@ -132,7 +146,10 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil)
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
|
Message: "Login successful",
|
||||||
|
Data: res,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// loginAdminReq represents the request body for the LoginAdmin endpoint.
|
// loginAdminReq represents the request body for the LoginAdmin endpoint.
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,79 @@ import (
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UpdateUser godoc
|
||||||
|
// @Summary Update user profile
|
||||||
|
// @Description Updates user profile information (partial updates supported)
|
||||||
|
// @Tags user
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param user_id path int true "User ID"
|
||||||
|
// @Param body body domain.UpdateUserReq true "Update user payload"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/{tenant_slug}/user [put]
|
||||||
|
func (h *Handler) UpdateUser(c *fiber.Ctx) error {
|
||||||
|
// Extract user ID from context
|
||||||
|
userIDStr, ok := c.Locals("user_id").(string)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid user context",
|
||||||
|
Error: "User ID not found in request context",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := strconv.ParseInt(userIDStr, 10, 64)
|
||||||
|
if err != nil || userID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid user ID",
|
||||||
|
Error: "User ID must be a positive integer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var req domain.UpdateUserReq
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid request body",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce user identity
|
||||||
|
req.UserID = userID
|
||||||
|
|
||||||
|
// Optional: lightweight validation (example)
|
||||||
|
// if req.Status.IsSet() {
|
||||||
|
// if !domain.(req.Status.Value) {
|
||||||
|
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
// Message: "Invalid status value",
|
||||||
|
// Error: "Unsupported user status",
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Call service
|
||||||
|
if err := h.userSvc.UpdateUser(c.Context(), req); err != nil {
|
||||||
|
if errors.Is(err, authentication.ErrUserNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "User not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to update user",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
|
Message: "User updated successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateUserKnowledgeLevel godoc
|
// UpdateUserKnowledgeLevel godoc
|
||||||
// @Summary Update user's knowledge level
|
// @Summary Update user's knowledge level
|
||||||
// @Description Updates the knowledge level of the specified user after initial assessment
|
// @Description Updates the knowledge level of the specified user after initial assessment
|
||||||
|
|
@ -157,8 +230,10 @@ func (h *Handler) ResendOtp(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
Message: "OTP resent successfully",
|
Message: "OTP resent successfully",
|
||||||
Data: nil,
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
Data: nil,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -560,6 +635,13 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
|
||||||
Country: req.Country,
|
Country: req.Country,
|
||||||
Region: req.Region,
|
Region: req.Region,
|
||||||
PreferredLanguage: req.PreferredLanguage,
|
PreferredLanguage: req.PreferredLanguage,
|
||||||
|
|
||||||
|
NickName: req.NickName,
|
||||||
|
Occupation: req.Occupation,
|
||||||
|
LearningGoal: req.LearningGoal,
|
||||||
|
LanguageGoal: req.LanguageGoal,
|
||||||
|
LanguageChallange: req.LanguageChallange,
|
||||||
|
FavoutiteTopic: req.FavoutiteTopic,
|
||||||
}
|
}
|
||||||
|
|
||||||
medium, err := getMedium(req.Email, req.PhoneNumber)
|
medium, err := getMedium(req.Email, req.PhoneNumber)
|
||||||
|
|
@ -600,6 +682,30 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MapRegisterReqToUser(req domain.RegisterUserReq) domain.User {
|
||||||
|
return domain.User{
|
||||||
|
FirstName: req.FirstName,
|
||||||
|
LastName: req.LastName,
|
||||||
|
UserName: req.UserName,
|
||||||
|
Email: req.Email,
|
||||||
|
PhoneNumber: req.PhoneNumber,
|
||||||
|
Password: []byte(req.Password), // or hashed password
|
||||||
|
Role: domain.Role(req.Role),
|
||||||
|
Age: req.Age,
|
||||||
|
EducationLevel: req.EducationLevel,
|
||||||
|
Country: req.Country,
|
||||||
|
Region: req.Region,
|
||||||
|
PreferredLanguage: req.PreferredLanguage,
|
||||||
|
|
||||||
|
NickName: req.NickName,
|
||||||
|
Occupation: req.Occupation,
|
||||||
|
LearningGoal: req.LearningGoal,
|
||||||
|
LanguageGoal: req.LanguageGoal,
|
||||||
|
LanguageChallange: req.LanguageChallange,
|
||||||
|
FavoutiteTopic: req.FavoutiteTopic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ResetCodeReq struct {
|
type ResetCodeReq struct {
|
||||||
Email string `json:"email" example:"john.doe@example.com"`
|
Email string `json:"email" example:"john.doe@example.com"`
|
||||||
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
|
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
|
||||||
|
|
@ -654,7 +760,7 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
|
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, domain.AfroMessage); err != nil {
|
if err := h.userSvc.SendResetCode(c.Context(), "", medium, sentTo, domain.AfroMessage); err != nil {
|
||||||
h.mongoLoggerSvc.Error("Failed to send reset code",
|
h.mongoLoggerSvc.Error("Failed to send reset code",
|
||||||
zap.String("medium", string(medium)),
|
zap.String("medium", string(medium)),
|
||||||
zap.String("sentTo", string(sentTo)),
|
zap.String("sentTo", string(sentTo)),
|
||||||
|
|
@ -721,7 +827,7 @@ func (h *Handler) SendTenantResetCode(c *fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
|
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, domain.AfroMessage); err != nil {
|
if err := h.userSvc.SendResetCode(c.Context(), "", medium, sentTo, domain.AfroMessage); err != nil {
|
||||||
h.mongoLoggerSvc.Error("Failed to send reset code",
|
h.mongoLoggerSvc.Error("Failed to send reset code",
|
||||||
zap.String("medium", string(medium)),
|
zap.String("medium", string(medium)),
|
||||||
zap.String("sentTo", string(sentTo)),
|
zap.String("sentTo", string(sentTo)),
|
||||||
|
|
|
||||||
|
|
@ -70,24 +70,24 @@ func (a *App) authMiddleware(c *fiber.Ctx) error {
|
||||||
|
|
||||||
}
|
}
|
||||||
// Asserting to make sure that only the super admin can have a nil company ID
|
// Asserting to make sure that only the super admin can have a nil company ID
|
||||||
if claim.Role != domain.RoleSuperAdmin && !claim.CompanyID.Valid {
|
// if claim.Role != domain.RoleSuperAdmin && !claim.CompanyID.Valid {
|
||||||
a.mongoLoggerSvc.Error("Company Role without Company ID",
|
// a.mongoLoggerSvc.Error("Company Role without Company ID",
|
||||||
zap.Int64("userID", claim.UserId),
|
// zap.Int64("userID", claim.UserId),
|
||||||
zap.Int("status_code", fiber.StatusInternalServerError),
|
// zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
zap.Error(err),
|
// zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
// zap.Time("timestamp", time.Now()),
|
||||||
)
|
// )
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Company Role without Company ID")
|
// return fiber.NewError(fiber.StatusInternalServerError, "Company Role without Company ID")
|
||||||
}
|
// }
|
||||||
c.Locals("user_id", claim.UserId)
|
c.Locals("user_id", claim.UserId)
|
||||||
c.Locals("role", claim.Role)
|
c.Locals("role", claim.Role)
|
||||||
c.Locals("company_id", domain.ValidInt64{
|
// c.Locals("company_id", domain.ValidInt64{
|
||||||
Value: claim.CompanyID.Value,
|
// Value: claim.CompanyID.Value,
|
||||||
Valid: claim.CompanyID.Valid,
|
// Valid: claim.CompanyID.Valid,
|
||||||
})
|
// })
|
||||||
c.Locals("refresh_token", refreshToken)
|
c.Locals("refresh_token", refreshToken)
|
||||||
|
|
||||||
var branchID domain.ValidInt64
|
// var branchID domain.ValidInt64
|
||||||
|
|
||||||
if claim.Role == domain.RoleAdmin {
|
if claim.Role == domain.RoleAdmin {
|
||||||
// branch, err := a.branchSvc.GetBranchByCashier(c.Context(), claim.UserId)
|
// branch, err := a.branchSvc.GetBranchByCashier(c.Context(), claim.UserId)
|
||||||
|
|
@ -107,7 +107,7 @@ func (a *App) authMiddleware(c *fiber.Ctx) error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Locals("branch_id", branchID)
|
// c.Locals("branch_id", branchID)
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,20 +126,20 @@ func (a *App) SuperAdminOnly(c *fiber.Ctx) error {
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) CompanyOnly(c *fiber.Ctx) error {
|
// func (a *App) CompanyOnly(c *fiber.Ctx) error {
|
||||||
userID := c.Locals("user_id").(int64)
|
// userID := c.Locals("user_id").(int64)
|
||||||
userRole := c.Locals("role").(domain.Role)
|
// userRole := c.Locals("role").(domain.Role)
|
||||||
if userRole == domain.RoleStudent {
|
// if userRole == domain.RoleStudent {
|
||||||
a.mongoLoggerSvc.Warn("Attempt to access restricted CompanyOnly route",
|
// a.mongoLoggerSvc.Warn("Attempt to access restricted CompanyOnly route",
|
||||||
zap.Int64("userID", userID),
|
// zap.Int64("userID", userID),
|
||||||
zap.String("role", string(userRole)),
|
// zap.String("role", string(userRole)),
|
||||||
zap.Int("status_code", fiber.StatusForbidden),
|
// zap.Int("status_code", fiber.StatusForbidden),
|
||||||
zap.Time("timestamp", time.Now()),
|
// zap.Time("timestamp", time.Now()),
|
||||||
)
|
// )
|
||||||
return fiber.NewError(fiber.StatusForbidden, "This route is restricted")
|
// return fiber.NewError(fiber.StatusForbidden, "This route is restricted")
|
||||||
}
|
// }
|
||||||
return c.Next()
|
// return c.Next()
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (a *App) OnlyAdminAndAbove(c *fiber.Ctx) error {
|
func (a *App) OnlyAdminAndAbove(c *fiber.Ctx) error {
|
||||||
userID := c.Locals("user_id").(int64)
|
userID := c.Locals("user_id").(int64)
|
||||||
|
|
|
||||||
|
|
@ -42,31 +42,31 @@ func (a *App) initAppRoutes() {
|
||||||
|
|
||||||
// Groups
|
// Groups
|
||||||
groupV1 := a.fiber.Group("/api/v1")
|
groupV1 := a.fiber.Group("/api/v1")
|
||||||
tenant := groupV1.Group("/tenant/:tenant_slug", a.TenantMiddleware)
|
// tenant := groupV1.Group("/tenant/:tenant_slug", a.TenantMiddleware)
|
||||||
tenant.Get("/test", a.authMiddleware, a.authMiddleware, func(c *fiber.Ctx) error {
|
// groupV1.Get("/test", a.authMiddleware, a.authMiddleware, func(c *fiber.Ctx) error {
|
||||||
fmt.Printf("\nTest Route %v\n", c.Route().Path)
|
// fmt.Printf("\nTest Route %v\n", c.Route().Path)
|
||||||
companyID := c.Locals("company_id").(domain.ValidInt64)
|
// companyID := c.Locals("company_id").(domain.ValidInt64)
|
||||||
if !companyID.Valid {
|
// if !companyID.Valid {
|
||||||
h.BadRequestLogger().Error("invalid company id")
|
// h.BadRequestLogger().Error("invalid company id")
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
|
// return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
|
||||||
}
|
// }
|
||||||
|
|
||||||
fmt.Printf("In the tenant auth test \n")
|
// fmt.Printf("In the tenant auth test \n")
|
||||||
return c.JSON(fiber.Map{
|
// return c.JSON(fiber.Map{
|
||||||
"message": "Is is fine",
|
// "message": "Is is fine",
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
tenant.Get("/", func(c *fiber.Ctx) error {
|
// groupV1.Get("/", func(c *fiber.Ctx) error {
|
||||||
fmt.Printf("\nTenant Route %v\n", c.Route().Path)
|
// fmt.Printf("\nTenant Route %v\n", c.Route().Path)
|
||||||
companyID := c.Locals("company_id").(domain.ValidInt64)
|
// companyID := c.Locals("company_id").(domain.ValidInt64)
|
||||||
if !companyID.Valid {
|
// if !companyID.Valid {
|
||||||
h.BadRequestLogger().Error("invalid company id")
|
// h.BadRequestLogger().Error("invalid company id")
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
|
// return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
|
||||||
}
|
// }
|
||||||
return c.JSON(fiber.Map{
|
// return c.JSON(fiber.Map{
|
||||||
"message": "Company Tenant Active",
|
// "message": "Company Tenant Active",
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
|
|
||||||
// Get S
|
// Get S
|
||||||
groupV1.Get("/tenant", a.authMiddleware, h.GetTenantSlugByToken)
|
groupV1.Get("/tenant", a.authMiddleware, h.GetTenantSlugByToken)
|
||||||
|
|
@ -84,7 +84,7 @@ func (a *App) initAppRoutes() {
|
||||||
//assessment Routes
|
//assessment Routes
|
||||||
groupV1.Post("/assessment/questions", h.CreateAssessmentQuestion)
|
groupV1.Post("/assessment/questions", h.CreateAssessmentQuestion)
|
||||||
groupV1.Get("/assessment/questions", h.GetActiveAssessmentQuestions)
|
groupV1.Get("/assessment/questions", h.GetActiveAssessmentQuestions)
|
||||||
tenant.Post("/assessment/submit", a.authMiddleware, h.SubmitAssessment)
|
groupV1.Post("/assessment/submit", a.authMiddleware, h.SubmitAssessment)
|
||||||
|
|
||||||
// Course Management Routes
|
// Course Management Routes
|
||||||
groupV1.Post("/course-categories", h.CreateCourseCategory)
|
groupV1.Post("/course-categories", h.CreateCourseCategory)
|
||||||
|
|
@ -114,8 +114,8 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Post("/levels", h.CreateLevel)
|
groupV1.Post("/levels", h.CreateLevel)
|
||||||
|
|
||||||
// Auth Routes
|
// Auth Routes
|
||||||
tenant.Post("/auth/customer-login", h.LoginUser)
|
groupV1.Post("/auth/customer-login", h.LoginUser)
|
||||||
tenant.Post("/auth/admin-login", h.LoginAdmin)
|
groupV1.Post("/auth/admin-login", h.LoginAdmin)
|
||||||
groupV1.Post("/auth/super-login", h.LoginSuper)
|
groupV1.Post("/auth/super-login", h.LoginSuper)
|
||||||
groupV1.Post("/auth/refresh", h.RefreshToken)
|
groupV1.Post("/auth/refresh", h.RefreshToken)
|
||||||
groupV1.Post("/auth/logout", a.authMiddleware, h.LogOutuser)
|
groupV1.Post("/auth/logout", a.authMiddleware, h.LogOutuser)
|
||||||
|
|
@ -156,7 +156,8 @@ func (a *App) initAppRoutes() {
|
||||||
// groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler
|
// groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler
|
||||||
|
|
||||||
// User Routes
|
// User Routes
|
||||||
tenant.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel)
|
groupV1.Put("/user", a.authMiddleware, h.UpdateUser)
|
||||||
|
groupV1.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel)
|
||||||
groupV1.Get("/user/:user_name/is-unique", h.CheckUserNameUnique)
|
groupV1.Get("/user/:user_name/is-unique", h.CheckUserNameUnique)
|
||||||
groupV1.Get("/user/:user_name/is-pending", h.CheckUserPending)
|
groupV1.Get("/user/:user_name/is-pending", h.CheckUserPending)
|
||||||
groupV1.Post("/user/resetPassword", h.ResetPassword)
|
groupV1.Post("/user/resetPassword", h.ResetPassword)
|
||||||
|
|
@ -164,15 +165,15 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Post("/user/verify-otp", h.VerifyOtp)
|
groupV1.Post("/user/verify-otp", h.VerifyOtp)
|
||||||
groupV1.Post("/user/resend-otp", h.ResendOtp)
|
groupV1.Post("/user/resend-otp", h.ResendOtp)
|
||||||
|
|
||||||
tenant.Post("/user/resetPassword", h.ResetTenantPassword)
|
groupV1.Post("/user/resetPassword", h.ResetTenantPassword)
|
||||||
tenant.Post("/user/sendResetCode", h.SendTenantResetCode)
|
groupV1.Post("/user/sendResetCode", h.SendTenantResetCode)
|
||||||
tenant.Post("/user/register", h.RegisterUser)
|
groupV1.Post("/user/register", h.RegisterUser)
|
||||||
tenant.Post("/user/sendRegisterCode", h.SendRegisterCode)
|
groupV1.Post("/user/sendRegisterCode", h.SendRegisterCode)
|
||||||
tenant.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist)
|
groupV1.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist)
|
||||||
|
|
||||||
groupV1.Get("/user/admin-profile", a.authMiddleware, h.AdminProfile)
|
groupV1.Get("/user/admin-profile", a.authMiddleware, h.AdminProfile)
|
||||||
|
|
||||||
tenant.Get("/user/user-profile", a.authMiddleware, h.GetUserProfile)
|
groupV1.Get("/user/user-profile", a.authMiddleware, h.GetUserProfile)
|
||||||
|
|
||||||
groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID)
|
groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID)
|
||||||
groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser)
|
groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user