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),
|
||||
// )
|
||||
|
||||
authSvc := authentication.NewService(
|
||||
repository.NewUserStore(store),
|
||||
repository.NewTokenStore(store),
|
||||
cfg.RefreshExpiry,
|
||||
)
|
||||
userSvc := user.NewService(
|
||||
repository.NewUserStore(store),
|
||||
repository.NewOTPStore(store),
|
||||
messengerSvc,
|
||||
cfg,
|
||||
)
|
||||
|
||||
authSvc := authentication.NewService(
|
||||
repository.NewUserStore(store),
|
||||
*userSvc,
|
||||
repository.NewTokenStore(store),
|
||||
cfg.RefreshExpiry,
|
||||
)
|
||||
// leagueSvc := league.New(repository.NewLeagueStore(store))
|
||||
// eventSvc := event.New(
|
||||
// cfg.Bet365Token,
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ CREATE TABLE assessment_attempts (
|
|||
|
||||
CREATE TABLE assessment_answers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
|
||||
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
|
||||
selected_option_id BIGINT REFERENCES assessment_question_options(id),
|
||||
short_answer TEXT,
|
||||
|
|
|
|||
|
|
@ -22,21 +22,21 @@ RETURNING
|
|||
knowledge_level,
|
||||
completed_at;
|
||||
|
||||
-- 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
|
||||
);
|
||||
-- -- 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
|
||||
-- );
|
||||
|
||||
-- name: GetAssessmentOptionByID :one
|
||||
SELECT
|
||||
|
|
|
|||
|
|
@ -120,14 +120,12 @@ SELECT
|
|||
education_level,
|
||||
country,
|
||||
region,
|
||||
|
||||
nick_name,
|
||||
occupation,
|
||||
learning_goal,
|
||||
language_goal,
|
||||
language_challange,
|
||||
favoutite_topic,
|
||||
|
||||
initial_assessment_completed,
|
||||
profile_picture_url,
|
||||
preferred_language,
|
||||
|
|
@ -135,30 +133,18 @@ SELECT
|
|||
phone_verified,
|
||||
status,
|
||||
profile_completed,
|
||||
preferred_language,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM users
|
||||
WHERE (
|
||||
role = $1 OR $1 IS NULL
|
||||
)
|
||||
AND (
|
||||
first_name ILIKE '%' || sqlc.narg('query') || '%'
|
||||
OR last_name ILIKE '%' || sqlc.narg('query') || '%'
|
||||
OR phone_number ILIKE '%' || sqlc.narg('query') || '%'
|
||||
OR email ILIKE '%' || sqlc.narg('query') || '%'
|
||||
OR sqlc.narg('query') IS NULL
|
||||
)
|
||||
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');
|
||||
WHERE ($1 IS NULL OR role = $1)
|
||||
AND ($2 IS NULL OR first_name ILIKE '%' || $2 || '%'
|
||||
OR last_name ILIKE '%' || $2 || '%'
|
||||
OR phone_number ILIKE '%' || $2 || '%'
|
||||
OR email ILIKE '%' || $2 || '%')
|
||||
AND ($3 IS NULL OR created_at >= $3)
|
||||
AND ($4 IS NULL OR created_at <= $4)
|
||||
LIMIT $5
|
||||
OFFSET $6;
|
||||
|
||||
-- name: GetTotalUsers :one
|
||||
SELECT COUNT(*)
|
||||
|
|
|
|||
384
docs/docs.go
384
docs/docs.go
|
|
@ -1504,7 +1504,7 @@ const docTemplate = `{
|
|||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.loginAdminReq"
|
||||
"$ref": "#/definitions/authentication.LoginRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
@ -2024,7 +2024,7 @@ const docTemplate = `{
|
|||
"in": "body",
|
||||
"required": true,
|
||||
"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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"post": {
|
||||
"description": "Login user",
|
||||
|
|
@ -2181,7 +2181,7 @@ const docTemplate = `{
|
|||
"in": "body",
|
||||
"required": true,
|
||||
"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": {
|
||||
"authentication.LoginRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"otp_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone_number": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.AssessmentOption": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -3078,11 +3168,11 @@ const docTemplate = `{
|
|||
},
|
||||
"domain.ResendOtpReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"user_name"
|
||||
],
|
||||
"properties": {
|
||||
"user_name": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone_number": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
|
@ -3120,21 +3210,6 @@ const docTemplate = `{
|
|||
"RoleSupport"
|
||||
]
|
||||
},
|
||||
"domain.SubmitAssessmentReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"answers"
|
||||
],
|
||||
"properties": {
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/definitions/domain.UserAnswer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.UpdateKnowledgeLevelReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -3147,22 +3222,71 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.UserAnswer": {
|
||||
"domain.UpdateUserReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isCorrect": {
|
||||
"type": "boolean"
|
||||
"age": {
|
||||
"$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",
|
||||
"format": "int64"
|
||||
},
|
||||
"selectedOptionID": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"shortAnswer": {
|
||||
"type": "string"
|
||||
"userName": {
|
||||
"$ref": "#/definitions/domain.ValidString"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -3268,17 +3392,52 @@ const docTemplate = `{
|
|||
"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": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"otp",
|
||||
"user_name"
|
||||
"otp"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"otp": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_name": {
|
||||
"phone_number": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -3556,6 +3681,9 @@ const docTemplate = `{
|
|||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1496,7 +1496,7 @@
|
|||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.loginAdminReq"
|
||||
"$ref": "#/definitions/authentication.LoginRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
@ -2016,7 +2016,7 @@
|
|||
"in": "body",
|
||||
"required": true,
|
||||
"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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"post": {
|
||||
"description": "Login user",
|
||||
|
|
@ -2173,7 +2173,7 @@
|
|||
"in": "body",
|
||||
"required": true,
|
||||
"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": {
|
||||
"authentication.LoginRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"otp_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone_number": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.AssessmentOption": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -3070,11 +3160,11 @@
|
|||
},
|
||||
"domain.ResendOtpReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"user_name"
|
||||
],
|
||||
"properties": {
|
||||
"user_name": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone_number": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
|
@ -3112,21 +3202,6 @@
|
|||
"RoleSupport"
|
||||
]
|
||||
},
|
||||
"domain.SubmitAssessmentReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"answers"
|
||||
],
|
||||
"properties": {
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/definitions/domain.UserAnswer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.UpdateKnowledgeLevelReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -3139,22 +3214,71 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.UserAnswer": {
|
||||
"domain.UpdateUserReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isCorrect": {
|
||||
"type": "boolean"
|
||||
"age": {
|
||||
"$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",
|
||||
"format": "int64"
|
||||
},
|
||||
"selectedOptionID": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"shortAnswer": {
|
||||
"type": "string"
|
||||
"userName": {
|
||||
"$ref": "#/definitions/domain.ValidString"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -3260,17 +3384,52 @@
|
|||
"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": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"otp",
|
||||
"user_name"
|
||||
"otp"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"otp": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_name": {
|
||||
"phone_number": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -3548,6 +3673,9 @@
|
|||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,15 @@
|
|||
definitions:
|
||||
authentication.LoginRequest:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
otp_code:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
phone_number:
|
||||
type: string
|
||||
type: object
|
||||
domain.AssessmentOption:
|
||||
properties:
|
||||
id:
|
||||
|
|
@ -300,10 +311,10 @@ definitions:
|
|||
type: object
|
||||
domain.ResendOtpReq:
|
||||
properties:
|
||||
user_name:
|
||||
email:
|
||||
type: string
|
||||
phone_number:
|
||||
type: string
|
||||
required:
|
||||
- user_name
|
||||
type: object
|
||||
domain.Response:
|
||||
properties:
|
||||
|
|
@ -330,16 +341,6 @@ definitions:
|
|||
- RoleStudent
|
||||
- RoleInstructor
|
||||
- RoleSupport
|
||||
domain.SubmitAssessmentReq:
|
||||
properties:
|
||||
answers:
|
||||
items:
|
||||
$ref: '#/definitions/domain.UserAnswer'
|
||||
minItems: 1
|
||||
type: array
|
||||
required:
|
||||
- answers
|
||||
type: object
|
||||
domain.UpdateKnowledgeLevelReq:
|
||||
properties:
|
||||
knowledge_level:
|
||||
|
|
@ -348,18 +349,49 @@ definitions:
|
|||
user_id:
|
||||
type: integer
|
||||
type: object
|
||||
domain.UserAnswer:
|
||||
domain.UpdateUserReq:
|
||||
properties:
|
||||
isCorrect:
|
||||
type: boolean
|
||||
questionID:
|
||||
age:
|
||||
$ref: '#/definitions/domain.ValidInt'
|
||||
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
|
||||
type: integer
|
||||
selectedOptionID:
|
||||
format: int64
|
||||
type: integer
|
||||
shortAnswer:
|
||||
type: string
|
||||
userName:
|
||||
$ref: '#/definitions/domain.ValidString'
|
||||
type: object
|
||||
domain.UserProfileResponse:
|
||||
properties:
|
||||
|
|
@ -431,15 +463,37 @@ definitions:
|
|||
- UserStatusActive
|
||||
- UserStatusSuspended
|
||||
- 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:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
otp:
|
||||
type: string
|
||||
user_name:
|
||||
phone_number:
|
||||
type: string
|
||||
required:
|
||||
- otp
|
||||
- user_name
|
||||
type: object
|
||||
handlers.AdminProfileRes:
|
||||
properties:
|
||||
|
|
@ -596,30 +650,6 @@ definitions:
|
|||
- message
|
||||
- recipient
|
||||
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:
|
||||
properties:
|
||||
access_token:
|
||||
|
|
@ -628,6 +658,8 @@ definitions:
|
|||
type: string
|
||||
role:
|
||||
type: string
|
||||
user_id:
|
||||
type: integer
|
||||
type: object
|
||||
handlers.logoutReq:
|
||||
properties:
|
||||
|
|
@ -708,7 +740,7 @@ paths:
|
|||
name: login
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.loginAdminReq'
|
||||
$ref: '#/definitions/authentication.LoginRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
|
@ -731,46 +763,6 @@ paths:
|
|||
summary: Login user
|
||||
tags:
|
||||
- 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:
|
||||
post:
|
||||
consumes:
|
||||
|
|
@ -801,6 +793,45 @@ paths:
|
|||
summary: Resend OTP
|
||||
tags:
|
||||
- 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:
|
||||
post:
|
||||
consumes:
|
||||
|
|
@ -812,7 +843,7 @@ paths:
|
|||
name: login
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.loginUserReq'
|
||||
$ref: '#/definitions/authentication.LoginRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
|
@ -1108,6 +1139,54 @@ paths:
|
|||
summary: Get user profile
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
consumes:
|
||||
|
|
@ -2023,7 +2102,7 @@ paths:
|
|||
name: login
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.loginAdminReq'
|
||||
$ref: '#/definitions/authentication.LoginRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
|
|
|||
|
|
@ -11,42 +11,6 @@ import (
|
|||
"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
|
||||
INSERT INTO assessment_attempts (
|
||||
user_id,
|
||||
|
|
@ -197,6 +161,7 @@ func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]Assessmen
|
|||
}
|
||||
|
||||
const GetAssessmentOptionByID = `-- name: GetAssessmentOptionByID :one
|
||||
|
||||
SELECT
|
||||
id,
|
||||
question_id,
|
||||
|
|
@ -207,6 +172,25 @@ WHERE id = $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) {
|
||||
row := q.db.QueryRow(ctx, GetAssessmentOptionByID, id)
|
||||
var i AssessmentQuestionOption
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
|
||||
type AssessmentAnswer struct {
|
||||
ID int64 `json:"id"`
|
||||
AttemptID int64 `json:"attempt_id"`
|
||||
QuestionID int64 `json:"question_id"`
|
||||
SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
|
||||
ShortAnswer pgtype.Text `json:"short_answer"`
|
||||
|
|
@ -198,6 +197,7 @@ type User struct {
|
|||
EducationLevel pgtype.Text `json:"education_level"`
|
||||
Country pgtype.Text `json:"country"`
|
||||
Region pgtype.Text `json:"region"`
|
||||
Medium string `json:"medium"`
|
||||
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
|
||||
NickName pgtype.Text `json:"nick_name"`
|
||||
Occupation pgtype.Text `json:"occupation"`
|
||||
|
|
|
|||
|
|
@ -267,14 +267,12 @@ SELECT
|
|||
education_level,
|
||||
country,
|
||||
region,
|
||||
|
||||
nick_name,
|
||||
occupation,
|
||||
learning_goal,
|
||||
language_goal,
|
||||
language_challange,
|
||||
favoutite_topic,
|
||||
|
||||
initial_assessment_completed,
|
||||
profile_picture_url,
|
||||
preferred_language,
|
||||
|
|
@ -282,39 +280,27 @@ SELECT
|
|||
phone_verified,
|
||||
status,
|
||||
profile_completed,
|
||||
preferred_language,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM users
|
||||
WHERE (
|
||||
role = $1 OR $1 IS NULL
|
||||
)
|
||||
AND (
|
||||
first_name ILIKE '%' || $2 || '%'
|
||||
WHERE ($1 IS NULL OR role = $1)
|
||||
AND ($2 IS NULL OR first_name ILIKE '%' || $2 || '%'
|
||||
OR last_name ILIKE '%' || $2 || '%'
|
||||
OR phone_number ILIKE '%' || $2 || '%'
|
||||
OR email ILIKE '%' || $2 || '%'
|
||||
OR $2 IS NULL
|
||||
)
|
||||
AND (
|
||||
created_at >= $3
|
||||
OR $3 IS NULL
|
||||
)
|
||||
AND (
|
||||
created_at <= $4
|
||||
OR $4 IS NULL
|
||||
)
|
||||
LIMIT $6
|
||||
OFFSET $5
|
||||
OR email ILIKE '%' || $2 || '%')
|
||||
AND ($3 IS NULL OR created_at >= $3)
|
||||
AND ($4 IS NULL OR created_at <= $4)
|
||||
LIMIT $5
|
||||
OFFSET $6
|
||||
`
|
||||
|
||||
type GetAllUsersParams struct {
|
||||
Role string `json:"role"`
|
||||
Query pgtype.Text `json:"query"`
|
||||
CreatedAfter pgtype.Timestamptz `json:"created_after"`
|
||||
CreatedBefore pgtype.Timestamptz `json:"created_before"`
|
||||
Offset pgtype.Int4 `json:"offset"`
|
||||
Limit pgtype.Int4 `json:"limit"`
|
||||
Column1 interface{} `json:"column_1"`
|
||||
Column2 interface{} `json:"column_2"`
|
||||
Column3 interface{} `json:"column_3"`
|
||||
Column4 interface{} `json:"column_4"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
type GetAllUsersRow struct {
|
||||
|
|
@ -343,19 +329,18 @@ type GetAllUsersRow struct {
|
|||
PhoneVerified bool `json:"phone_verified"`
|
||||
Status string `json:"status"`
|
||||
ProfileCompleted bool `json:"profile_completed"`
|
||||
PreferredLanguage_2 pgtype.Text `json:"preferred_language_2"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]GetAllUsersRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetAllUsers,
|
||||
arg.Role,
|
||||
arg.Query,
|
||||
arg.CreatedAfter,
|
||||
arg.CreatedBefore,
|
||||
arg.Offset,
|
||||
arg.Column1,
|
||||
arg.Column2,
|
||||
arg.Column3,
|
||||
arg.Column4,
|
||||
arg.Limit,
|
||||
arg.Offset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -390,7 +375,6 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
|
|||
&i.PhoneVerified,
|
||||
&i.Status,
|
||||
&i.ProfileCompleted,
|
||||
&i.PreferredLanguage_2,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
|
|
@ -439,6 +423,7 @@ SELECT
|
|||
language_challange,
|
||||
favoutite_topic,
|
||||
|
||||
medium,
|
||||
email_verified,
|
||||
phone_verified,
|
||||
status,
|
||||
|
|
@ -478,6 +463,7 @@ type GetUserByEmailPhoneRow struct {
|
|||
LanguageGoal pgtype.Text `json:"language_goal"`
|
||||
LanguageChallange pgtype.Text `json:"language_challange"`
|
||||
FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
|
||||
Medium string `json:"medium"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
PhoneVerified bool `json:"phone_verified"`
|
||||
Status string `json:"status"`
|
||||
|
|
@ -511,6 +497,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
|||
&i.LanguageGoal,
|
||||
&i.LanguageChallange,
|
||||
&i.FavoutiteTopic,
|
||||
&i.Medium,
|
||||
&i.EmailVerified,
|
||||
&i.PhoneVerified,
|
||||
&i.Status,
|
||||
|
|
@ -525,7 +512,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
|||
}
|
||||
|
||||
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
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -546,6 +533,7 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
|||
&i.EducationLevel,
|
||||
&i.Country,
|
||||
&i.Region,
|
||||
&i.Medium,
|
||||
&i.KnowledgeLevel,
|
||||
&i.NickName,
|
||||
&i.Occupation,
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ var (
|
|||
// }
|
||||
|
||||
type AFROSMSConfig struct {
|
||||
AfroSMSSenderName string `mapstructure:"afrom_sms_sender_name"`
|
||||
AfroSMSIdentifierID string `mapstructure:"afro_sms_identifier_id"`
|
||||
AfroSMSAPIKey string `mapstructure:"afro_sms_api_key"`
|
||||
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.AfroSMSIdentifierID = os.Getenv("AFRO_SMS_IDENTIFIER_ID")
|
||||
c.AFROSMSConfig.AfroSMSBaseURL = os.Getenv("AFRO_SMS_BASE_URL")
|
||||
c.AFROSMSConfig.AfroSMSSenderName = os.Getenv("AFRO_SMS_SENDER_NAME")
|
||||
|
||||
//Telebirr
|
||||
c.TELEBIRR.TelebirrBaseURL = os.Getenv("TELEBIRR_BASE_URL")
|
||||
|
|
|
|||
|
|
@ -40,10 +40,12 @@ type Otp 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"`
|
||||
}
|
||||
|
||||
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,
|
||||
) (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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/ports"
|
||||
"context"
|
||||
"math/big"
|
||||
|
||||
"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
|
||||
func (s *Store) SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error) {
|
||||
total := len(answers)
|
||||
correct := 0
|
||||
// func (s *Store) SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error) {
|
||||
// total := len(answers)
|
||||
// correct := 0
|
||||
|
||||
for _, ans := range answers {
|
||||
if ans.IsCorrect {
|
||||
correct++
|
||||
}
|
||||
}
|
||||
// for _, ans := range answers {
|
||||
// if ans.IsCorrect {
|
||||
// correct++
|
||||
// }
|
||||
// }
|
||||
|
||||
score := float64(correct) / float64(total) * 100
|
||||
knowledgeLevel := "BEGINNER"
|
||||
switch {
|
||||
case score >= 80:
|
||||
knowledgeLevel = "ADVANCED"
|
||||
case score >= 50:
|
||||
knowledgeLevel = "INTERMEDIATE"
|
||||
}
|
||||
// score := float64(correct) / float64(total) * 100
|
||||
// knowledgeLevel := "BEGINNER"
|
||||
// switch {
|
||||
// case score >= 80:
|
||||
// knowledgeLevel = "ADVANCED"
|
||||
// case score >= 50:
|
||||
// knowledgeLevel = "INTERMEDIATE"
|
||||
// }
|
||||
|
||||
// Save attempt
|
||||
attemptRow, err := s.queries.CreateAssessmentAttempt(ctx, dbgen.CreateAssessmentAttemptParams{
|
||||
UserID: userID,
|
||||
TotalQuestions: int32(total),
|
||||
CorrectAnswers: int32(correct),
|
||||
ScorePercentage: pgtype.Numeric{Int: big.NewInt(int64(score * 100)), Valid: true},
|
||||
KnowledgeLevel: knowledgeLevel,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.AssessmentAttempt{}, err
|
||||
}
|
||||
// // Save attempt
|
||||
// attemptRow, err := s.queries.CreateAssessmentAttempt(ctx, dbgen.CreateAssessmentAttemptParams{
|
||||
// UserID: userID,
|
||||
// TotalQuestions: int32(total),
|
||||
// CorrectAnswers: int32(correct),
|
||||
// ScorePercentage: pgtype.Numeric{Int: big.NewInt(int64(score * 100)), Valid: true},
|
||||
// KnowledgeLevel: knowledgeLevel,
|
||||
// })
|
||||
// if err != nil {
|
||||
// return domain.AssessmentAttempt{}, err
|
||||
// }
|
||||
|
||||
// Save answers
|
||||
for _, ans := range answers {
|
||||
err := s.queries.CreateAssessmentAnswer(ctx, dbgen.CreateAssessmentAnswerParams{
|
||||
AttemptID: attemptRow.ID,
|
||||
QuestionID: ans.QuestionID,
|
||||
SelectedOptionID: pgtype.Int8{Int64: ans.SelectedOptionID, Valid: true},
|
||||
ShortAnswer: pgtype.Text{String: ans.ShortAnswer, Valid: true},
|
||||
IsCorrect: ans.IsCorrect,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.AssessmentAttempt{}, err
|
||||
}
|
||||
}
|
||||
// // Save answers
|
||||
// for _, ans := range answers {
|
||||
// err := s.queries.CreateAssessmentAnswer(ctx, dbgen.CreateAssessmentAnswerParams{
|
||||
// AttemptID: attemptRow.ID,
|
||||
// QuestionID: ans.QuestionID,
|
||||
// SelectedOptionID: pgtype.Int8{Int64: ans.SelectedOptionID, Valid: true},
|
||||
// ShortAnswer: pgtype.Text{String: ans.ShortAnswer, Valid: true},
|
||||
// IsCorrect: ans.IsCorrect,
|
||||
// })
|
||||
// if err != nil {
|
||||
// return domain.AssessmentAttempt{}, err
|
||||
// }
|
||||
// }
|
||||
|
||||
return domain.AssessmentAttempt{
|
||||
ID: attemptRow.ID,
|
||||
UserID: userID,
|
||||
TotalQuestions: total,
|
||||
CorrectAnswers: correct,
|
||||
ScorePercentage: score,
|
||||
KnowledgeLevel: knowledgeLevel,
|
||||
CompletedAt: attemptRow.CompletedAt.Time,
|
||||
}, nil
|
||||
}
|
||||
// return domain.AssessmentAttempt{
|
||||
// ID: attemptRow.ID,
|
||||
// UserID: userID,
|
||||
// TotalQuestions: total,
|
||||
// CorrectAnswers: correct,
|
||||
// ScorePercentage: score,
|
||||
// KnowledgeLevel: knowledgeLevel,
|
||||
// CompletedAt: attemptRow.CompletedAt.Time,
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
// GetOptionByID fetches a single option to validate correctness
|
||||
func (s *Store) GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, error) {
|
||||
|
|
|
|||
|
|
@ -254,25 +254,41 @@ func (s *Store) GetAllUsers(
|
|||
limit, offset int32,
|
||||
) ([]domain.User, int64, error) {
|
||||
|
||||
params := dbgen.GetAllUsersParams{
|
||||
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||
var roleParam sql.NullString
|
||||
if role != nil && *role != "" {
|
||||
roleParam = sql.NullString{String: *role, Valid: true}
|
||||
} else {
|
||||
roleParam = sql.NullString{Valid: false} // This will make $1 IS NULL work
|
||||
}
|
||||
|
||||
if role != nil {
|
||||
params.Role = *role
|
||||
}
|
||||
|
||||
if query != nil {
|
||||
params.Query = pgtype.Text{String: *query, Valid: true}
|
||||
}
|
||||
|
||||
if createdBefore != nil {
|
||||
params.CreatedBefore = pgtype.Timestamptz{Time: *createdBefore, Valid: true}
|
||||
var queryParam sql.NullString
|
||||
if query != nil && *query != "" {
|
||||
queryParam = sql.NullString{String: *query, Valid: true}
|
||||
} else {
|
||||
queryParam = sql.NullString{Valid: false}
|
||||
}
|
||||
|
||||
var createdAfterParam sql.NullTime
|
||||
if createdAfter != nil {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Service) GetActiveAssessmentQuestions(
|
||||
|
|
@ -70,77 +69,77 @@ func (s *Service) CreateAssessmentQuestion(
|
|||
return s.initialAssessmentStore.CreateAssessmentQuestion(ctx, q)
|
||||
}
|
||||
|
||||
func (s *Service) SubmitAssessment(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
responses []domain.UserAnswer,
|
||||
) (domain.AssessmentAttempt, error) {
|
||||
// func (s *Service) SubmitAssessment(
|
||||
// ctx context.Context,
|
||||
// userID int64,
|
||||
// responses []domain.UserAnswer,
|
||||
// ) (domain.AssessmentAttempt, error) {
|
||||
|
||||
if userID <= 0 {
|
||||
return domain.AssessmentAttempt{}, errors.New("invalid user id")
|
||||
}
|
||||
// if userID <= 0 {
|
||||
// return domain.AssessmentAttempt{}, errors.New("invalid user id")
|
||||
// }
|
||||
|
||||
if len(responses) == 0 {
|
||||
return domain.AssessmentAttempt{}, errors.New("no responses submitted")
|
||||
}
|
||||
// if len(responses) == 0 {
|
||||
// return domain.AssessmentAttempt{}, errors.New("no responses submitted")
|
||||
// }
|
||||
|
||||
// Step 1: Validate and evaluate answers
|
||||
for i, ans := range responses {
|
||||
if ans.QuestionID == 0 {
|
||||
return domain.AssessmentAttempt{}, errors.New("invalid question id")
|
||||
}
|
||||
// // Step 1: Validate and evaluate answers
|
||||
// for i, ans := range responses {
|
||||
// if ans.QuestionID == 0 {
|
||||
// return domain.AssessmentAttempt{}, errors.New("invalid question id")
|
||||
// }
|
||||
|
||||
isCorrect, err := s.validateAnswer(ctx, ans)
|
||||
if err != nil {
|
||||
return domain.AssessmentAttempt{}, err
|
||||
}
|
||||
// isCorrect, err := s.validateAnswer(ctx, ans)
|
||||
// if err != nil {
|
||||
// return domain.AssessmentAttempt{}, err
|
||||
// }
|
||||
|
||||
responses[i].IsCorrect = isCorrect
|
||||
}
|
||||
// responses[i].IsCorrect = isCorrect
|
||||
// }
|
||||
|
||||
// Step 2: Persist assessment attempt + answers
|
||||
attempt, err := s.initialAssessmentStore.SaveAssessmentAttempt(
|
||||
ctx,
|
||||
userID,
|
||||
responses,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.AssessmentAttempt{}, err
|
||||
}
|
||||
// // Step 2: Persist assessment attempt + answers
|
||||
// attempt, err := s.initialAssessmentStore.SaveAssessmentAttempt(
|
||||
// ctx,
|
||||
// userID,
|
||||
// responses,
|
||||
// )
|
||||
// if err != nil {
|
||||
// return domain.AssessmentAttempt{}, err
|
||||
// }
|
||||
|
||||
// Step 3: Update user's knowledge level
|
||||
if err := s.userStore.UpdateUserKnowledgeLevel(
|
||||
ctx,
|
||||
userID,
|
||||
attempt.KnowledgeLevel,
|
||||
); err != nil {
|
||||
return domain.AssessmentAttempt{}, err
|
||||
}
|
||||
// // Step 3: Update user's knowledge level
|
||||
// if err := s.userStore.UpdateUserKnowledgeLevel(
|
||||
// ctx,
|
||||
// userID,
|
||||
// attempt.KnowledgeLevel,
|
||||
// ); err != nil {
|
||||
// return domain.AssessmentAttempt{}, err
|
||||
// }
|
||||
|
||||
// Step 4: Send in-app notification
|
||||
notification := &domain.Notification{
|
||||
// // Step 4: Send in-app notification
|
||||
// notification := &domain.Notification{
|
||||
|
||||
RecipientID: userID,
|
||||
Level: domain.NotificationLevelInfo,
|
||||
Reciever: domain.NotificationRecieverSideCustomer,
|
||||
IsRead: false,
|
||||
DeliveryStatus: domain.DeliveryStatusSent,
|
||||
DeliveryChannel: domain.DeliveryChannelInApp,
|
||||
Payload: domain.NotificationPayload{
|
||||
Headline: "Knowledge Assessment Completed",
|
||||
Message: "Your knowledge assessment is complete. Your knowledge level is " + attempt.KnowledgeLevel + ".",
|
||||
Tags: []string{"assessment", "knowledge-level"},
|
||||
},
|
||||
Timestamp: time.Now(),
|
||||
Type: domain.NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE,
|
||||
}
|
||||
// RecipientID: userID,
|
||||
// Level: domain.NotificationLevelInfo,
|
||||
// Reciever: domain.NotificationRecieverSideCustomer,
|
||||
// IsRead: false,
|
||||
// DeliveryStatus: domain.DeliveryStatusSent,
|
||||
// DeliveryChannel: domain.DeliveryChannelInApp,
|
||||
// Payload: domain.NotificationPayload{
|
||||
// Headline: "Knowledge Assessment Completed",
|
||||
// Message: "Your knowledge assessment is complete. Your knowledge level is " + attempt.KnowledgeLevel + ".",
|
||||
// Tags: []string{"assessment", "knowledge-level"},
|
||||
// },
|
||||
// Timestamp: time.Now(),
|
||||
// Type: domain.NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE,
|
||||
// }
|
||||
|
||||
if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
|
||||
return domain.AssessmentAttempt{}, err
|
||||
}
|
||||
// if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
|
||||
// return domain.AssessmentAttempt{}, err
|
||||
// }
|
||||
|
||||
return attempt, nil
|
||||
}
|
||||
// return attempt, nil
|
||||
// }
|
||||
|
||||
func (s *Service) validateAnswer(
|
||||
ctx context.Context,
|
||||
|
|
|
|||
|
|
@ -26,13 +26,22 @@ type LoginSuccess struct {
|
|||
RfToken string
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email"`
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
Password string `json:"password"`
|
||||
OTPCode string `json:"otp_code"`
|
||||
}
|
||||
|
||||
func (s *Service) Login(
|
||||
ctx context.Context,
|
||||
userName, password string,
|
||||
req LoginRequest,
|
||||
) (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 not found by username, try email or phone lookup using the same identifier
|
||||
return LoginSuccess{}, err
|
||||
}
|
||||
|
||||
|
|
@ -40,16 +49,21 @@ func (s *Service) Login(
|
|||
return LoginSuccess{}, domain.ErrUserNotVerified
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if err := matchPassword(password, user.Password); err != nil {
|
||||
return LoginSuccess{}, err
|
||||
}
|
||||
|
||||
// Status check instead of Suspended
|
||||
if user.Status == domain.UserStatusSuspended {
|
||||
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
|
||||
oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
|
||||
if err != nil && !errors.Is(err, ErrRefreshTokenNotFound) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package authentication
|
||||
|
||||
import "Yimaru-Backend/internal/ports"
|
||||
import (
|
||||
"Yimaru-Backend/internal/ports"
|
||||
"Yimaru-Backend/internal/services/user"
|
||||
)
|
||||
|
||||
// type EmailPhone struct {
|
||||
// Email ValidString
|
||||
|
|
@ -17,13 +20,15 @@ type Tokens struct {
|
|||
}
|
||||
type Service struct {
|
||||
userStore ports.UserStore
|
||||
UserSvc user.Service
|
||||
tokenStore ports.TokenStore
|
||||
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{
|
||||
userStore: userStore,
|
||||
UserSvc: userSvc,
|
||||
tokenStore: tokenStore,
|
||||
RefreshExpiry: RefreshExpiry,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ func (s *Service) SendSMS(ctx context.Context, receiverPhone, message string) er
|
|||
|
||||
switch settingsList.SMSProvider {
|
||||
case domain.AfroMessage:
|
||||
return s.SendAfroMessageSMSLatest(ctx, receiverPhone, message, nil)
|
||||
return s.SendAfroMessageSMS(ctx, receiverPhone, message)
|
||||
case domain.TwilioSms:
|
||||
return s.SendTwilioSMS(ctx, receiverPhone, message)
|
||||
default:
|
||||
|
|
@ -44,12 +44,14 @@ func (s *Service) SendAfroMessageSMS(ctx context.Context, receiverPhone, message
|
|||
// API endpoint has been updated
|
||||
// TODO: no need for package for the afro message operations (pretty simple stuff)
|
||||
request := afro.GetRequest(apiKey, endpoint, hostURL)
|
||||
request.BaseURL = "https://api.afromessage.com/api/send"
|
||||
request.BaseURL = "https://api.afromessage.com"
|
||||
|
||||
request.Method = "GET"
|
||||
request.Sender(senderName)
|
||||
request.To(receiverPhone, message)
|
||||
|
||||
fmt.Printf("the afro SMS request is: %v", request)
|
||||
|
||||
response, err := afro.MakeRequestWithContext(ctx, request)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -76,7 +78,7 @@ func (s *Service) SendAfroMessageSMSLatest(
|
|||
params := url.Values{}
|
||||
params.Set("to", receiverPhone)
|
||||
params.Set("message", message)
|
||||
params.Set("sender", s.config.AFRO_SMS_SENDER_NAME)
|
||||
params.Set("sender", s.config.AFROSMSConfig.AfroSMSSenderName)
|
||||
|
||||
// Optional parameters
|
||||
if s.config.AFROSMSConfig.AfroSMSIdentifierID != "" {
|
||||
|
|
@ -88,7 +90,7 @@ func (s *Service) SendAfroMessageSMSLatest(
|
|||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
|
|
@ -96,7 +98,7 @@ func (s *Service) SendAfroMessageSMSLatest(
|
|||
}
|
||||
|
||||
// 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")
|
||||
|
||||
client := &http.Client{
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
// "github.com/segmentio/kafka-go"
|
||||
"go.uber.org/zap"
|
||||
// afro "github.com/amanuelabay/afrosms-go"
|
||||
afro "github.com/amanuelabay/afrosms-go"
|
||||
"github.com/gorilla/websocket"
|
||||
// "github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
|
@ -72,6 +73,38 @@ func New(
|
|||
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(
|
||||
ctx context.Context,
|
||||
receiverPhone string,
|
||||
|
|
|
|||
|
|
@ -10,11 +10,69 @@ import (
|
|||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func (s *Service) VerifyOtp(ctx context.Context, email, phone, otpCode string) error {
|
||||
|
||||
user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 1. Retrieve the OTP from the store
|
||||
storedOtp, err := s.otpStore.GetOtp(ctx, user.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(
|
||||
ctx context.Context,
|
||||
userName string,
|
||||
email, phone string,
|
||||
) error {
|
||||
|
||||
user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
otpCode := helpers.GenerateOTP()
|
||||
|
||||
message := fmt.Sprintf(
|
||||
|
|
@ -22,7 +80,7 @@ func (s *Service) ResendOtp(
|
|||
otpCode,
|
||||
)
|
||||
|
||||
otp, err := s.otpStore.GetOtp(ctx, userName)
|
||||
otp, err := s.otpStore.GetOtp(ctx, user.UserName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -48,7 +106,7 @@ func (s *Service) ResendOtp(
|
|||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,54 +6,6 @@ import (
|
|||
"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
|
||||
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,14 @@ import (
|
|||
"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) {
|
||||
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) {
|
||||
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.
|
||||
type loginUserReq struct {
|
||||
UserName string `json:"user_name" validate:"required" example:"johndoe"`
|
||||
Password string `json:"password" validate:"required" example:"password123"`
|
||||
Email string `json:"email"`
|
||||
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.
|
||||
|
|
@ -24,6 +28,7 @@ type loginUserRes struct {
|
|||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Role string `json:"role"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
// Loginuser godoc
|
||||
|
|
@ -32,14 +37,14 @@ type loginUserRes struct {
|
|||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param login body loginUserReq true "Login user"
|
||||
// @Param login body authentication.LoginRequest true "Login user"
|
||||
// @Success 200 {object} loginUserRes
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/v1/{tenant_slug}/user-login [post]
|
||||
func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
||||
var req loginUserReq
|
||||
var req authentication.LoginRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
h.mongoLoggerSvc.Info("Failed to parse LoginUser request",
|
||||
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 {
|
||||
switch {
|
||||
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
||||
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
|
||||
zap.Int("status_code", fiber.StatusUnauthorized),
|
||||
zap.String("user_name", req.UserName),
|
||||
zap.String("email", req.Email),
|
||||
zap.String("phone_number", req.PhoneNumber),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
|
|
@ -80,7 +86,8 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
|||
case errors.Is(err, authentication.ErrUserSuspended):
|
||||
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
|
||||
zap.Int("status_code", fiber.StatusUnauthorized),
|
||||
zap.String("user_name", req.UserName),
|
||||
zap.String("email", req.Email),
|
||||
zap.String("phone_number", req.PhoneNumber),
|
||||
zap.Error(err),
|
||||
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",
|
||||
zap.Int("status_code", fiber.StatusForbidden),
|
||||
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()),
|
||||
)
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
|
|
@ -137,6 +145,7 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
|
|||
AccessToken: accessToken,
|
||||
RefreshToken: successRes.RfToken,
|
||||
Role: string(successRes.Role),
|
||||
UserID: successRes.UserId,
|
||||
}
|
||||
|
||||
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.
|
||||
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"`
|
||||
}
|
||||
|
||||
|
|
@ -171,14 +180,14 @@ type LoginAdminRes struct {
|
|||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param login body loginAdminReq true "Login admin"
|
||||
// @Param login body authentication.LoginRequest true "Login admin"
|
||||
// @Success 200 {object} LoginAdminRes
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/v1/{tenant_slug}/admin-login [post]
|
||||
func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
||||
var req loginAdminReq
|
||||
var req authentication.LoginRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
|
||||
zap.Int("status_code", fiber.StatusBadRequest),
|
||||
|
|
@ -196,13 +205,13 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
|||
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 {
|
||||
switch {
|
||||
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
||||
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
|
||||
zap.Int("status_code", fiber.StatusBadRequest),
|
||||
zap.String("user_name", req.UserName),
|
||||
zap.String("email", req.Email),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
|
|
@ -210,7 +219,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
|||
case errors.Is(err, authentication.ErrUserSuspended):
|
||||
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
|
||||
zap.Int("status_code", fiber.StatusForbidden),
|
||||
zap.String("user_name", req.UserName),
|
||||
zap.String("email", req.Email),
|
||||
zap.Error(err),
|
||||
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",
|
||||
zap.Int("status_code", fiber.StatusForbidden),
|
||||
zap.String("role", string(successRes.Role)),
|
||||
zap.String("user_name", req.UserName),
|
||||
zap.String("email", req.Email),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
|
|
@ -251,6 +260,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
|||
AccessToken: accessToken,
|
||||
RefreshToken: successRes.RfToken,
|
||||
Role: string(successRes.Role),
|
||||
UserID: successRes.UserId,
|
||||
}
|
||||
|
||||
h.mongoLoggerSvc.Info("Login successful",
|
||||
|
|
@ -269,14 +279,14 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
|
|||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param login body loginAdminReq true "Login super-admin"
|
||||
// @Param login body authentication.LoginRequest true "Login super-admin"
|
||||
// @Success 200 {object} LoginAdminRes
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /api/v1/super-login [post]
|
||||
func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
||||
var req loginAdminReq
|
||||
var req authentication.LoginRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
|
||||
zap.Int("status_code", fiber.StatusBadRequest),
|
||||
|
|
@ -294,13 +304,13 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
|||
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 {
|
||||
switch {
|
||||
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
||||
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
|
||||
zap.Int("status_code", fiber.StatusBadRequest),
|
||||
zap.String("user_name", req.UserName),
|
||||
zap.String("email", req.Email),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
|
|
@ -308,7 +318,7 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
|
|||
case errors.Is(err, authentication.ErrUserSuspended):
|
||||
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
|
||||
zap.Int("status_code", fiber.StatusForbidden),
|
||||
zap.String("user_name", req.UserName),
|
||||
zap.String("email", req.Email),
|
||||
zap.Error(err),
|
||||
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",
|
||||
zap.Int("status_code", fiber.StatusForbidden),
|
||||
zap.String("role", string(successRes.Role)),
|
||||
zap.String("user_name", req.UserName),
|
||||
zap.String("email", req.Email),
|
||||
zap.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
)
|
||||
|
|
@ -384,6 +394,7 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error {
|
|||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Role string `json:"role"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
var req refreshToken
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@ package handlers
|
|||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/services/authentication"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
|
@ -80,63 +77,63 @@ func (h *Handler) GetActiveAssessmentQuestions(c *fiber.Ctx) error {
|
|||
// @Failure 404 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @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)
|
||||
userIDStr, ok := c.Locals("user_id").(string)
|
||||
if !ok || userIDStr == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid user context",
|
||||
Error: "User ID not found in request context",
|
||||
})
|
||||
}
|
||||
// // User ID (from auth context or path, depending on your setup)
|
||||
// userIDStr, ok := c.Locals("user_id").(string)
|
||||
// if !ok || userIDStr == "" {
|
||||
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
// Message: "Invalid user context",
|
||||
// Error: "User ID not found in request context",
|
||||
// })
|
||||
// }
|
||||
|
||||
userID, err := strconv.ParseInt(userIDStr, 10, 64)
|
||||
if err != nil || userID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid user ID",
|
||||
Error: "User ID must be a positive integer",
|
||||
})
|
||||
}
|
||||
// userID, err := strconv.ParseInt(userIDStr, 10, 64)
|
||||
// if err != nil || userID <= 0 {
|
||||
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
// Message: "Invalid user ID",
|
||||
// Error: "User ID must be a positive integer",
|
||||
// })
|
||||
// }
|
||||
|
||||
// Parse request body
|
||||
var req domain.SubmitAssessmentReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
// // Parse request body
|
||||
// var req domain.SubmitAssessmentReq
|
||||
// if err := c.BodyParser(&req); err != nil {
|
||||
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
// Message: "Invalid request body",
|
||||
// Error: err.Error(),
|
||||
// })
|
||||
// }
|
||||
|
||||
if len(req.Answers) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "No answers submitted",
|
||||
Error: "Assessment answers cannot be empty",
|
||||
})
|
||||
}
|
||||
// if len(req.Answers) == 0 {
|
||||
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
// Message: "No answers submitted",
|
||||
// Error: "Assessment answers cannot be empty",
|
||||
// })
|
||||
// }
|
||||
|
||||
// Submit assessment
|
||||
attempt, err := h.assessmentSvc.SubmitAssessment(
|
||||
c.Context(),
|
||||
userID,
|
||||
req.Answers,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, authentication.ErrUserNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "User not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
// // Submit assessment
|
||||
// attempt, err := h.assessmentSvc.SubmitAssessment(
|
||||
// c.Context(),
|
||||
// userID,
|
||||
// req.Answers,
|
||||
// )
|
||||
// if err != nil {
|
||||
// if errors.Is(err, authentication.ErrUserNotFound) {
|
||||
// return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
// Message: "User not found",
|
||||
// Error: err.Error(),
|
||||
// })
|
||||
// }
|
||||
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to submit assessment",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
// return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
// Message: "Failed to submit assessment",
|
||||
// Error: err.Error(),
|
||||
// })
|
||||
// }
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Assessment submitted successfully",
|
||||
Data: attempt,
|
||||
})
|
||||
}
|
||||
// return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
// Message: "Assessment submitted successfully",
|
||||
// Data: attempt,
|
||||
// })
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -497,11 +497,10 @@ func (h *Handler) SendSingleAfroSMS(c *fiber.Ctx) error {
|
|||
}
|
||||
|
||||
// Send SMS via service
|
||||
if err := h.notificationSvc.SendAfroMessageSMSTemp(
|
||||
if err := h.notificationSvc.SendAfroMessageSMS(
|
||||
c.Context(),
|
||||
req.Recipient,
|
||||
req.Message,
|
||||
nil,
|
||||
); err != nil {
|
||||
|
||||
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 {
|
||||
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.Error(err),
|
||||
zap.Time("timestamp", time.Now()),
|
||||
|
|
@ -212,7 +213,8 @@ func (h *Handler) ResendOtp(c *fiber.Ctx) error {
|
|||
|
||||
if err := h.userSvc.ResendOtp(
|
||||
c.Context(),
|
||||
req.UserName,
|
||||
req.Email,
|
||||
req.PhoneNumber,
|
||||
); err != nil {
|
||||
|
||||
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
|
||||
// @Summary Verify OTP
|
||||
// @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
|
||||
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 {
|
||||
var errMsg string
|
||||
switch {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ func (a *App) initAppRoutes() {
|
|||
//assessment Routes
|
||||
groupV1.Post("/assessment/questions", h.CreateAssessmentQuestion)
|
||||
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
|
||||
groupV1.Post("/course-categories", h.CreateCourseCategory)
|
||||
|
|
@ -156,6 +156,7 @@ func (a *App) initAppRoutes() {
|
|||
// groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler
|
||||
|
||||
// User Routes
|
||||
groupV1.Get("/users", a.authMiddleware, h.GetAllUsers)
|
||||
groupV1.Put("/user", a.authMiddleware, h.UpdateUser)
|
||||
groupV1.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel)
|
||||
groupV1.Get("/user/:user_name/is-unique", h.CheckUserNameUnique)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user