updated the authentication method from username to email/phone_numner
This commit is contained in:
parent
3afb2ec878
commit
7309a2bc83
12
cmd/main.go
12
cmd/main.go
|
|
@ -90,17 +90,19 @@ func main() {
|
||||||
// repository.NewBranchStatStore(store),
|
// repository.NewBranchStatStore(store),
|
||||||
// )
|
// )
|
||||||
|
|
||||||
authSvc := authentication.NewService(
|
|
||||||
repository.NewUserStore(store),
|
|
||||||
repository.NewTokenStore(store),
|
|
||||||
cfg.RefreshExpiry,
|
|
||||||
)
|
|
||||||
userSvc := user.NewService(
|
userSvc := user.NewService(
|
||||||
repository.NewUserStore(store),
|
repository.NewUserStore(store),
|
||||||
repository.NewOTPStore(store),
|
repository.NewOTPStore(store),
|
||||||
messengerSvc,
|
messengerSvc,
|
||||||
cfg,
|
cfg,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
authSvc := authentication.NewService(
|
||||||
|
repository.NewUserStore(store),
|
||||||
|
*userSvc,
|
||||||
|
repository.NewTokenStore(store),
|
||||||
|
cfg.RefreshExpiry,
|
||||||
|
)
|
||||||
// leagueSvc := league.New(repository.NewLeagueStore(store))
|
// leagueSvc := league.New(repository.NewLeagueStore(store))
|
||||||
// eventSvc := event.New(
|
// eventSvc := event.New(
|
||||||
// cfg.Bet365Token,
|
// cfg.Bet365Token,
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,6 @@ CREATE TABLE assessment_attempts (
|
||||||
|
|
||||||
CREATE TABLE assessment_answers (
|
CREATE TABLE assessment_answers (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
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),
|
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
|
||||||
selected_option_id BIGINT REFERENCES assessment_question_options(id),
|
selected_option_id BIGINT REFERENCES assessment_question_options(id),
|
||||||
short_answer TEXT,
|
short_answer TEXT,
|
||||||
|
|
|
||||||
|
|
@ -22,21 +22,21 @@ RETURNING
|
||||||
knowledge_level,
|
knowledge_level,
|
||||||
completed_at;
|
completed_at;
|
||||||
|
|
||||||
-- name: CreateAssessmentAnswer :exec
|
-- -- name: CreateAssessmentAnswer :exec
|
||||||
INSERT INTO assessment_answers (
|
-- INSERT INTO assessment_answers (
|
||||||
attempt_id,
|
-- attempt_id,
|
||||||
question_id,
|
-- question_id,
|
||||||
selected_option_id,
|
-- selected_option_id,
|
||||||
short_answer,
|
-- short_answer,
|
||||||
is_correct
|
-- is_correct
|
||||||
)
|
-- )
|
||||||
VALUES (
|
-- VALUES (
|
||||||
$1, -- attempt_id
|
-- $1, -- attempt_id
|
||||||
$2, -- question_id
|
-- $2, -- question_id
|
||||||
$3, -- selected_option_id
|
-- $3, -- selected_option_id
|
||||||
$4, -- short_answer
|
-- $4, -- short_answer
|
||||||
$5 -- is_correct
|
-- $5 -- is_correct
|
||||||
);
|
-- );
|
||||||
|
|
||||||
-- name: GetAssessmentOptionByID :one
|
-- name: GetAssessmentOptionByID :one
|
||||||
SELECT
|
SELECT
|
||||||
|
|
|
||||||
|
|
@ -120,14 +120,12 @@ SELECT
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
|
||||||
nick_name,
|
nick_name,
|
||||||
occupation,
|
occupation,
|
||||||
learning_goal,
|
learning_goal,
|
||||||
language_goal,
|
language_goal,
|
||||||
language_challange,
|
language_challange,
|
||||||
favoutite_topic,
|
favoutite_topic,
|
||||||
|
|
||||||
initial_assessment_completed,
|
initial_assessment_completed,
|
||||||
profile_picture_url,
|
profile_picture_url,
|
||||||
preferred_language,
|
preferred_language,
|
||||||
|
|
@ -135,30 +133,18 @@ SELECT
|
||||||
phone_verified,
|
phone_verified,
|
||||||
status,
|
status,
|
||||||
profile_completed,
|
profile_completed,
|
||||||
preferred_language,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM users
|
FROM users
|
||||||
WHERE (
|
WHERE ($1 IS NULL OR role = $1)
|
||||||
role = $1 OR $1 IS NULL
|
AND ($2 IS NULL OR first_name ILIKE '%' || $2 || '%'
|
||||||
)
|
OR last_name ILIKE '%' || $2 || '%'
|
||||||
AND (
|
OR phone_number ILIKE '%' || $2 || '%'
|
||||||
first_name ILIKE '%' || sqlc.narg('query') || '%'
|
OR email ILIKE '%' || $2 || '%')
|
||||||
OR last_name ILIKE '%' || sqlc.narg('query') || '%'
|
AND ($3 IS NULL OR created_at >= $3)
|
||||||
OR phone_number ILIKE '%' || sqlc.narg('query') || '%'
|
AND ($4 IS NULL OR created_at <= $4)
|
||||||
OR email ILIKE '%' || sqlc.narg('query') || '%'
|
LIMIT $5
|
||||||
OR sqlc.narg('query') IS NULL
|
OFFSET $6;
|
||||||
)
|
|
||||||
AND (
|
|
||||||
created_at >= sqlc.narg('created_after')
|
|
||||||
OR sqlc.narg('created_after') IS NULL
|
|
||||||
)
|
|
||||||
AND (
|
|
||||||
created_at <= sqlc.narg('created_before')
|
|
||||||
OR sqlc.narg('created_before') IS NULL
|
|
||||||
)
|
|
||||||
LIMIT sqlc.narg('limit')
|
|
||||||
OFFSET sqlc.narg('offset');
|
|
||||||
|
|
||||||
-- name: GetTotalUsers :one
|
-- name: GetTotalUsers :one
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
|
|
|
||||||
384
docs/docs.go
384
docs/docs.go
|
|
@ -1504,7 +1504,7 @@ const docTemplate = `{
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/handlers.loginAdminReq"
|
"$ref": "#/definitions/authentication.LoginRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -2024,7 +2024,7 @@ const docTemplate = `{
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/handlers.loginAdminReq"
|
"$ref": "#/definitions/authentication.LoginRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -2056,65 +2056,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/{tenant_slug}/assessment/submit": {
|
|
||||||
"post": {
|
|
||||||
"description": "Evaluates user responses, calculates knowledge level, updates user profile, and sends notification",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"assessment"
|
|
||||||
],
|
|
||||||
"summary": "Submit initial knowledge assessment",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "User ID",
|
|
||||||
"name": "user_id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Assessment responses",
|
|
||||||
"name": "payload",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.SubmitAssessmentReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/{tenant_slug}/otp/resend": {
|
"/api/v1/{tenant_slug}/otp/resend": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Resend OTP if the previous one is expired",
|
"description": "Resend OTP if the previous one is expired",
|
||||||
|
|
@ -2161,6 +2102,65 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/{tenant_slug}/user": {
|
||||||
|
"put": {
|
||||||
|
"description": "Updates user profile information (partial updates supported)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Update user profile",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "User ID",
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Update user payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.UpdateUserReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/{tenant_slug}/user-login": {
|
"/api/v1/{tenant_slug}/user-login": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Login user",
|
"description": "Login user",
|
||||||
|
|
@ -2181,7 +2181,7 @@ const docTemplate = `{
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/handlers.loginUserReq"
|
"$ref": "#/definitions/authentication.LoginRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -2631,9 +2631,99 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/{tenant_slug}/users": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get users with optional filters",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Get all users",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Role filter",
|
||||||
|
"name": "role",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search query",
|
||||||
|
"name": "query",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page number",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page size",
|
||||||
|
"name": "page_size",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Created before (RFC3339)",
|
||||||
|
"name": "created_before",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Created after (RFC3339)",
|
||||||
|
"name": "created_after",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"authentication.LoginRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"otp_code": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.AssessmentOption": {
|
"domain.AssessmentOption": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -3078,11 +3168,11 @@ const docTemplate = `{
|
||||||
},
|
},
|
||||||
"domain.ResendOtpReq": {
|
"domain.ResendOtpReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
|
||||||
"user_name"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"user_name": {
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3120,21 +3210,6 @@ const docTemplate = `{
|
||||||
"RoleSupport"
|
"RoleSupport"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"domain.SubmitAssessmentReq": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"answers"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"answers": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.UserAnswer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.UpdateKnowledgeLevelReq": {
|
"domain.UpdateKnowledgeLevelReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -3147,22 +3222,71 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.UserAnswer": {
|
"domain.UpdateUserReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"isCorrect": {
|
"age": {
|
||||||
"type": "boolean"
|
"$ref": "#/definitions/domain.ValidInt"
|
||||||
},
|
},
|
||||||
"questionID": {
|
"country": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"educationLevel": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"favoutiteTopic": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"firstName": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"knowledgeLevel": {
|
||||||
|
"description": "Profile fields",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"languageChallange": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"languageGoal": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"lastName": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"learningGoal": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"nickName": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"occupation": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"preferredLanguage": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"profileCompleted": {
|
||||||
|
"$ref": "#/definitions/domain.ValidBool"
|
||||||
|
},
|
||||||
|
"profilePictureURL": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"userID": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64"
|
"format": "int64"
|
||||||
},
|
},
|
||||||
"selectedOptionID": {
|
"userName": {
|
||||||
"type": "integer",
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
"format": "int64"
|
|
||||||
},
|
|
||||||
"shortAnswer": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -3268,17 +3392,52 @@ const docTemplate = `{
|
||||||
"UserStatusDeactivated"
|
"UserStatusDeactivated"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"domain.ValidBool": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.ValidInt": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.ValidString": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.VerifyOtpReq": {
|
"domain.VerifyOtpReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"otp",
|
"otp"
|
||||||
"user_name"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"user_name": {
|
"phone_number": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3511,40 +3670,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers.loginAdminReq": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"password",
|
|
||||||
"user_name"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"password": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "password123"
|
|
||||||
},
|
|
||||||
"user_name": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "adminuser"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handlers.loginUserReq": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"password",
|
|
||||||
"user_name"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"password": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "password123"
|
|
||||||
},
|
|
||||||
"user_name": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "johndoe"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handlers.loginUserRes": {
|
"handlers.loginUserRes": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -3556,6 +3681,9 @@ const docTemplate = `{
|
||||||
},
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1496,7 +1496,7 @@
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/handlers.loginAdminReq"
|
"$ref": "#/definitions/authentication.LoginRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -2016,7 +2016,7 @@
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/handlers.loginAdminReq"
|
"$ref": "#/definitions/authentication.LoginRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -2048,65 +2048,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/{tenant_slug}/assessment/submit": {
|
|
||||||
"post": {
|
|
||||||
"description": "Evaluates user responses, calculates knowledge level, updates user profile, and sends notification",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"assessment"
|
|
||||||
],
|
|
||||||
"summary": "Submit initial knowledge assessment",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "User ID",
|
|
||||||
"name": "user_id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Assessment responses",
|
|
||||||
"name": "payload",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.SubmitAssessmentReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/{tenant_slug}/otp/resend": {
|
"/api/v1/{tenant_slug}/otp/resend": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Resend OTP if the previous one is expired",
|
"description": "Resend OTP if the previous one is expired",
|
||||||
|
|
@ -2153,6 +2094,65 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/{tenant_slug}/user": {
|
||||||
|
"put": {
|
||||||
|
"description": "Updates user profile information (partial updates supported)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Update user profile",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "User ID",
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Update user payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.UpdateUserReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/{tenant_slug}/user-login": {
|
"/api/v1/{tenant_slug}/user-login": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Login user",
|
"description": "Login user",
|
||||||
|
|
@ -2173,7 +2173,7 @@
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/handlers.loginUserReq"
|
"$ref": "#/definitions/authentication.LoginRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -2623,9 +2623,99 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/{tenant_slug}/users": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get users with optional filters",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Get all users",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Role filter",
|
||||||
|
"name": "role",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search query",
|
||||||
|
"name": "query",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page number",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page size",
|
||||||
|
"name": "page_size",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Created before (RFC3339)",
|
||||||
|
"name": "created_before",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Created after (RFC3339)",
|
||||||
|
"name": "created_after",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"authentication.LoginRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"otp_code": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.AssessmentOption": {
|
"domain.AssessmentOption": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -3070,11 +3160,11 @@
|
||||||
},
|
},
|
||||||
"domain.ResendOtpReq": {
|
"domain.ResendOtpReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
|
||||||
"user_name"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"user_name": {
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3112,21 +3202,6 @@
|
||||||
"RoleSupport"
|
"RoleSupport"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"domain.SubmitAssessmentReq": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"answers"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"answers": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.UserAnswer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.UpdateKnowledgeLevelReq": {
|
"domain.UpdateKnowledgeLevelReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -3139,22 +3214,71 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.UserAnswer": {
|
"domain.UpdateUserReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"isCorrect": {
|
"age": {
|
||||||
"type": "boolean"
|
"$ref": "#/definitions/domain.ValidInt"
|
||||||
},
|
},
|
||||||
"questionID": {
|
"country": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"educationLevel": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"favoutiteTopic": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"firstName": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"knowledgeLevel": {
|
||||||
|
"description": "Profile fields",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"languageChallange": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"languageGoal": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"lastName": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"learningGoal": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"nickName": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"occupation": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"preferredLanguage": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"profileCompleted": {
|
||||||
|
"$ref": "#/definitions/domain.ValidBool"
|
||||||
|
},
|
||||||
|
"profilePictureURL": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
|
},
|
||||||
|
"userID": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64"
|
"format": "int64"
|
||||||
},
|
},
|
||||||
"selectedOptionID": {
|
"userName": {
|
||||||
"type": "integer",
|
"$ref": "#/definitions/domain.ValidString"
|
||||||
"format": "int64"
|
|
||||||
},
|
|
||||||
"shortAnswer": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -3260,17 +3384,52 @@
|
||||||
"UserStatusDeactivated"
|
"UserStatusDeactivated"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"domain.ValidBool": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.ValidInt": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.ValidString": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.VerifyOtpReq": {
|
"domain.VerifyOtpReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"otp",
|
"otp"
|
||||||
"user_name"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"user_name": {
|
"phone_number": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3503,40 +3662,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers.loginAdminReq": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"password",
|
|
||||||
"user_name"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"password": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "password123"
|
|
||||||
},
|
|
||||||
"user_name": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "adminuser"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handlers.loginUserReq": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"password",
|
|
||||||
"user_name"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"password": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "password123"
|
|
||||||
},
|
|
||||||
"user_name": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "johndoe"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handlers.loginUserRes": {
|
"handlers.loginUserRes": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -3548,6 +3673,9 @@
|
||||||
},
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,15 @@
|
||||||
definitions:
|
definitions:
|
||||||
|
authentication.LoginRequest:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
otp_code:
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
phone_number:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
domain.AssessmentOption:
|
domain.AssessmentOption:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
|
|
@ -300,10 +311,10 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
domain.ResendOtpReq:
|
domain.ResendOtpReq:
|
||||||
properties:
|
properties:
|
||||||
user_name:
|
email:
|
||||||
|
type: string
|
||||||
|
phone_number:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- user_name
|
|
||||||
type: object
|
type: object
|
||||||
domain.Response:
|
domain.Response:
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -330,16 +341,6 @@ definitions:
|
||||||
- RoleStudent
|
- RoleStudent
|
||||||
- RoleInstructor
|
- RoleInstructor
|
||||||
- RoleSupport
|
- RoleSupport
|
||||||
domain.SubmitAssessmentReq:
|
|
||||||
properties:
|
|
||||||
answers:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/domain.UserAnswer'
|
|
||||||
minItems: 1
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- answers
|
|
||||||
type: object
|
|
||||||
domain.UpdateKnowledgeLevelReq:
|
domain.UpdateKnowledgeLevelReq:
|
||||||
properties:
|
properties:
|
||||||
knowledge_level:
|
knowledge_level:
|
||||||
|
|
@ -348,18 +349,49 @@ definitions:
|
||||||
user_id:
|
user_id:
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
domain.UserAnswer:
|
domain.UpdateUserReq:
|
||||||
properties:
|
properties:
|
||||||
isCorrect:
|
age:
|
||||||
type: boolean
|
$ref: '#/definitions/domain.ValidInt'
|
||||||
questionID:
|
country:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
educationLevel:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
favoutiteTopic:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
firstName:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
knowledgeLevel:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/domain.ValidString'
|
||||||
|
description: Profile fields
|
||||||
|
languageChallange:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
languageGoal:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
lastName:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
learningGoal:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
nickName:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
occupation:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
preferredLanguage:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
profileCompleted:
|
||||||
|
$ref: '#/definitions/domain.ValidBool'
|
||||||
|
profilePictureURL:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
region:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
status:
|
||||||
|
$ref: '#/definitions/domain.ValidString'
|
||||||
|
userID:
|
||||||
format: int64
|
format: int64
|
||||||
type: integer
|
type: integer
|
||||||
selectedOptionID:
|
userName:
|
||||||
format: int64
|
$ref: '#/definitions/domain.ValidString'
|
||||||
type: integer
|
|
||||||
shortAnswer:
|
|
||||||
type: string
|
|
||||||
type: object
|
type: object
|
||||||
domain.UserProfileResponse:
|
domain.UserProfileResponse:
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -431,15 +463,37 @@ definitions:
|
||||||
- UserStatusActive
|
- UserStatusActive
|
||||||
- UserStatusSuspended
|
- UserStatusSuspended
|
||||||
- UserStatusDeactivated
|
- UserStatusDeactivated
|
||||||
|
domain.ValidBool:
|
||||||
|
properties:
|
||||||
|
valid:
|
||||||
|
type: boolean
|
||||||
|
value:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
domain.ValidInt:
|
||||||
|
properties:
|
||||||
|
valid:
|
||||||
|
type: boolean
|
||||||
|
value:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
domain.ValidString:
|
||||||
|
properties:
|
||||||
|
valid:
|
||||||
|
type: boolean
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
domain.VerifyOtpReq:
|
domain.VerifyOtpReq:
|
||||||
properties:
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
otp:
|
otp:
|
||||||
type: string
|
type: string
|
||||||
user_name:
|
phone_number:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- otp
|
- otp
|
||||||
- user_name
|
|
||||||
type: object
|
type: object
|
||||||
handlers.AdminProfileRes:
|
handlers.AdminProfileRes:
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -596,30 +650,6 @@ definitions:
|
||||||
- message
|
- message
|
||||||
- recipient
|
- recipient
|
||||||
type: object
|
type: object
|
||||||
handlers.loginAdminReq:
|
|
||||||
properties:
|
|
||||||
password:
|
|
||||||
example: password123
|
|
||||||
type: string
|
|
||||||
user_name:
|
|
||||||
example: adminuser
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- password
|
|
||||||
- user_name
|
|
||||||
type: object
|
|
||||||
handlers.loginUserReq:
|
|
||||||
properties:
|
|
||||||
password:
|
|
||||||
example: password123
|
|
||||||
type: string
|
|
||||||
user_name:
|
|
||||||
example: johndoe
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- password
|
|
||||||
- user_name
|
|
||||||
type: object
|
|
||||||
handlers.loginUserRes:
|
handlers.loginUserRes:
|
||||||
properties:
|
properties:
|
||||||
access_token:
|
access_token:
|
||||||
|
|
@ -628,6 +658,8 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
role:
|
role:
|
||||||
type: string
|
type: string
|
||||||
|
user_id:
|
||||||
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
handlers.logoutReq:
|
handlers.logoutReq:
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -708,7 +740,7 @@ paths:
|
||||||
name: login
|
name: login
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/handlers.loginAdminReq'
|
$ref: '#/definitions/authentication.LoginRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
|
@ -731,46 +763,6 @@ paths:
|
||||||
summary: Login user
|
summary: Login user
|
||||||
tags:
|
tags:
|
||||||
- auth
|
- auth
|
||||||
/api/v1/{tenant_slug}/assessment/submit:
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Evaluates user responses, calculates knowledge level, updates user
|
|
||||||
profile, and sends notification
|
|
||||||
parameters:
|
|
||||||
- description: User ID
|
|
||||||
in: path
|
|
||||||
name: user_id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: Assessment responses
|
|
||||||
in: body
|
|
||||||
name: payload
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.SubmitAssessmentReq'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"400":
|
|
||||||
description: Bad Request
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
"404":
|
|
||||||
description: Not Found
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
summary: Submit initial knowledge assessment
|
|
||||||
tags:
|
|
||||||
- assessment
|
|
||||||
/api/v1/{tenant_slug}/otp/resend:
|
/api/v1/{tenant_slug}/otp/resend:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
@ -801,6 +793,45 @@ paths:
|
||||||
summary: Resend OTP
|
summary: Resend OTP
|
||||||
tags:
|
tags:
|
||||||
- otp
|
- otp
|
||||||
|
/api/v1/{tenant_slug}/user:
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Updates user profile information (partial updates supported)
|
||||||
|
parameters:
|
||||||
|
- description: User ID
|
||||||
|
in: path
|
||||||
|
name: user_id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Update user payload
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.UpdateUserReq'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Response'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Update user profile
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
/api/v1/{tenant_slug}/user-login:
|
/api/v1/{tenant_slug}/user-login:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
@ -812,7 +843,7 @@ paths:
|
||||||
name: login
|
name: login
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/handlers.loginUserReq'
|
$ref: '#/definitions/authentication.LoginRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
|
@ -1108,6 +1139,54 @@ paths:
|
||||||
summary: Get user profile
|
summary: Get user profile
|
||||||
tags:
|
tags:
|
||||||
- user
|
- user
|
||||||
|
/api/v1/{tenant_slug}/users:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Get users with optional filters
|
||||||
|
parameters:
|
||||||
|
- description: Role filter
|
||||||
|
in: query
|
||||||
|
name: role
|
||||||
|
type: string
|
||||||
|
- description: Search query
|
||||||
|
in: query
|
||||||
|
name: query
|
||||||
|
type: string
|
||||||
|
- description: Page number
|
||||||
|
in: query
|
||||||
|
name: page
|
||||||
|
type: integer
|
||||||
|
- description: Page size
|
||||||
|
in: query
|
||||||
|
name: page_size
|
||||||
|
type: integer
|
||||||
|
- description: Created before (RFC3339)
|
||||||
|
in: query
|
||||||
|
name: created_before
|
||||||
|
type: string
|
||||||
|
- description: Created after (RFC3339)
|
||||||
|
in: query
|
||||||
|
name: created_after
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
summary: Get all users
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
/api/v1/admin:
|
/api/v1/admin:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
@ -2023,7 +2102,7 @@ paths:
|
||||||
name: login
|
name: login
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/handlers.loginAdminReq'
|
$ref: '#/definitions/authentication.LoginRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
|
|
||||||
|
|
@ -11,42 +11,6 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
const CreateAssessmentAnswer = `-- name: CreateAssessmentAnswer :exec
|
|
||||||
INSERT INTO assessment_answers (
|
|
||||||
attempt_id,
|
|
||||||
question_id,
|
|
||||||
selected_option_id,
|
|
||||||
short_answer,
|
|
||||||
is_correct
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
$1, -- attempt_id
|
|
||||||
$2, -- question_id
|
|
||||||
$3, -- selected_option_id
|
|
||||||
$4, -- short_answer
|
|
||||||
$5 -- is_correct
|
|
||||||
)
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateAssessmentAnswerParams struct {
|
|
||||||
AttemptID int64 `json:"attempt_id"`
|
|
||||||
QuestionID int64 `json:"question_id"`
|
|
||||||
SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
|
|
||||||
ShortAnswer pgtype.Text `json:"short_answer"`
|
|
||||||
IsCorrect bool `json:"is_correct"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateAssessmentAnswer(ctx context.Context, arg CreateAssessmentAnswerParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, CreateAssessmentAnswer,
|
|
||||||
arg.AttemptID,
|
|
||||||
arg.QuestionID,
|
|
||||||
arg.SelectedOptionID,
|
|
||||||
arg.ShortAnswer,
|
|
||||||
arg.IsCorrect,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateAssessmentAttempt = `-- name: CreateAssessmentAttempt :one
|
const CreateAssessmentAttempt = `-- name: CreateAssessmentAttempt :one
|
||||||
INSERT INTO assessment_attempts (
|
INSERT INTO assessment_attempts (
|
||||||
user_id,
|
user_id,
|
||||||
|
|
@ -197,6 +161,7 @@ func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]Assessmen
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetAssessmentOptionByID = `-- name: GetAssessmentOptionByID :one
|
const GetAssessmentOptionByID = `-- name: GetAssessmentOptionByID :one
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
question_id,
|
question_id,
|
||||||
|
|
@ -207,6 +172,25 @@ WHERE id = $1
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// -- name: CreateAssessmentAnswer :exec
|
||||||
|
// INSERT INTO assessment_answers (
|
||||||
|
//
|
||||||
|
// attempt_id,
|
||||||
|
// question_id,
|
||||||
|
// selected_option_id,
|
||||||
|
// short_answer,
|
||||||
|
// is_correct
|
||||||
|
//
|
||||||
|
// )
|
||||||
|
// VALUES (
|
||||||
|
//
|
||||||
|
// $1, -- attempt_id
|
||||||
|
// $2, -- question_id
|
||||||
|
// $3, -- selected_option_id
|
||||||
|
// $4, -- short_answer
|
||||||
|
// $5 -- is_correct
|
||||||
|
//
|
||||||
|
// );
|
||||||
func (q *Queries) GetAssessmentOptionByID(ctx context.Context, id int64) (AssessmentQuestionOption, error) {
|
func (q *Queries) GetAssessmentOptionByID(ctx context.Context, id int64) (AssessmentQuestionOption, error) {
|
||||||
row := q.db.QueryRow(ctx, GetAssessmentOptionByID, id)
|
row := q.db.QueryRow(ctx, GetAssessmentOptionByID, id)
|
||||||
var i AssessmentQuestionOption
|
var i AssessmentQuestionOption
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import (
|
||||||
|
|
||||||
type AssessmentAnswer struct {
|
type AssessmentAnswer struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
AttemptID int64 `json:"attempt_id"`
|
|
||||||
QuestionID int64 `json:"question_id"`
|
QuestionID int64 `json:"question_id"`
|
||||||
SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
|
SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
|
||||||
ShortAnswer pgtype.Text `json:"short_answer"`
|
ShortAnswer pgtype.Text `json:"short_answer"`
|
||||||
|
|
@ -198,6 +197,7 @@ type User struct {
|
||||||
EducationLevel pgtype.Text `json:"education_level"`
|
EducationLevel pgtype.Text `json:"education_level"`
|
||||||
Country pgtype.Text `json:"country"`
|
Country pgtype.Text `json:"country"`
|
||||||
Region pgtype.Text `json:"region"`
|
Region pgtype.Text `json:"region"`
|
||||||
|
Medium string `json:"medium"`
|
||||||
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
|
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
|
||||||
NickName pgtype.Text `json:"nick_name"`
|
NickName pgtype.Text `json:"nick_name"`
|
||||||
Occupation pgtype.Text `json:"occupation"`
|
Occupation pgtype.Text `json:"occupation"`
|
||||||
|
|
|
||||||
|
|
@ -267,14 +267,12 @@ SELECT
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
|
||||||
nick_name,
|
nick_name,
|
||||||
occupation,
|
occupation,
|
||||||
learning_goal,
|
learning_goal,
|
||||||
language_goal,
|
language_goal,
|
||||||
language_challange,
|
language_challange,
|
||||||
favoutite_topic,
|
favoutite_topic,
|
||||||
|
|
||||||
initial_assessment_completed,
|
initial_assessment_completed,
|
||||||
profile_picture_url,
|
profile_picture_url,
|
||||||
preferred_language,
|
preferred_language,
|
||||||
|
|
@ -282,39 +280,27 @@ SELECT
|
||||||
phone_verified,
|
phone_verified,
|
||||||
status,
|
status,
|
||||||
profile_completed,
|
profile_completed,
|
||||||
preferred_language,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM users
|
FROM users
|
||||||
WHERE (
|
WHERE ($1 IS NULL OR role = $1)
|
||||||
role = $1 OR $1 IS NULL
|
AND ($2 IS NULL OR first_name ILIKE '%' || $2 || '%'
|
||||||
)
|
|
||||||
AND (
|
|
||||||
first_name ILIKE '%' || $2 || '%'
|
|
||||||
OR last_name ILIKE '%' || $2 || '%'
|
OR last_name ILIKE '%' || $2 || '%'
|
||||||
OR phone_number ILIKE '%' || $2 || '%'
|
OR phone_number ILIKE '%' || $2 || '%'
|
||||||
OR email ILIKE '%' || $2 || '%'
|
OR email ILIKE '%' || $2 || '%')
|
||||||
OR $2 IS NULL
|
AND ($3 IS NULL OR created_at >= $3)
|
||||||
)
|
AND ($4 IS NULL OR created_at <= $4)
|
||||||
AND (
|
LIMIT $5
|
||||||
created_at >= $3
|
OFFSET $6
|
||||||
OR $3 IS NULL
|
|
||||||
)
|
|
||||||
AND (
|
|
||||||
created_at <= $4
|
|
||||||
OR $4 IS NULL
|
|
||||||
)
|
|
||||||
LIMIT $6
|
|
||||||
OFFSET $5
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetAllUsersParams struct {
|
type GetAllUsersParams struct {
|
||||||
Role string `json:"role"`
|
Column1 interface{} `json:"column_1"`
|
||||||
Query pgtype.Text `json:"query"`
|
Column2 interface{} `json:"column_2"`
|
||||||
CreatedAfter pgtype.Timestamptz `json:"created_after"`
|
Column3 interface{} `json:"column_3"`
|
||||||
CreatedBefore pgtype.Timestamptz `json:"created_before"`
|
Column4 interface{} `json:"column_4"`
|
||||||
Offset pgtype.Int4 `json:"offset"`
|
Limit int32 `json:"limit"`
|
||||||
Limit pgtype.Int4 `json:"limit"`
|
Offset int32 `json:"offset"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetAllUsersRow struct {
|
type GetAllUsersRow struct {
|
||||||
|
|
@ -343,19 +329,18 @@ type GetAllUsersRow struct {
|
||||||
PhoneVerified bool `json:"phone_verified"`
|
PhoneVerified bool `json:"phone_verified"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
ProfileCompleted bool `json:"profile_completed"`
|
ProfileCompleted bool `json:"profile_completed"`
|
||||||
PreferredLanguage_2 pgtype.Text `json:"preferred_language_2"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]GetAllUsersRow, error) {
|
func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]GetAllUsersRow, error) {
|
||||||
rows, err := q.db.Query(ctx, GetAllUsers,
|
rows, err := q.db.Query(ctx, GetAllUsers,
|
||||||
arg.Role,
|
arg.Column1,
|
||||||
arg.Query,
|
arg.Column2,
|
||||||
arg.CreatedAfter,
|
arg.Column3,
|
||||||
arg.CreatedBefore,
|
arg.Column4,
|
||||||
arg.Offset,
|
|
||||||
arg.Limit,
|
arg.Limit,
|
||||||
|
arg.Offset,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -390,7 +375,6 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
|
||||||
&i.PhoneVerified,
|
&i.PhoneVerified,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.ProfileCompleted,
|
&i.ProfileCompleted,
|
||||||
&i.PreferredLanguage_2,
|
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
@ -439,6 +423,7 @@ SELECT
|
||||||
language_challange,
|
language_challange,
|
||||||
favoutite_topic,
|
favoutite_topic,
|
||||||
|
|
||||||
|
medium,
|
||||||
email_verified,
|
email_verified,
|
||||||
phone_verified,
|
phone_verified,
|
||||||
status,
|
status,
|
||||||
|
|
@ -478,6 +463,7 @@ type GetUserByEmailPhoneRow struct {
|
||||||
LanguageGoal pgtype.Text `json:"language_goal"`
|
LanguageGoal pgtype.Text `json:"language_goal"`
|
||||||
LanguageChallange pgtype.Text `json:"language_challange"`
|
LanguageChallange pgtype.Text `json:"language_challange"`
|
||||||
FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
|
FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
|
||||||
|
Medium string `json:"medium"`
|
||||||
EmailVerified bool `json:"email_verified"`
|
EmailVerified bool `json:"email_verified"`
|
||||||
PhoneVerified bool `json:"phone_verified"`
|
PhoneVerified bool `json:"phone_verified"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
|
@ -511,6 +497,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
||||||
&i.LanguageGoal,
|
&i.LanguageGoal,
|
||||||
&i.LanguageChallange,
|
&i.LanguageChallange,
|
||||||
&i.FavoutiteTopic,
|
&i.FavoutiteTopic,
|
||||||
|
&i.Medium,
|
||||||
&i.EmailVerified,
|
&i.EmailVerified,
|
||||||
&i.PhoneVerified,
|
&i.PhoneVerified,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
|
|
@ -525,7 +512,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetUserByID = `-- name: GetUserByID :one
|
const GetUserByID = `-- name: GetUserByID :one
|
||||||
SELECT id, first_name, last_name, user_name, email, phone_number, role, password, age, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favoutite_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, user_name, email, phone_number, role, password, age, education_level, country, region, medium, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favoutite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -546,6 +533,7 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||||
&i.EducationLevel,
|
&i.EducationLevel,
|
||||||
&i.Country,
|
&i.Country,
|
||||||
&i.Region,
|
&i.Region,
|
||||||
|
&i.Medium,
|
||||||
&i.KnowledgeLevel,
|
&i.KnowledgeLevel,
|
||||||
&i.NickName,
|
&i.NickName,
|
||||||
&i.Occupation,
|
&i.Occupation,
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ var (
|
||||||
// }
|
// }
|
||||||
|
|
||||||
type AFROSMSConfig struct {
|
type AFROSMSConfig struct {
|
||||||
|
AfroSMSSenderName string `mapstructure:"afrom_sms_sender_name"`
|
||||||
AfroSMSIdentifierID string `mapstructure:"afro_sms_identifier_id"`
|
AfroSMSIdentifierID string `mapstructure:"afro_sms_identifier_id"`
|
||||||
AfroSMSAPIKey string `mapstructure:"afro_sms_api_key"`
|
AfroSMSAPIKey string `mapstructure:"afro_sms_api_key"`
|
||||||
AfroSMSBaseURL string `mapstructure:"afro_sms_base_url"`
|
AfroSMSBaseURL string `mapstructure:"afro_sms_base_url"`
|
||||||
|
|
@ -259,6 +260,7 @@ func (c *Config) loadEnv() error {
|
||||||
c.AFROSMSConfig.AfroSMSAPIKey = os.Getenv("AFRO_SMS_API_KEY")
|
c.AFROSMSConfig.AfroSMSAPIKey = os.Getenv("AFRO_SMS_API_KEY")
|
||||||
c.AFROSMSConfig.AfroSMSIdentifierID = os.Getenv("AFRO_SMS_IDENTIFIER_ID")
|
c.AFROSMSConfig.AfroSMSIdentifierID = os.Getenv("AFRO_SMS_IDENTIFIER_ID")
|
||||||
c.AFROSMSConfig.AfroSMSBaseURL = os.Getenv("AFRO_SMS_BASE_URL")
|
c.AFROSMSConfig.AfroSMSBaseURL = os.Getenv("AFRO_SMS_BASE_URL")
|
||||||
|
c.AFROSMSConfig.AfroSMSSenderName = os.Getenv("AFRO_SMS_SENDER_NAME")
|
||||||
|
|
||||||
//Telebirr
|
//Telebirr
|
||||||
c.TELEBIRR.TelebirrBaseURL = os.Getenv("TELEBIRR_BASE_URL")
|
c.TELEBIRR.TelebirrBaseURL = os.Getenv("TELEBIRR_BASE_URL")
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,12 @@ type Otp struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type VerifyOtpReq struct {
|
type VerifyOtpReq struct {
|
||||||
UserName string `json:"user_name" validate:"required"`
|
Email string `json:"email"`
|
||||||
|
PhoneNumber string `json:"phone_number"`
|
||||||
Otp string `json:"otp" validate:"required"`
|
Otp string `json:"otp" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResendOtpReq struct {
|
type ResendOtpReq struct {
|
||||||
UserName string `json:"user_name" validate:"required"`
|
Email string `json:"email"`
|
||||||
|
PhoneNumber string `json:"phone_number"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,6 @@ type InitialAssessmentStore interface {
|
||||||
q domain.AssessmentQuestion,
|
q domain.AssessmentQuestion,
|
||||||
) (domain.AssessmentQuestion, error)
|
) (domain.AssessmentQuestion, error)
|
||||||
GetActiveAssessmentQuestions(ctx context.Context) ([]domain.AssessmentQuestion, error)
|
GetActiveAssessmentQuestions(ctx context.Context) ([]domain.AssessmentQuestion, error)
|
||||||
SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error)
|
// SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error)
|
||||||
GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, error)
|
GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
"Yimaru-Backend/internal/ports"
|
"Yimaru-Backend/internal/ports"
|
||||||
"context"
|
"context"
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
@ -105,61 +104,61 @@ func (s *Store) GetActiveAssessmentQuestions(ctx context.Context) ([]domain.Asse
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveAssessmentAttempt saves the attempt summary and answers
|
// SaveAssessmentAttempt saves the attempt summary and answers
|
||||||
func (s *Store) SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error) {
|
// func (s *Store) SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error) {
|
||||||
total := len(answers)
|
// total := len(answers)
|
||||||
correct := 0
|
// correct := 0
|
||||||
|
|
||||||
for _, ans := range answers {
|
// for _, ans := range answers {
|
||||||
if ans.IsCorrect {
|
// if ans.IsCorrect {
|
||||||
correct++
|
// correct++
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
score := float64(correct) / float64(total) * 100
|
// score := float64(correct) / float64(total) * 100
|
||||||
knowledgeLevel := "BEGINNER"
|
// knowledgeLevel := "BEGINNER"
|
||||||
switch {
|
// switch {
|
||||||
case score >= 80:
|
// case score >= 80:
|
||||||
knowledgeLevel = "ADVANCED"
|
// knowledgeLevel = "ADVANCED"
|
||||||
case score >= 50:
|
// case score >= 50:
|
||||||
knowledgeLevel = "INTERMEDIATE"
|
// knowledgeLevel = "INTERMEDIATE"
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Save attempt
|
// // Save attempt
|
||||||
attemptRow, err := s.queries.CreateAssessmentAttempt(ctx, dbgen.CreateAssessmentAttemptParams{
|
// attemptRow, err := s.queries.CreateAssessmentAttempt(ctx, dbgen.CreateAssessmentAttemptParams{
|
||||||
UserID: userID,
|
// UserID: userID,
|
||||||
TotalQuestions: int32(total),
|
// TotalQuestions: int32(total),
|
||||||
CorrectAnswers: int32(correct),
|
// CorrectAnswers: int32(correct),
|
||||||
ScorePercentage: pgtype.Numeric{Int: big.NewInt(int64(score * 100)), Valid: true},
|
// ScorePercentage: pgtype.Numeric{Int: big.NewInt(int64(score * 100)), Valid: true},
|
||||||
KnowledgeLevel: knowledgeLevel,
|
// KnowledgeLevel: knowledgeLevel,
|
||||||
})
|
// })
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return domain.AssessmentAttempt{}, err
|
// return domain.AssessmentAttempt{}, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Save answers
|
// // Save answers
|
||||||
for _, ans := range answers {
|
// for _, ans := range answers {
|
||||||
err := s.queries.CreateAssessmentAnswer(ctx, dbgen.CreateAssessmentAnswerParams{
|
// err := s.queries.CreateAssessmentAnswer(ctx, dbgen.CreateAssessmentAnswerParams{
|
||||||
AttemptID: attemptRow.ID,
|
// AttemptID: attemptRow.ID,
|
||||||
QuestionID: ans.QuestionID,
|
// QuestionID: ans.QuestionID,
|
||||||
SelectedOptionID: pgtype.Int8{Int64: ans.SelectedOptionID, Valid: true},
|
// SelectedOptionID: pgtype.Int8{Int64: ans.SelectedOptionID, Valid: true},
|
||||||
ShortAnswer: pgtype.Text{String: ans.ShortAnswer, Valid: true},
|
// ShortAnswer: pgtype.Text{String: ans.ShortAnswer, Valid: true},
|
||||||
IsCorrect: ans.IsCorrect,
|
// IsCorrect: ans.IsCorrect,
|
||||||
})
|
// })
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return domain.AssessmentAttempt{}, err
|
// return domain.AssessmentAttempt{}, err
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return domain.AssessmentAttempt{
|
// return domain.AssessmentAttempt{
|
||||||
ID: attemptRow.ID,
|
// ID: attemptRow.ID,
|
||||||
UserID: userID,
|
// UserID: userID,
|
||||||
TotalQuestions: total,
|
// TotalQuestions: total,
|
||||||
CorrectAnswers: correct,
|
// CorrectAnswers: correct,
|
||||||
ScorePercentage: score,
|
// ScorePercentage: score,
|
||||||
KnowledgeLevel: knowledgeLevel,
|
// KnowledgeLevel: knowledgeLevel,
|
||||||
CompletedAt: attemptRow.CompletedAt.Time,
|
// CompletedAt: attemptRow.CompletedAt.Time,
|
||||||
}, nil
|
// }, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// GetOptionByID fetches a single option to validate correctness
|
// GetOptionByID fetches a single option to validate correctness
|
||||||
func (s *Store) GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, error) {
|
func (s *Store) GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, error) {
|
||||||
|
|
|
||||||
|
|
@ -254,25 +254,41 @@ func (s *Store) GetAllUsers(
|
||||||
limit, offset int32,
|
limit, offset int32,
|
||||||
) ([]domain.User, int64, error) {
|
) ([]domain.User, int64, error) {
|
||||||
|
|
||||||
params := dbgen.GetAllUsersParams{
|
var roleParam sql.NullString
|
||||||
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
if role != nil && *role != "" {
|
||||||
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
roleParam = sql.NullString{String: *role, Valid: true}
|
||||||
|
} else {
|
||||||
|
roleParam = sql.NullString{Valid: false} // This will make $1 IS NULL work
|
||||||
}
|
}
|
||||||
|
|
||||||
if role != nil {
|
var queryParam sql.NullString
|
||||||
params.Role = *role
|
if query != nil && *query != "" {
|
||||||
}
|
queryParam = sql.NullString{String: *query, Valid: true}
|
||||||
|
} else {
|
||||||
if query != nil {
|
queryParam = sql.NullString{Valid: false}
|
||||||
params.Query = pgtype.Text{String: *query, Valid: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
if createdBefore != nil {
|
|
||||||
params.CreatedBefore = pgtype.Timestamptz{Time: *createdBefore, Valid: true}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var createdAfterParam sql.NullTime
|
||||||
if createdAfter != nil {
|
if createdAfter != nil {
|
||||||
params.CreatedAfter = pgtype.Timestamptz{Time: *createdAfter, Valid: true}
|
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}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := s.queries.GetAllUsers(ctx, params)
|
rows, err := s.queries.GetAllUsers(ctx, params)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) GetActiveAssessmentQuestions(
|
func (s *Service) GetActiveAssessmentQuestions(
|
||||||
|
|
@ -70,77 +69,77 @@ func (s *Service) CreateAssessmentQuestion(
|
||||||
return s.initialAssessmentStore.CreateAssessmentQuestion(ctx, q)
|
return s.initialAssessmentStore.CreateAssessmentQuestion(ctx, q)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SubmitAssessment(
|
// func (s *Service) SubmitAssessment(
|
||||||
ctx context.Context,
|
// ctx context.Context,
|
||||||
userID int64,
|
// userID int64,
|
||||||
responses []domain.UserAnswer,
|
// responses []domain.UserAnswer,
|
||||||
) (domain.AssessmentAttempt, error) {
|
// ) (domain.AssessmentAttempt, error) {
|
||||||
|
|
||||||
if userID <= 0 {
|
// if userID <= 0 {
|
||||||
return domain.AssessmentAttempt{}, errors.New("invalid user id")
|
// return domain.AssessmentAttempt{}, errors.New("invalid user id")
|
||||||
}
|
// }
|
||||||
|
|
||||||
if len(responses) == 0 {
|
// if len(responses) == 0 {
|
||||||
return domain.AssessmentAttempt{}, errors.New("no responses submitted")
|
// return domain.AssessmentAttempt{}, errors.New("no responses submitted")
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Step 1: Validate and evaluate answers
|
// // Step 1: Validate and evaluate answers
|
||||||
for i, ans := range responses {
|
// for i, ans := range responses {
|
||||||
if ans.QuestionID == 0 {
|
// if ans.QuestionID == 0 {
|
||||||
return domain.AssessmentAttempt{}, errors.New("invalid question id")
|
// return domain.AssessmentAttempt{}, errors.New("invalid question id")
|
||||||
}
|
// }
|
||||||
|
|
||||||
isCorrect, err := s.validateAnswer(ctx, ans)
|
// isCorrect, err := s.validateAnswer(ctx, ans)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return domain.AssessmentAttempt{}, err
|
// return domain.AssessmentAttempt{}, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
responses[i].IsCorrect = isCorrect
|
// responses[i].IsCorrect = isCorrect
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Step 2: Persist assessment attempt + answers
|
// // Step 2: Persist assessment attempt + answers
|
||||||
attempt, err := s.initialAssessmentStore.SaveAssessmentAttempt(
|
// attempt, err := s.initialAssessmentStore.SaveAssessmentAttempt(
|
||||||
ctx,
|
// ctx,
|
||||||
userID,
|
// userID,
|
||||||
responses,
|
// responses,
|
||||||
)
|
// )
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return domain.AssessmentAttempt{}, err
|
// return domain.AssessmentAttempt{}, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Step 3: Update user's knowledge level
|
// // Step 3: Update user's knowledge level
|
||||||
if err := s.userStore.UpdateUserKnowledgeLevel(
|
// if err := s.userStore.UpdateUserKnowledgeLevel(
|
||||||
ctx,
|
// ctx,
|
||||||
userID,
|
// userID,
|
||||||
attempt.KnowledgeLevel,
|
// attempt.KnowledgeLevel,
|
||||||
); err != nil {
|
// ); err != nil {
|
||||||
return domain.AssessmentAttempt{}, err
|
// return domain.AssessmentAttempt{}, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Step 4: Send in-app notification
|
// // Step 4: Send in-app notification
|
||||||
notification := &domain.Notification{
|
// notification := &domain.Notification{
|
||||||
|
|
||||||
RecipientID: userID,
|
// RecipientID: userID,
|
||||||
Level: domain.NotificationLevelInfo,
|
// Level: domain.NotificationLevelInfo,
|
||||||
Reciever: domain.NotificationRecieverSideCustomer,
|
// Reciever: domain.NotificationRecieverSideCustomer,
|
||||||
IsRead: false,
|
// IsRead: false,
|
||||||
DeliveryStatus: domain.DeliveryStatusSent,
|
// DeliveryStatus: domain.DeliveryStatusSent,
|
||||||
DeliveryChannel: domain.DeliveryChannelInApp,
|
// DeliveryChannel: domain.DeliveryChannelInApp,
|
||||||
Payload: domain.NotificationPayload{
|
// Payload: domain.NotificationPayload{
|
||||||
Headline: "Knowledge Assessment Completed",
|
// Headline: "Knowledge Assessment Completed",
|
||||||
Message: "Your knowledge assessment is complete. Your knowledge level is " + attempt.KnowledgeLevel + ".",
|
// Message: "Your knowledge assessment is complete. Your knowledge level is " + attempt.KnowledgeLevel + ".",
|
||||||
Tags: []string{"assessment", "knowledge-level"},
|
// Tags: []string{"assessment", "knowledge-level"},
|
||||||
},
|
// },
|
||||||
Timestamp: time.Now(),
|
// Timestamp: time.Now(),
|
||||||
Type: domain.NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE,
|
// Type: domain.NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE,
|
||||||
}
|
// }
|
||||||
|
|
||||||
if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
|
// if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
|
||||||
return domain.AssessmentAttempt{}, err
|
// return domain.AssessmentAttempt{}, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
return attempt, nil
|
// return attempt, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (s *Service) validateAnswer(
|
func (s *Service) validateAnswer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,22 @@ type LoginSuccess struct {
|
||||||
RfToken string
|
RfToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
PhoneNumber string `json:"phone_number"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
OTPCode string `json:"otp_code"`
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) Login(
|
func (s *Service) Login(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userName, password string,
|
req LoginRequest,
|
||||||
) (LoginSuccess, error) {
|
) (LoginSuccess, error) {
|
||||||
|
|
||||||
user, err := s.userStore.GetUserByUserName(ctx, userName)
|
// Try to find user by username first
|
||||||
|
user, err := s.userStore.GetUserByEmailPhone(ctx, req.Email, req.PhoneNumber)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// If not found by username, try email or phone lookup using the same identifier
|
||||||
return LoginSuccess{}, err
|
return LoginSuccess{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,16 +49,21 @@ func (s *Service) Login(
|
||||||
return LoginSuccess{}, domain.ErrUserNotVerified
|
return LoginSuccess{}, domain.ErrUserNotVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify password
|
|
||||||
if err := matchPassword(password, user.Password); err != nil {
|
|
||||||
return LoginSuccess{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status check instead of Suspended
|
// Status check instead of Suspended
|
||||||
if user.Status == domain.UserStatusSuspended {
|
if user.Status == domain.UserStatusSuspended {
|
||||||
return LoginSuccess{}, ErrUserSuspended
|
return LoginSuccess{}, ErrUserSuspended
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Email != "" {
|
||||||
|
if err := matchPassword(req.Password, user.Password); err != nil {
|
||||||
|
return LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
} else if req.PhoneNumber != "" {
|
||||||
|
if err := s.UserSvc.VerifyOtp(ctx, req.Email, req.PhoneNumber, req.OTPCode); err != nil {
|
||||||
|
return LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle existing refresh token
|
// Handle existing refresh token
|
||||||
oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
|
oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
|
||||||
if err != nil && !errors.Is(err, ErrRefreshTokenNotFound) {
|
if err != nil && !errors.Is(err, ErrRefreshTokenNotFound) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package authentication
|
package authentication
|
||||||
|
|
||||||
import "Yimaru-Backend/internal/ports"
|
import (
|
||||||
|
"Yimaru-Backend/internal/ports"
|
||||||
|
"Yimaru-Backend/internal/services/user"
|
||||||
|
)
|
||||||
|
|
||||||
// type EmailPhone struct {
|
// type EmailPhone struct {
|
||||||
// Email ValidString
|
// Email ValidString
|
||||||
|
|
@ -17,13 +20,15 @@ type Tokens struct {
|
||||||
}
|
}
|
||||||
type Service struct {
|
type Service struct {
|
||||||
userStore ports.UserStore
|
userStore ports.UserStore
|
||||||
|
UserSvc user.Service
|
||||||
tokenStore ports.TokenStore
|
tokenStore ports.TokenStore
|
||||||
RefreshExpiry int
|
RefreshExpiry int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(userStore ports.UserStore, tokenStore ports.TokenStore, RefreshExpiry int) *Service {
|
func NewService(userStore ports.UserStore, userSvc user.Service, tokenStore ports.TokenStore, RefreshExpiry int) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
userStore: userStore,
|
userStore: userStore,
|
||||||
|
UserSvc: userSvc,
|
||||||
tokenStore: tokenStore,
|
tokenStore: tokenStore,
|
||||||
RefreshExpiry: RefreshExpiry,
|
RefreshExpiry: RefreshExpiry,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ func (s *Service) SendSMS(ctx context.Context, receiverPhone, message string) er
|
||||||
|
|
||||||
switch settingsList.SMSProvider {
|
switch settingsList.SMSProvider {
|
||||||
case domain.AfroMessage:
|
case domain.AfroMessage:
|
||||||
return s.SendAfroMessageSMSLatest(ctx, receiverPhone, message, nil)
|
return s.SendAfroMessageSMS(ctx, receiverPhone, message)
|
||||||
case domain.TwilioSms:
|
case domain.TwilioSms:
|
||||||
return s.SendTwilioSMS(ctx, receiverPhone, message)
|
return s.SendTwilioSMS(ctx, receiverPhone, message)
|
||||||
default:
|
default:
|
||||||
|
|
@ -44,12 +44,14 @@ func (s *Service) SendAfroMessageSMS(ctx context.Context, receiverPhone, message
|
||||||
// API endpoint has been updated
|
// API endpoint has been updated
|
||||||
// TODO: no need for package for the afro message operations (pretty simple stuff)
|
// TODO: no need for package for the afro message operations (pretty simple stuff)
|
||||||
request := afro.GetRequest(apiKey, endpoint, hostURL)
|
request := afro.GetRequest(apiKey, endpoint, hostURL)
|
||||||
request.BaseURL = "https://api.afromessage.com/api/send"
|
request.BaseURL = "https://api.afromessage.com"
|
||||||
|
|
||||||
request.Method = "GET"
|
request.Method = "GET"
|
||||||
request.Sender(senderName)
|
request.Sender(senderName)
|
||||||
request.To(receiverPhone, message)
|
request.To(receiverPhone, message)
|
||||||
|
|
||||||
|
fmt.Printf("the afro SMS request is: %v", request)
|
||||||
|
|
||||||
response, err := afro.MakeRequestWithContext(ctx, request)
|
response, err := afro.MakeRequestWithContext(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -76,7 +78,7 @@ func (s *Service) SendAfroMessageSMSLatest(
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Set("to", receiverPhone)
|
params.Set("to", receiverPhone)
|
||||||
params.Set("message", message)
|
params.Set("message", message)
|
||||||
params.Set("sender", s.config.AFRO_SMS_SENDER_NAME)
|
params.Set("sender", s.config.AFROSMSConfig.AfroSMSSenderName)
|
||||||
|
|
||||||
// Optional parameters
|
// Optional parameters
|
||||||
if s.config.AFROSMSConfig.AfroSMSIdentifierID != "" {
|
if s.config.AFROSMSConfig.AfroSMSIdentifierID != "" {
|
||||||
|
|
@ -88,7 +90,7 @@ func (s *Service) SendAfroMessageSMSLatest(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct full URL
|
// Construct full URL
|
||||||
reqURL := fmt.Sprintf("%s?%s", baseURL, params.Encode())
|
reqURL := fmt.Sprintf("%s?%s", baseURL+"/api/send", params.Encode())
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -96,7 +98,7 @@ func (s *Service) SendAfroMessageSMSLatest(
|
||||||
}
|
}
|
||||||
|
|
||||||
// AfroMessage authentication (API key)
|
// AfroMessage authentication (API key)
|
||||||
req.Header.Set("Authorization", "Bearer "+s.config.AFRO_SMS_API_KEY)
|
req.Header.Set("Authorization", "Bearer "+s.config.AFROSMSConfig.AfroSMSAPIKey)
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import (
|
||||||
// "github.com/segmentio/kafka-go"
|
// "github.com/segmentio/kafka-go"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
// afro "github.com/amanuelabay/afrosms-go"
|
// afro "github.com/amanuelabay/afrosms-go"
|
||||||
|
afro "github.com/amanuelabay/afrosms-go"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
// "github.com/redis/go-redis/v9"
|
// "github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
@ -72,6 +73,38 @@ func New(
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendAfroMessageSMS(ctx context.Context, receiverPhone, message string) error {
|
||||||
|
apiKey := s.config.AFRO_SMS_API_KEY
|
||||||
|
senderName := s.config.AFRO_SMS_SENDER_NAME
|
||||||
|
|
||||||
|
baseURL := "https://api.afromessage.com"
|
||||||
|
endpoint := "/api/send"
|
||||||
|
|
||||||
|
request := afro.GetRequest(apiKey, endpoint, baseURL)
|
||||||
|
|
||||||
|
// MUST be POST
|
||||||
|
request.Method = "POST"
|
||||||
|
|
||||||
|
request.Sender(senderName)
|
||||||
|
request.To(receiverPhone, message)
|
||||||
|
|
||||||
|
response, err := afro.MakeRequestWithContext(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ack, ok := response["acknowledge"].(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unexpected SMS response format: %v", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ack != "success" {
|
||||||
|
return fmt.Errorf("SMS delivery failed: %v", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) SendAfroMessageSMSTemp(
|
func (s *Service) SendAfroMessageSMSTemp(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
receiverPhone string,
|
receiverPhone string,
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,69 @@ import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (s *Service) VerifyOtp(ctx context.Context, email, phone, otpCode string) error {
|
||||||
|
|
||||||
|
user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 1. Retrieve the OTP from the store
|
||||||
|
storedOtp, err := s.otpStore.GetOtp(ctx, user.UserName)
|
||||||
|
if err != nil {
|
||||||
|
return err // could be ErrOtpNotFound or other DB errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check if OTP was already used
|
||||||
|
if storedOtp.Used {
|
||||||
|
return domain.ErrOtpAlreadyUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check if OTP has expired
|
||||||
|
if time.Now().After(storedOtp.ExpiresAt) {
|
||||||
|
return domain.ErrOtpExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check if the provided OTP matches
|
||||||
|
if storedOtp.Otp != otpCode {
|
||||||
|
return domain.ErrInvalidOtp
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Mark OTP as used
|
||||||
|
storedOtp.Used = true
|
||||||
|
storedOtp.UsedAt = timePtr(time.Now())
|
||||||
|
|
||||||
|
if err := s.otpStore.MarkOtpAsUsed(ctx, storedOtp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) ResendOtp(
|
func (s *Service) ResendOtp(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userName string,
|
email, phone string,
|
||||||
) error {
|
) error {
|
||||||
|
|
||||||
|
user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
otpCode := helpers.GenerateOTP()
|
otpCode := helpers.GenerateOTP()
|
||||||
|
|
||||||
message := fmt.Sprintf(
|
message := fmt.Sprintf(
|
||||||
|
|
@ -22,7 +80,7 @@ func (s *Service) ResendOtp(
|
||||||
otpCode,
|
otpCode,
|
||||||
)
|
)
|
||||||
|
|
||||||
otp, err := s.otpStore.GetOtp(ctx, userName)
|
otp, err := s.otpStore.GetOtp(ctx, user.UserName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +106,7 @@ 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.UpdateOtp(ctx, otpCode, userName); err != nil {
|
if err := s.otpStore.UpdateOtp(ctx, otpCode, user.UserName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,54 +6,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) VerifyOtp(ctx context.Context, userName string, otpCode string) error {
|
|
||||||
// 1. Retrieve the OTP from the store
|
|
||||||
storedOtp, err := s.otpStore.GetOtp(ctx, userName)
|
|
||||||
if err != nil {
|
|
||||||
return err // could be ErrOtpNotFound or other DB errors
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Check if OTP was already used
|
|
||||||
if storedOtp.Used {
|
|
||||||
return domain.ErrOtpAlreadyUsed
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Check if OTP has expired
|
|
||||||
if time.Now().After(storedOtp.ExpiresAt) {
|
|
||||||
return domain.ErrOtpExpired
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Check if the provided OTP matches
|
|
||||||
if storedOtp.Otp != otpCode {
|
|
||||||
return domain.ErrInvalidOtp
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Mark OTP as used
|
|
||||||
storedOtp.Used = true
|
|
||||||
storedOtp.UsedAt = timePtr(time.Now())
|
|
||||||
|
|
||||||
if err := s.otpStore.MarkOtpAsUsed(ctx, storedOtp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
|
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
|
||||||
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
|
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,14 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (s *Service) GetUserByEmailPhone(
|
||||||
|
ctx context.Context,
|
||||||
|
email string,
|
||||||
|
phone string,
|
||||||
|
) (domain.User, error) {
|
||||||
|
return s.userStore.GetUserByEmailPhone(ctx, email, phone)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) IsUserPending(ctx context.Context, userName string) (bool, error) {
|
func (s *Service) IsUserPending(ctx context.Context, userName string) (bool, error) {
|
||||||
return s.userStore.IsUserPending(ctx, userName)
|
return s.userStore.IsUserPending(ctx, userName)
|
||||||
}
|
}
|
||||||
|
|
@ -65,3 +73,40 @@ func (s *Service) UpdateUser(ctx context.Context, req domain.UpdateUserReq) erro
|
||||||
func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
|
func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
|
||||||
return s.userStore.GetUserByID(ctx, id)
|
return s.userStore.GetUserByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllUsers retrieves users based on the provided filter
|
||||||
|
// func (s *Service) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) {
|
||||||
|
// var role *string
|
||||||
|
// if filter.Role != "" {
|
||||||
|
// role = &filter.Role
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var query *string
|
||||||
|
// if filter.Query.Valid {
|
||||||
|
// q := filter.Query.Value
|
||||||
|
// query = &q
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var createdBefore *time.Time
|
||||||
|
// if filter.CreatedBefore.Valid {
|
||||||
|
// b := filter.CreatedBefore.Value
|
||||||
|
// createdBefore = &b
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var createdAfter *time.Time
|
||||||
|
// if filter.CreatedAfter.Valid {
|
||||||
|
// a := filter.CreatedAfter.Value
|
||||||
|
// createdAfter = &a
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var limit int32 = 10
|
||||||
|
// var offset int32 = 0
|
||||||
|
// if filter.PageSize.Valid {
|
||||||
|
// limit = int32(filter.PageSize.Value)
|
||||||
|
// }
|
||||||
|
// if filter.Page.Valid && filter.PageSize.Valid {
|
||||||
|
// offset = int32(filter.Page.Value * filter.PageSize.Value)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return s.userStore.GetAllUsers(ctx, role, query, createdBefore, createdAfter, limit, offset)
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,12 @@ import (
|
||||||
|
|
||||||
// loginUserReq represents the request body for the Loginuser endpoint.
|
// loginUserReq represents the request body for the Loginuser endpoint.
|
||||||
type loginUserReq struct {
|
type loginUserReq struct {
|
||||||
UserName string `json:"user_name" validate:"required" example:"johndoe"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password" validate:"required" example:"password123"`
|
PhoneNumber string `json:"phone_number"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
OtpCode string `json:"otp_code"`
|
||||||
|
// UserName string `json:"user_name" validate:"required" example:"johndoe"`
|
||||||
|
// Password string `json:"password" validate:"required" example:"password123"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// loginUserRes represents the response body for the Loginuser endpoint.
|
// loginUserRes represents the response body for the Loginuser endpoint.
|
||||||
|
|
@ -24,6 +28,7 @@ type loginUserRes struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loginuser godoc
|
// Loginuser godoc
|
||||||
|
|
@ -32,14 +37,14 @@ type loginUserRes struct {
|
||||||
// @Tags auth
|
// @Tags auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param login body loginUserReq true "Login user"
|
// @Param login body authentication.LoginRequest true "Login user"
|
||||||
// @Success 200 {object} loginUserRes
|
// @Success 200 {object} loginUserRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /api/v1/{tenant_slug}/user-login [post]
|
// @Router /api/v1/{tenant_slug}/user-login [post]
|
||||||
func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
var req loginUserReq
|
var req authentication.LoginRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.mongoLoggerSvc.Info("Failed to parse LoginUser request",
|
h.mongoLoggerSvc.Info("Failed to parse LoginUser request",
|
||||||
zap.Int("status_code", fiber.StatusBadRequest),
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
|
@ -63,13 +68,14 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
successRes, err := h.authSvc.Login(c.Context(), req.UserName, req.Password)
|
successRes, err := h.authSvc.Login(c.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
||||||
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
|
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
|
||||||
zap.Int("status_code", fiber.StatusUnauthorized),
|
zap.Int("status_code", fiber.StatusUnauthorized),
|
||||||
zap.String("user_name", req.UserName),
|
zap.String("email", req.Email),
|
||||||
|
zap.String("phone_number", req.PhoneNumber),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
@ -80,7 +86,8 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
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),
|
||||||
zap.String("user_name", req.UserName),
|
zap.String("email", req.Email),
|
||||||
|
zap.String("phone_number", req.PhoneNumber),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
@ -105,7 +112,8 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
h.mongoLoggerSvc.Info("Login attempt: user login of other role",
|
h.mongoLoggerSvc.Info("Login attempt: user login of other role",
|
||||||
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("email", req.Email),
|
||||||
|
zap.String("phone_number", req.PhoneNumber),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||||
|
|
@ -137,6 +145,7 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
RefreshToken: successRes.RfToken,
|
RefreshToken: successRes.RfToken,
|
||||||
Role: string(successRes.Role),
|
Role: string(successRes.Role),
|
||||||
|
UserID: successRes.UserId,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.mongoLoggerSvc.Info("Login successful",
|
h.mongoLoggerSvc.Info("Login successful",
|
||||||
|
|
@ -154,7 +163,7 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// loginAdminReq represents the request body for the LoginAdmin endpoint.
|
// loginAdminReq represents the request body for the LoginAdmin endpoint.
|
||||||
type loginAdminReq struct {
|
type loginAdminReq struct {
|
||||||
UserName string `json:"user_name" validate:"required" example:"adminuser"`
|
Email string `json:"email" validate:"required" example:"adminuser"`
|
||||||
Password string `json:"password" validate:"required" example:"password123"`
|
Password string `json:"password" validate:"required" example:"password123"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,14 +180,14 @@ type LoginAdminRes struct {
|
||||||
// @Tags auth
|
// @Tags auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param login body loginAdminReq true "Login admin"
|
// @Param login body authentication.LoginRequest true "Login admin"
|
||||||
// @Success 200 {object} LoginAdminRes
|
// @Success 200 {object} LoginAdminRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /api/v1/{tenant_slug}/admin-login [post]
|
// @Router /api/v1/{tenant_slug}/admin-login [post]
|
||||||
func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
||||||
var req loginAdminReq
|
var req authentication.LoginRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
|
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
|
||||||
zap.Int("status_code", fiber.StatusBadRequest),
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
|
@ -196,13 +205,13 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, errMsg)
|
return fiber.NewError(fiber.StatusBadRequest, errMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
successRes, err := h.authSvc.Login(c.Context(), req.UserName, req.Password)
|
successRes, err := h.authSvc.Login(c.Context(), authentication.LoginRequest(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
||||||
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
|
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
|
||||||
zap.Int("status_code", fiber.StatusBadRequest),
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
zap.String("user_name", req.UserName),
|
zap.String("email", req.Email),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
@ -210,7 +219,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
||||||
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.StatusForbidden),
|
zap.Int("status_code", fiber.StatusForbidden),
|
||||||
zap.String("user_name", req.UserName),
|
zap.String("email", req.Email),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
@ -229,7 +238,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
||||||
h.mongoLoggerSvc.Warn("Login attempt: admin login of user",
|
h.mongoLoggerSvc.Warn("Login attempt: admin login of user",
|
||||||
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("email", req.Email),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
@ -251,6 +260,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
RefreshToken: successRes.RfToken,
|
RefreshToken: successRes.RfToken,
|
||||||
Role: string(successRes.Role),
|
Role: string(successRes.Role),
|
||||||
|
UserID: successRes.UserId,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.mongoLoggerSvc.Info("Login successful",
|
h.mongoLoggerSvc.Info("Login successful",
|
||||||
|
|
@ -269,14 +279,14 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
||||||
// @Tags auth
|
// @Tags auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param login body loginAdminReq true "Login super-admin"
|
// @Param login body authentication.LoginRequest true "Login super-admin"
|
||||||
// @Success 200 {object} LoginAdminRes
|
// @Success 200 {object} LoginAdminRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 401 {object} response.APIResponse
|
// @Failure 401 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /api/v1/super-login [post]
|
// @Router /api/v1/super-login [post]
|
||||||
func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
||||||
var req loginAdminReq
|
var req authentication.LoginRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
|
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
|
||||||
zap.Int("status_code", fiber.StatusBadRequest),
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
|
@ -294,13 +304,13 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, errMsg)
|
return fiber.NewError(fiber.StatusBadRequest, errMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
successRes, err := h.authSvc.Login(c.Context(), req.UserName, req.Password)
|
successRes, err := h.authSvc.Login(c.Context(), authentication.LoginRequest(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
||||||
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
|
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
|
||||||
zap.Int("status_code", fiber.StatusBadRequest),
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
zap.String("user_name", req.UserName),
|
zap.String("email", req.Email),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
@ -308,7 +318,7 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
||||||
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.StatusForbidden),
|
zap.Int("status_code", fiber.StatusForbidden),
|
||||||
zap.String("user_name", req.UserName),
|
zap.String("email", req.Email),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
@ -327,7 +337,7 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
||||||
h.mongoLoggerSvc.Warn("Login attempt: super-admin login of non-super-admin",
|
h.mongoLoggerSvc.Warn("Login attempt: super-admin login of non-super-admin",
|
||||||
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("email", req.Email),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
@ -384,6 +394,7 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var req refreshToken
|
var req refreshToken
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
@ -80,63 +77,63 @@ func (h *Handler) GetActiveAssessmentQuestions(c *fiber.Ctx) error {
|
||||||
// @Failure 404 {object} domain.ErrorResponse
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
// @Router /api/v1/{tenant_slug}/assessment/submit [post]
|
// @Router /api/v1/{tenant_slug}/assessment/submit [post]
|
||||||
func (h *Handler) SubmitAssessment(c *fiber.Ctx) error {
|
// func (h *Handler) SubmitAssessment(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// User ID (from auth context or path, depending on your setup)
|
// // User ID (from auth context or path, depending on your setup)
|
||||||
userIDStr, ok := c.Locals("user_id").(string)
|
// userIDStr, ok := c.Locals("user_id").(string)
|
||||||
if !ok || userIDStr == "" {
|
// if !ok || userIDStr == "" {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Invalid user context",
|
// Message: "Invalid user context",
|
||||||
Error: "User ID not found in request context",
|
// Error: "User ID not found in request context",
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
userID, err := strconv.ParseInt(userIDStr, 10, 64)
|
// userID, err := strconv.ParseInt(userIDStr, 10, 64)
|
||||||
if err != nil || userID <= 0 {
|
// if err != nil || userID <= 0 {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Invalid user ID",
|
// Message: "Invalid user ID",
|
||||||
Error: "User ID must be a positive integer",
|
// Error: "User ID must be a positive integer",
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Parse request body
|
// // Parse request body
|
||||||
var req domain.SubmitAssessmentReq
|
// var req domain.SubmitAssessmentReq
|
||||||
if err := c.BodyParser(&req); err != nil {
|
// if err := c.BodyParser(&req); err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Invalid request body",
|
// Message: "Invalid request body",
|
||||||
Error: err.Error(),
|
// Error: err.Error(),
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
if len(req.Answers) == 0 {
|
// if len(req.Answers) == 0 {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "No answers submitted",
|
// Message: "No answers submitted",
|
||||||
Error: "Assessment answers cannot be empty",
|
// Error: "Assessment answers cannot be empty",
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Submit assessment
|
// // Submit assessment
|
||||||
attempt, err := h.assessmentSvc.SubmitAssessment(
|
// attempt, err := h.assessmentSvc.SubmitAssessment(
|
||||||
c.Context(),
|
// c.Context(),
|
||||||
userID,
|
// userID,
|
||||||
req.Answers,
|
// req.Answers,
|
||||||
)
|
// )
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
if errors.Is(err, authentication.ErrUserNotFound) {
|
// if errors.Is(err, authentication.ErrUserNotFound) {
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
// return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
Message: "User not found",
|
// Message: "User not found",
|
||||||
Error: err.Error(),
|
// Error: err.Error(),
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
// return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to submit assessment",
|
// Message: "Failed to submit assessment",
|
||||||
Error: err.Error(),
|
// Error: err.Error(),
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
// return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
Message: "Assessment submitted successfully",
|
// Message: "Assessment submitted successfully",
|
||||||
Data: attempt,
|
// Data: attempt,
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -497,11 +497,10 @@ func (h *Handler) SendSingleAfroSMS(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send SMS via service
|
// Send SMS via service
|
||||||
if err := h.notificationSvc.SendAfroMessageSMSTemp(
|
if err := h.notificationSvc.SendAfroMessageSMS(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
req.Recipient,
|
req.Recipient,
|
||||||
req.Message,
|
req.Message,
|
||||||
nil,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
||||||
h.mongoLoggerSvc.Error("Failed to send AfroMessage SMS",
|
h.mongoLoggerSvc.Error("Failed to send AfroMessage SMS",
|
||||||
|
|
|
||||||
|
|
@ -174,10 +174,11 @@ func (h *Handler) ResendOtp(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.userSvc.GetUserByUserName(c.Context(), req.UserName)
|
user, err := h.userSvc.GetUserByEmailPhone(c.Context(), req.Email, req.PhoneNumber)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.mongoLoggerSvc.Info("Failed to get user by user name",
|
h.mongoLoggerSvc.Info("Failed to get user by user name",
|
||||||
zap.String("user_name", req.UserName),
|
zap.String("email", req.Email),
|
||||||
|
zap.String("phone_number", req.PhoneNumber),
|
||||||
zap.Int("status_code", fiber.StatusBadRequest),
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
|
|
@ -212,7 +213,8 @@ func (h *Handler) ResendOtp(c *fiber.Ctx) error {
|
||||||
|
|
||||||
if err := h.userSvc.ResendOtp(
|
if err := h.userSvc.ResendOtp(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
req.UserName,
|
req.Email,
|
||||||
|
req.PhoneNumber,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
||||||
h.mongoLoggerSvc.Error("Failed to resend OTP",
|
h.mongoLoggerSvc.Error("Failed to resend OTP",
|
||||||
|
|
@ -316,6 +318,123 @@ func (h *Handler) CheckUserPending(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllUsers godoc
|
||||||
|
// @Summary Get all users
|
||||||
|
// @Description Get users with optional filters
|
||||||
|
// @Tags user
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param role query string false "Role filter"
|
||||||
|
// @Param query query string false "Search query"
|
||||||
|
// @Param page query int false "Page number"
|
||||||
|
// @Param page_size query int false "Page size"
|
||||||
|
// @Param created_before query string false "Created before (RFC3339)"
|
||||||
|
// @Param created_after query string false "Created after (RFC3339)"
|
||||||
|
// @Success 200 {object} response.APIResponse
|
||||||
|
// @Failure 400 {object} response.APIResponse
|
||||||
|
// @Failure 500 {object} response.APIResponse
|
||||||
|
// @Router /api/v1/{tenant_slug}/users [get]
|
||||||
|
func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
|
||||||
|
searchQuery := c.Query("query")
|
||||||
|
searchString := domain.ValidString{
|
||||||
|
Value: searchQuery,
|
||||||
|
Valid: searchQuery != "",
|
||||||
|
}
|
||||||
|
|
||||||
|
createdBeforeQuery := c.Query("created_before")
|
||||||
|
var createdBefore domain.ValidTime
|
||||||
|
if createdBeforeQuery != "" {
|
||||||
|
parsed, err := time.Parse(time.RFC3339, createdBeforeQuery)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Info("invalid created_before format", "error", err)
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid created_before format")
|
||||||
|
}
|
||||||
|
createdBefore = domain.ValidTime{Value: parsed, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
createdAfterQuery := c.Query("created_after")
|
||||||
|
var createdAfter domain.ValidTime
|
||||||
|
if createdAfterQuery != "" {
|
||||||
|
parsed, err := time.Parse(time.RFC3339, createdAfterQuery)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Info("invalid created_after format", "error", err)
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid created_after format")
|
||||||
|
}
|
||||||
|
createdAfter = domain.ValidTime{Value: parsed, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := domain.UserFilter{
|
||||||
|
Role: c.Query("role"),
|
||||||
|
Page: domain.ValidInt{
|
||||||
|
Value: c.QueryInt("page", 1) - 1,
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
PageSize: domain.ValidInt{
|
||||||
|
Value: c.QueryInt("page_size", 10),
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
Query: searchString,
|
||||||
|
CreatedBefore: createdBefore,
|
||||||
|
CreatedAfter: createdAfter,
|
||||||
|
}
|
||||||
|
|
||||||
|
if valErrs, ok := h.validator.Validate(c, filter); !ok {
|
||||||
|
var errMsg string
|
||||||
|
for f, msg := range valErrs {
|
||||||
|
errMsg += fmt.Sprintf("%s: %s; ", f, msg)
|
||||||
|
}
|
||||||
|
h.mongoLoggerSvc.Info("invalid filter values in GetAllUsers request",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Any("validation_errors", valErrs),
|
||||||
|
zap.Time("timestamp", time.Now()))
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
users, total, err := h.userSvc.GetAllUsers(c.Context(), filter)
|
||||||
|
if err != nil {
|
||||||
|
h.mongoLoggerSvc.Error("failed to get users",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Any("filter", filter),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()))
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get users: "+err.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,
|
||||||
|
FavoutiteTopic: u.FavoutiteTopic,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
// VerifyOtp godoc
|
// VerifyOtp godoc
|
||||||
// @Summary Verify OTP
|
// @Summary Verify OTP
|
||||||
// @Description Verify OTP for registration or other actions
|
// @Description Verify OTP for registration or other actions
|
||||||
|
|
@ -353,7 +472,7 @@ func (h *Handler) VerifyOtp(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call service to verify OTP
|
// Call service to verify OTP
|
||||||
err := h.userSvc.VerifyOtp(c.Context(), req.UserName, req.Otp)
|
err := h.userSvc.VerifyOtp(c.Context(), req.Email, req.PhoneNumber, req.Otp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var errMsg string
|
var errMsg string
|
||||||
switch {
|
switch {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
groupV1.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)
|
||||||
|
|
@ -156,6 +156,7 @@ 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
|
||||||
|
groupV1.Get("/users", a.authMiddleware, h.GetAllUsers)
|
||||||
groupV1.Put("/user", a.authMiddleware, h.UpdateUser)
|
groupV1.Put("/user", a.authMiddleware, h.UpdateUser)
|
||||||
groupV1.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel)
|
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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user