schema adjustment and profile management fixes

This commit is contained in:
Yared Yemane 2025-12-31 07:53:59 -08:00
parent 8ed0a5f1c6
commit d94774c138
19 changed files with 416 additions and 224 deletions

View File

@ -1,40 +1,3 @@
CREATE TABLE assessment_questions (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
question_type VARCHAR(50) NOT NULL, -- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
difficulty_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE TABLE assessment_question_options (
id BIGSERIAL PRIMARY KEY,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
option_text TEXT NOT NULL,
is_correct BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE assessment_attempts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total_questions INT NOT NULL,
correct_answers INT NOT NULL,
score_percentage NUMERIC(5,2) NOT NULL,
knowledge_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE assessment_answers (
id BIGSERIAL PRIMARY KEY,
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
selected_option_id BIGINT REFERENCES assessment_question_options(id),
short_answer TEXT,
is_correct BOOLEAN NOT NULL
);
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
first_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL,
@ -73,6 +36,43 @@ CREATE TABLE IF NOT EXISTS users (
CHECK (email IS NOT NULL OR phone_number IS NOT NULL) CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
); );
CREATE TABLE assessment_questions (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
question_type VARCHAR(50) NOT NULL, -- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
difficulty_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE TABLE assessment_question_options (
id BIGSERIAL PRIMARY KEY,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
option_text TEXT NOT NULL,
is_correct BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE assessment_attempts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total_questions INT NOT NULL,
correct_answers INT NOT NULL,
score_percentage NUMERIC(5,2) NOT NULL,
knowledge_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE assessment_answers (
id BIGSERIAL PRIMARY KEY,
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
selected_option_id BIGINT REFERENCES assessment_question_options(id),
short_answer TEXT,
is_correct BOOLEAN NOT NULL
);
CREATE TABLE refresh_tokens ( CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,

View File

@ -5,13 +5,18 @@ SET
used = FALSE, used = FALSE,
used_at = NULL, used_at = NULL,
expires_at = $3 expires_at = $3
WHERE WHERE user_name = $1;
user_name = $1
AND expires_at <= NOW();
-- name: CreateOtp :exec -- name: CreateOtp :exec
INSERT INTO otps (user_name, sent_to, medium, otp_for, otp, used, created_at, expires_at) INSERT INTO otps (
VALUES ($1, $2, $3, $4, $5, FALSE, $6, $7); user_name,
sent_to,
medium,
otp_for,
otp,
expires_at
)
VALUES ($1, $2, $3, $4, $5, $6);
-- name: GetOtp :one -- name: GetOtp :one
SELECT id, user_name, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at SELECT id, user_name, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at

View File

@ -210,30 +210,29 @@ WHERE (
-- name: UpdateUser :exec -- name: UpdateUser :exec
UPDATE users UPDATE users
SET SET
first_name = $1, first_name = COALESCE($1, first_name),
last_name = $2, last_name = COALESCE($2, last_name),
user_name = $3, user_name = COALESCE($3, user_name),
age = $4, knowledge_level = COALESCE($4, knowledge_level),
education_level = $5, age = COALESCE($5, age),
country = $6, education_level = COALESCE($6, education_level),
region = $7, country = COALESCE($7, country),
region = COALESCE($8, region),
nick_name = $8, nick_name = COALESCE($9, nick_name),
occupation = $9, occupation = COALESCE($10, occupation),
learning_goal = $10, learning_goal = COALESCE($11, learning_goal),
language_goal = $11, language_goal = COALESCE($12, language_goal),
language_challange = $12, language_challange = COALESCE($13, language_challange),
favoutite_topic = $13, favoutite_topic = COALESCE($14, favoutite_topic),
initial_assessment_completed = COALESCE($15, initial_assessment_completed),
initial_assessment_completed = $14, email_verified = COALESCE($16, email_verified),
email_verified = $15, phone_verified = COALESCE($17, phone_verified),
phone_verified = $16, status = COALESCE($18, status),
status = $17, profile_completed = COALESCE($19, profile_completed),
profile_completed = $18, profile_picture_url = COALESCE($20, profile_picture_url),
profile_picture_url = $19, preferred_language = COALESCE($21, preferred_language),
preferred_language = $20,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $21; WHERE id = $22;
-- name: DeleteUser :exec -- name: DeleteUser :exec
DELETE FROM users DELETE FROM users

View File

@ -21,6 +21,23 @@ services:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
- ./exports:/exports - ./exports:/exports
pgadmin:
container_name: yimaru-pgadmin
image: dpage/pgadmin4:latest
restart: always
ports:
- "5050:80"
environment:
PGADMIN_DEFAULT_EMAIL: admin@local.dev
PGADMIN_DEFAULT_PASSWORD: admin
depends_on:
postgres:
condition: service_healthy
networks:
- app
volumes:
- pgadmin_data:/var/lib/pgadmin
mongo: mongo:
container_name: yimaru-mongo container_name: yimaru-mongo
image: mongo:7.0.11 image: mongo:7.0.11
@ -53,21 +70,11 @@ services:
"/migrations", "/migrations",
"-database", "-database",
"postgresql://root:secret@postgres:5432/gh?sslmode=disable", "postgresql://root:secret@postgres:5432/gh?sslmode=disable",
"up", "up"
] ]
networks: networks:
- app - app
# redis:
# image: redis:7-alpine
# ports:
# - "6379:6379"
# networks:
# - app
# healthcheck:
# test: ["CMD", "redis-cli", "ping"]
# interval: 10s
# timeout: 5s
# retries: 5
app: app:
build: build:
context: . context: .
@ -84,8 +91,6 @@ services:
networks: networks:
- app - app
command: ["/app/bin/web"] command: ["/app/bin/web"]
# volumes:
# - "C:/Users/User/Desktop:/host-desktop"
test: test:
build: build:
@ -105,3 +110,4 @@ networks:
volumes: volumes:
postgres_data: postgres_data:
mongo_data: mongo_data:
pgadmin_data:

View File

@ -12,8 +12,15 @@ import (
) )
const CreateOtp = `-- name: CreateOtp :exec const CreateOtp = `-- name: CreateOtp :exec
INSERT INTO otps (user_name, sent_to, medium, otp_for, otp, used, created_at, expires_at) INSERT INTO otps (
VALUES ($1, $2, $3, $4, $5, FALSE, $6, $7) user_name,
sent_to,
medium,
otp_for,
otp,
expires_at
)
VALUES ($1, $2, $3, $4, $5, $6)
` `
type CreateOtpParams struct { type CreateOtpParams struct {
@ -22,7 +29,6 @@ type CreateOtpParams struct {
Medium string `json:"medium"` Medium string `json:"medium"`
OtpFor string `json:"otp_for"` OtpFor string `json:"otp_for"`
Otp string `json:"otp"` Otp string `json:"otp"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"` ExpiresAt pgtype.Timestamptz `json:"expires_at"`
} }
@ -33,7 +39,6 @@ func (q *Queries) CreateOtp(ctx context.Context, arg CreateOtpParams) error {
arg.Medium, arg.Medium,
arg.OtpFor, arg.OtpFor,
arg.Otp, arg.Otp,
arg.CreatedAt,
arg.ExpiresAt, arg.ExpiresAt,
) )
return err return err
@ -100,9 +105,7 @@ SET
used = FALSE, used = FALSE,
used_at = NULL, used_at = NULL,
expires_at = $3 expires_at = $3
WHERE WHERE user_name = $1
user_name = $1
AND expires_at <= NOW()
` `
type UpdateExpiredOtpParams struct { type UpdateExpiredOtpParams struct {

View File

@ -842,36 +842,36 @@ func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams)
const UpdateUser = `-- name: UpdateUser :exec const UpdateUser = `-- name: UpdateUser :exec
UPDATE users UPDATE users
SET SET
first_name = $1, first_name = COALESCE($1, first_name),
last_name = $2, last_name = COALESCE($2, last_name),
user_name = $3, user_name = COALESCE($3, user_name),
age = $4, knowledge_level = COALESCE($4, knowledge_level),
education_level = $5, age = COALESCE($5, age),
country = $6, education_level = COALESCE($6, education_level),
region = $7, country = COALESCE($7, country),
region = COALESCE($8, region),
nick_name = $8, nick_name = COALESCE($9, nick_name),
occupation = $9, occupation = COALESCE($10, occupation),
learning_goal = $10, learning_goal = COALESCE($11, learning_goal),
language_goal = $11, language_goal = COALESCE($12, language_goal),
language_challange = $12, language_challange = COALESCE($13, language_challange),
favoutite_topic = $13, favoutite_topic = COALESCE($14, favoutite_topic),
initial_assessment_completed = COALESCE($15, initial_assessment_completed),
initial_assessment_completed = $14, email_verified = COALESCE($16, email_verified),
email_verified = $15, phone_verified = COALESCE($17, phone_verified),
phone_verified = $16, status = COALESCE($18, status),
status = $17, profile_completed = COALESCE($19, profile_completed),
profile_completed = $18, profile_picture_url = COALESCE($20, profile_picture_url),
profile_picture_url = $19, preferred_language = COALESCE($21, preferred_language),
preferred_language = $20,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $21 WHERE id = $22
` `
type UpdateUserParams struct { type UpdateUserParams struct {
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
UserName string `json:"user_name"` UserName string `json:"user_name"`
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
Age pgtype.Int4 `json:"age"` Age pgtype.Int4 `json:"age"`
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
@ -897,6 +897,7 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
arg.FirstName, arg.FirstName,
arg.LastName, arg.LastName,
arg.UserName, arg.UserName,
arg.KnowledgeLevel,
arg.Age, arg.Age,
arg.EducationLevel, arg.EducationLevel,
arg.Country, arg.Country,

View File

@ -46,6 +46,7 @@ type User struct {
Region string Region string
// Profile fields // Profile fields
KnowledgeLevel string
initial_assessment_completed bool initial_assessment_completed bool
NickName string NickName string
Occupation string Occupation string
@ -185,6 +186,7 @@ type UpdateUserReq struct {
Region ValidString Region ValidString
// Profile fields // Profile fields
KnowledgeLevel ValidString
NickName ValidString NickName ValidString
Occupation ValidString Occupation ValidString
LearningGoal ValidString LearningGoal ValidString

View File

@ -9,6 +9,7 @@ import (
) )
type UserStore interface { type UserStore interface {
UpdateUserStatus(ctx context.Context, user domain.UpdateUserReq) error
GetCorrectOptionForQuestion( GetCorrectOptionForQuestion(
ctx context.Context, ctx context.Context,
questionID int64, questionID int64,
@ -67,7 +68,7 @@ type EmailGateway interface {
SendEmailOTP(ctx context.Context, email string, otp string) error SendEmailOTP(ctx context.Context, email string, otp string) error
} }
type OtpStore interface { type OtpStore interface {
UpdateExpiredOtp(ctx context.Context, otp, userName string) error UpdateOtp(ctx context.Context, otp, userName string) error
MarkOtpAsUsed(ctx context.Context, otp domain.Otp) error MarkOtpAsUsed(ctx context.Context, otp domain.Otp) error
CreateOtp(ctx context.Context, otp domain.Otp) error CreateOtp(ctx context.Context, otp domain.Otp) error
GetOtp(ctx context.Context, userName string) (domain.Otp, error) GetOtp(ctx context.Context, userName string) (domain.Otp, error)

View File

@ -21,11 +21,19 @@ func NewTokenStore(s *Store) ports.TokenStore {
// CreateRefreshToken inserts a new refresh token into the database // CreateRefreshToken inserts a new refresh token into the database
func (s *Store) CreateRefreshToken(ctx context.Context, rt domain.RefreshToken) error { func (s *Store) CreateRefreshToken(ctx context.Context, rt domain.RefreshToken) error {
rt.ExpiresAt = time.Now().Add(10 * time.Minute)
return s.queries.CreateRefreshToken(ctx, dbgen.CreateRefreshTokenParams{ return s.queries.CreateRefreshToken(ctx, dbgen.CreateRefreshTokenParams{
UserID: rt.UserID, UserID: rt.UserID,
Token: rt.Token, Token: rt.Token,
ExpiresAt: pgtype.Timestamptz{Time: rt.ExpiresAt}, ExpiresAt: pgtype.Timestamptz{
CreatedAt: pgtype.Timestamptz{Time: rt.CreatedAt}, Time: rt.ExpiresAt,
Valid: true,
},
CreatedAt: pgtype.Timestamptz{
Time: time.Now(),
Valid: true,
},
Revoked: rt.Revoked, Revoked: rt.Revoked,
}) })
} }

View File

@ -16,7 +16,7 @@ import (
// Interface for creating new otp store // Interface for creating new otp store
func NewOTPStore(s *Store) ports.OtpStore { return s } func NewOTPStore(s *Store) ports.OtpStore { return s }
func (s *Store) UpdateExpiredOtp(ctx context.Context, otp, userName string) error { func (s *Store) UpdateOtp(ctx context.Context, otp, userName string) error {
return s.queries.UpdateExpiredOtp(ctx, dbgen.UpdateExpiredOtpParams{ return s.queries.UpdateExpiredOtp(ctx, dbgen.UpdateExpiredOtpParams{
UserName: userName, UserName: userName,
Otp: otp, Otp: otp,
@ -29,6 +29,7 @@ func (s *Store) UpdateExpiredOtp(ctx context.Context, otp, userName string) erro
func (s *Store) CreateOtp(ctx context.Context, otp domain.Otp) error { func (s *Store) CreateOtp(ctx context.Context, otp domain.Otp) error {
return s.queries.CreateOtp(ctx, dbgen.CreateOtpParams{ return s.queries.CreateOtp(ctx, dbgen.CreateOtpParams{
UserName: otp.UserName,
SentTo: otp.SentTo, SentTo: otp.SentTo,
Medium: string(otp.Medium), Medium: string(otp.Medium),
OtpFor: string(otp.For), OtpFor: string(otp.For),
@ -37,12 +38,9 @@ func (s *Store) CreateOtp(ctx context.Context, otp domain.Otp) error {
Time: otp.ExpiresAt, Time: otp.ExpiresAt,
Valid: true, Valid: true,
}, },
CreatedAt: pgtype.Timestamptz{
Time: otp.CreatedAt,
Valid: true,
},
}) })
} }
func (s *Store) GetOtp(ctx context.Context, userName string) (domain.Otp, error) { func (s *Store) GetOtp(ctx context.Context, userName string) (domain.Otp, error) {
row, err := s.queries.GetOtp(ctx, userName) row, err := s.queries.GetOtp(ctx, userName)
if err != nil { if err != nil {
@ -54,6 +52,7 @@ func (s *Store) GetOtp(ctx context.Context, userName string) (domain.Otp, error)
} }
return domain.Otp{ return domain.Otp{
ID: row.ID, ID: row.ID,
UserName: row.UserName,
SentTo: row.SentTo, SentTo: row.SentTo,
Medium: domain.OtpMedium(row.Medium), Medium: domain.OtpMedium(row.Medium),
For: domain.OtpFor(row.OtpFor), For: domain.OtpFor(row.OtpFor),

View File

@ -42,6 +42,13 @@ func (s *Store) IsUserNameUnique(ctx context.Context, userName string) (bool, er
return isUnique, nil return isUnique, nil
} }
func (s *Store) UpdateUserStatus(ctx context.Context, user domain.UpdateUserReq) error {
return s.queries.UpdateUserStatus(ctx, dbgen.UpdateUserStatusParams{
Status: user.Status.Value,
ID: user.UserID,
})
}
func (s *Store) CreateUserWithoutOtp( func (s *Store) CreateUserWithoutOtp(
ctx context.Context, ctx context.Context,
user domain.User, user domain.User,

View File

@ -48,14 +48,14 @@ func (s *Service) ResendOtp(
return fmt.Errorf("invalid otp medium: %s", otp.Medium) return fmt.Errorf("invalid otp medium: %s", otp.Medium)
} }
if err := s.otpStore.UpdateExpiredOtp(ctx, otp.Otp, userName); err != nil { if err := s.otpStore.UpdateOtp(ctx, otpCode, userName); err != nil {
return err return err
} }
return nil return nil
} }
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.SMSProvider) error { func (s *Service) SendOtp(ctx context.Context, userName string, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.SMSProvider) error {
otpCode := helpers.GenerateOTP() otpCode := helpers.GenerateOTP()
message := fmt.Sprintf("Welcome to Yimaru Online Learning Platform, your OTP is %s please don't share with anyone.", otpCode) message := fmt.Sprintf("Welcome to Yimaru Online Learning Platform, your OTP is %s please don't share with anyone.", otpCode)
@ -82,6 +82,7 @@ func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpF
} }
otp := domain.Otp{ otp := domain.Otp{
UserName: userName,
SentTo: sentTo, SentTo: sentTo,
Medium: medium, Medium: medium,
For: otpFor, For: otpFor,

View File

@ -36,6 +36,21 @@ func (s *Service) VerifyOtp(ctx context.Context, userName string, otpCode string
return err return err
} }
user, err := s.userStore.GetUserByUserName(ctx, userName)
if err != nil {
return err
}
newUser := domain.UpdateUserReq{
UserID: user.ID,
Status: domain.ValidString{
Value: string(domain.UserStatusActive),
Valid: true,
},
}
s.userStore.UpdateUserStatus(ctx, newUser)
return nil return nil
} }
@ -59,7 +74,7 @@ func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium,
} }
// send otp based on the medium // send otp based on the medium
return s.SendOtp(ctx, sentTo, domain.OtpRegister, medium, provider) return s.SendOtp(ctx, "", sentTo, domain.OtpRegister, medium, provider)
} }
func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) { func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) {
@ -94,7 +109,7 @@ func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterU
PhoneNumber: registerReq.PhoneNumber, PhoneNumber: registerReq.PhoneNumber,
Password: hashedPassword, Password: hashedPassword,
Role: domain.RoleStudent, Role: domain.RoleStudent,
EmailVerified: false, // verification pending via OTP EmailVerified: false,
PhoneVerified: false, PhoneVerified: false,
EducationLevel: registerReq.EducationLevel, EducationLevel: registerReq.EducationLevel,
Age: registerReq.Age, Age: registerReq.Age,
@ -103,6 +118,16 @@ func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterU
Status: domain.UserStatusPending, Status: domain.UserStatusPending,
ProfileCompleted: false, ProfileCompleted: false,
PreferredLanguage: registerReq.PreferredLanguage, PreferredLanguage: registerReq.PreferredLanguage,
// Optional fields
NickName: registerReq.NickName,
Occupation: registerReq.Occupation,
LearningGoal: registerReq.LearningGoal,
LanguageGoal: registerReq.LanguageGoal,
LanguageChallange: registerReq.LanguageChallange,
FavoutiteTopic: registerReq.FavoutiteTopic,
// ProfilePictureURL: registerReq.ProfilePictureURL,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
@ -115,7 +140,7 @@ func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterU
} }
// Send OTP to the user (email/SMS) // Send OTP to the user (email/SMS)
if err := s.SendOtp(ctx, sentTo, domain.OtpRegister, registerReq.OtpMedium, domain.TwilioSms); err != nil { if err := s.SendOtp(ctx, registerReq.UserName, sentTo, domain.OtpRegister, registerReq.OtpMedium, domain.TwilioSms); err != nil {
return domain.User{}, err return domain.User{}, err
} }

View File

@ -7,7 +7,7 @@ import (
"time" "time"
) )
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error { func (s *Service) SendResetCode(ctx context.Context, userName string, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error {
var err error var err error
// check if user exists // check if user exists
@ -22,7 +22,7 @@ func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, se
return err return err
} }
return s.SendOtp(ctx, sentTo, domain.OtpReset, medium, provider) return s.SendOtp(ctx, userName, sentTo, domain.OtpReset, medium, provider)
} }

View File

@ -31,16 +31,27 @@ func (s *Service) SearchUserByNameOrPhone(ctx context.Context, searchString stri
return s.userStore.SearchUserByNameOrPhone(ctx, searchString, roleStr) return s.userStore.SearchUserByNameOrPhone(ctx, searchString, roleStr)
} }
func (s *Service) UpdateUser(ctx context.Context, req domain.UpdateUserReq) error { func (s *Service) UpdateUser(ctx context.Context, req domain.UpdateUserReq) error {
newUser := domain.User{ newUser := domain.User{
ID: req.UserID, ID: req.UserID,
FirstName: req.FirstName.Value, FirstName: req.FirstName.Value,
LastName: req.LastName.Value, LastName: req.LastName.Value,
KnowledgeLevel: req.KnowledgeLevel.Value,
UserName: req.UserName.Value, UserName: req.UserName.Value,
Age: req.Age.Value, Age: req.Age.Value,
EducationLevel: req.EducationLevel.Value, EducationLevel: req.EducationLevel.Value,
Country: req.Country.Value, Country: req.Country.Value,
Region: req.Region.Value, Region: req.Region.Value,
Status: domain.UserStatus(req.Status.Value),
NickName: req.NickName.Value,
Occupation: req.Occupation.Value,
LearningGoal: req.LearningGoal.Value,
LanguageGoal: req.LanguageGoal.Value,
LanguageChallange: req.LanguageChallange.Value,
FavoutiteTopic: req.FavoutiteTopic.Value,
PreferredLanguage: req.PreferredLanguage.Value,
ProfilePictureURL: req.ProfilePictureURL.Value,
} }
// Update user in the store // Update user in the store

View File

@ -39,11 +39,6 @@ type loginUserRes struct {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/user-login [post] // @Router /api/v1/{tenant_slug}/user-login [post]
func (h *Handler) LoginUser(c *fiber.Ctx) error { func (h *Handler) LoginUser(c *fiber.Ctx) error {
// OrganizationID := c.Locals("company_id").(domain.ValidInt64)
// if !OrganizationID.Valid {
// h.BadRequestLogger().Error("invalid company id")
// return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
// }
var req loginUserReq var req loginUserReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse LoginUser request", h.mongoLoggerSvc.Info("Failed to parse LoginUser request",
@ -51,7 +46,10 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body"+err.Error()) return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to login",
Error: "Invalid request body: " + err.Error(),
})
} }
if valErrs, ok := h.validator.Validate(c, req); !ok { if valErrs, ok := h.validator.Validate(c, req); !ok {
@ -59,11 +57,13 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
for field, msg := range valErrs { for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg) errMsg += fmt.Sprintf("%s: %s; ", field, msg)
} }
return fiber.NewError(fiber.StatusBadRequest, errMsg) return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to login",
Error: errMsg,
})
} }
successRes, err := h.authSvc.Login(c.Context(), req.UserName, req.Password) successRes, err := h.authSvc.Login(c.Context(), req.UserName, req.Password)
if err != nil { if err != nil {
switch { switch {
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
@ -73,7 +73,10 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials") return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Message: "Failed to login",
Error: fmt.Sprintf("Invalid credentials: %v", err),
})
case errors.Is(err, authentication.ErrUserSuspended): case errors.Is(err, authentication.ErrUserSuspended):
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked", h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
zap.Int("status_code", fiber.StatusUnauthorized), zap.Int("status_code", fiber.StatusUnauthorized),
@ -81,14 +84,20 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusUnauthorized, "User login has been locked") return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Message: "Failed to login",
Error: fmt.Sprintf("User login has been locked: %v", err),
})
default: default:
h.mongoLoggerSvc.Error("Login failed", h.mongoLoggerSvc.Error("Login failed",
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error") return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to login",
Error: err.Error(),
})
} }
} }
@ -97,10 +106,12 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
zap.Int("status_code", fiber.StatusForbidden), zap.Int("status_code", fiber.StatusForbidden),
zap.String("role", string(successRes.Role)), zap.String("role", string(successRes.Role)),
zap.String("user_name", req.UserName), zap.String("user_name", req.UserName),
zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusForbidden, "Only users are allowed to login ") return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: "Failed to login",
Error: "Only users are allowed to login",
})
} }
accessToken, err := jwtutil.CreateJwt( accessToken, err := jwtutil.CreateJwt(
@ -116,7 +127,10 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to login",
Error: "Failed to generate access token",
})
} }
res := loginUserRes{ res := loginUserRes{
@ -132,7 +146,10 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil) return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Login successful",
Data: res,
})
} }
// loginAdminReq represents the request body for the LoginAdmin endpoint. // loginAdminReq represents the request body for the LoginAdmin endpoint.

View File

@ -13,6 +13,79 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
// UpdateUser godoc
// @Summary Update user profile
// @Description Updates user profile information (partial updates supported)
// @Tags user
// @Accept json
// @Produce json
// @Param user_id path int true "User ID"
// @Param body body domain.UpdateUserReq true "Update user payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/{tenant_slug}/user [put]
func (h *Handler) UpdateUser(c *fiber.Ctx) error {
// Extract user ID from context
userIDStr, ok := c.Locals("user_id").(string)
if !ok {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid user context",
Error: "User ID not found in request context",
})
}
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil || userID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid user ID",
Error: "User ID must be a positive integer",
})
}
// Parse request body
var req domain.UpdateUserReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
// Enforce user identity
req.UserID = userID
// Optional: lightweight validation (example)
// if req.Status.IsSet() {
// if !domain.(req.Status.Value) {
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
// Message: "Invalid status value",
// Error: "Unsupported user status",
// })
// }
// }
// Call service
if err := h.userSvc.UpdateUser(c.Context(), req); err != nil {
if errors.Is(err, authentication.ErrUserNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "User not found",
Error: err.Error(),
})
}
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update user",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "User updated successfully",
})
}
// UpdateUserKnowledgeLevel godoc // UpdateUserKnowledgeLevel godoc
// @Summary Update user's knowledge level // @Summary Update user's knowledge level
// @Description Updates the knowledge level of the specified user after initial assessment // @Description Updates the knowledge level of the specified user after initial assessment
@ -158,6 +231,8 @@ func (h *Handler) ResendOtp(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(domain.Response{ return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "OTP resent successfully", Message: "OTP resent successfully",
Success: true,
StatusCode: fiber.StatusOK,
Data: nil, Data: nil,
}) })
} }
@ -560,6 +635,13 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
Country: req.Country, Country: req.Country,
Region: req.Region, Region: req.Region,
PreferredLanguage: req.PreferredLanguage, PreferredLanguage: req.PreferredLanguage,
NickName: req.NickName,
Occupation: req.Occupation,
LearningGoal: req.LearningGoal,
LanguageGoal: req.LanguageGoal,
LanguageChallange: req.LanguageChallange,
FavoutiteTopic: req.FavoutiteTopic,
} }
medium, err := getMedium(req.Email, req.PhoneNumber) medium, err := getMedium(req.Email, req.PhoneNumber)
@ -600,6 +682,30 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
}) })
} }
func MapRegisterReqToUser(req domain.RegisterUserReq) domain.User {
return domain.User{
FirstName: req.FirstName,
LastName: req.LastName,
UserName: req.UserName,
Email: req.Email,
PhoneNumber: req.PhoneNumber,
Password: []byte(req.Password), // or hashed password
Role: domain.Role(req.Role),
Age: req.Age,
EducationLevel: req.EducationLevel,
Country: req.Country,
Region: req.Region,
PreferredLanguage: req.PreferredLanguage,
NickName: req.NickName,
Occupation: req.Occupation,
LearningGoal: req.LearningGoal,
LanguageGoal: req.LanguageGoal,
LanguageChallange: req.LanguageChallange,
FavoutiteTopic: req.FavoutiteTopic,
}
}
type ResetCodeReq struct { type ResetCodeReq struct {
Email string `json:"email" example:"john.doe@example.com"` Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
@ -654,7 +760,7 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
} }
if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, domain.AfroMessage); err != nil { if err := h.userSvc.SendResetCode(c.Context(), "", medium, sentTo, domain.AfroMessage); err != nil {
h.mongoLoggerSvc.Error("Failed to send reset code", h.mongoLoggerSvc.Error("Failed to send reset code",
zap.String("medium", string(medium)), zap.String("medium", string(medium)),
zap.String("sentTo", string(sentTo)), zap.String("sentTo", string(sentTo)),
@ -721,7 +827,7 @@ func (h *Handler) SendTenantResetCode(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
} }
if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, domain.AfroMessage); err != nil { if err := h.userSvc.SendResetCode(c.Context(), "", medium, sentTo, domain.AfroMessage); err != nil {
h.mongoLoggerSvc.Error("Failed to send reset code", h.mongoLoggerSvc.Error("Failed to send reset code",
zap.String("medium", string(medium)), zap.String("medium", string(medium)),
zap.String("sentTo", string(sentTo)), zap.String("sentTo", string(sentTo)),

View File

@ -70,24 +70,24 @@ func (a *App) authMiddleware(c *fiber.Ctx) error {
} }
// Asserting to make sure that only the super admin can have a nil company ID // Asserting to make sure that only the super admin can have a nil company ID
if claim.Role != domain.RoleSuperAdmin && !claim.CompanyID.Valid { // if claim.Role != domain.RoleSuperAdmin && !claim.CompanyID.Valid {
a.mongoLoggerSvc.Error("Company Role without Company ID", // a.mongoLoggerSvc.Error("Company Role without Company ID",
zap.Int64("userID", claim.UserId), // zap.Int64("userID", claim.UserId),
zap.Int("status_code", fiber.StatusInternalServerError), // zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err), // zap.Error(err),
zap.Time("timestamp", time.Now()), // zap.Time("timestamp", time.Now()),
) // )
return fiber.NewError(fiber.StatusInternalServerError, "Company Role without Company ID") // return fiber.NewError(fiber.StatusInternalServerError, "Company Role without Company ID")
} // }
c.Locals("user_id", claim.UserId) c.Locals("user_id", claim.UserId)
c.Locals("role", claim.Role) c.Locals("role", claim.Role)
c.Locals("company_id", domain.ValidInt64{ // c.Locals("company_id", domain.ValidInt64{
Value: claim.CompanyID.Value, // Value: claim.CompanyID.Value,
Valid: claim.CompanyID.Valid, // Valid: claim.CompanyID.Valid,
}) // })
c.Locals("refresh_token", refreshToken) c.Locals("refresh_token", refreshToken)
var branchID domain.ValidInt64 // var branchID domain.ValidInt64
if claim.Role == domain.RoleAdmin { if claim.Role == domain.RoleAdmin {
// branch, err := a.branchSvc.GetBranchByCashier(c.Context(), claim.UserId) // branch, err := a.branchSvc.GetBranchByCashier(c.Context(), claim.UserId)
@ -107,7 +107,7 @@ func (a *App) authMiddleware(c *fiber.Ctx) error {
} }
c.Locals("branch_id", branchID) // c.Locals("branch_id", branchID)
return c.Next() return c.Next()
} }
@ -126,20 +126,20 @@ func (a *App) SuperAdminOnly(c *fiber.Ctx) error {
return c.Next() return c.Next()
} }
func (a *App) CompanyOnly(c *fiber.Ctx) error { // func (a *App) CompanyOnly(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64) // userID := c.Locals("user_id").(int64)
userRole := c.Locals("role").(domain.Role) // userRole := c.Locals("role").(domain.Role)
if userRole == domain.RoleStudent { // if userRole == domain.RoleStudent {
a.mongoLoggerSvc.Warn("Attempt to access restricted CompanyOnly route", // a.mongoLoggerSvc.Warn("Attempt to access restricted CompanyOnly route",
zap.Int64("userID", userID), // zap.Int64("userID", userID),
zap.String("role", string(userRole)), // zap.String("role", string(userRole)),
zap.Int("status_code", fiber.StatusForbidden), // zap.Int("status_code", fiber.StatusForbidden),
zap.Time("timestamp", time.Now()), // zap.Time("timestamp", time.Now()),
) // )
return fiber.NewError(fiber.StatusForbidden, "This route is restricted") // return fiber.NewError(fiber.StatusForbidden, "This route is restricted")
} // }
return c.Next() // return c.Next()
} // }
func (a *App) OnlyAdminAndAbove(c *fiber.Ctx) error { func (a *App) OnlyAdminAndAbove(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64) userID := c.Locals("user_id").(int64)

View File

@ -42,31 +42,31 @@ func (a *App) initAppRoutes() {
// Groups // Groups
groupV1 := a.fiber.Group("/api/v1") groupV1 := a.fiber.Group("/api/v1")
tenant := groupV1.Group("/tenant/:tenant_slug", a.TenantMiddleware) // tenant := groupV1.Group("/tenant/:tenant_slug", a.TenantMiddleware)
tenant.Get("/test", a.authMiddleware, a.authMiddleware, func(c *fiber.Ctx) error { // groupV1.Get("/test", a.authMiddleware, a.authMiddleware, func(c *fiber.Ctx) error {
fmt.Printf("\nTest Route %v\n", c.Route().Path) // fmt.Printf("\nTest Route %v\n", c.Route().Path)
companyID := c.Locals("company_id").(domain.ValidInt64) // companyID := c.Locals("company_id").(domain.ValidInt64)
if !companyID.Valid { // if !companyID.Valid {
h.BadRequestLogger().Error("invalid company id") // h.BadRequestLogger().Error("invalid company id")
return fiber.NewError(fiber.StatusBadRequest, "invalid company id") // return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
} // }
fmt.Printf("In the tenant auth test \n") // fmt.Printf("In the tenant auth test \n")
return c.JSON(fiber.Map{ // return c.JSON(fiber.Map{
"message": "Is is fine", // "message": "Is is fine",
}) // })
}) // })
tenant.Get("/", func(c *fiber.Ctx) error { // groupV1.Get("/", func(c *fiber.Ctx) error {
fmt.Printf("\nTenant Route %v\n", c.Route().Path) // fmt.Printf("\nTenant Route %v\n", c.Route().Path)
companyID := c.Locals("company_id").(domain.ValidInt64) // companyID := c.Locals("company_id").(domain.ValidInt64)
if !companyID.Valid { // if !companyID.Valid {
h.BadRequestLogger().Error("invalid company id") // h.BadRequestLogger().Error("invalid company id")
return fiber.NewError(fiber.StatusBadRequest, "invalid company id") // return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
} // }
return c.JSON(fiber.Map{ // return c.JSON(fiber.Map{
"message": "Company Tenant Active", // "message": "Company Tenant Active",
}) // })
}) // })
// Get S // Get S
groupV1.Get("/tenant", a.authMiddleware, h.GetTenantSlugByToken) groupV1.Get("/tenant", a.authMiddleware, h.GetTenantSlugByToken)
@ -84,7 +84,7 @@ func (a *App) initAppRoutes() {
//assessment Routes //assessment Routes
groupV1.Post("/assessment/questions", h.CreateAssessmentQuestion) groupV1.Post("/assessment/questions", h.CreateAssessmentQuestion)
groupV1.Get("/assessment/questions", h.GetActiveAssessmentQuestions) groupV1.Get("/assessment/questions", h.GetActiveAssessmentQuestions)
tenant.Post("/assessment/submit", a.authMiddleware, h.SubmitAssessment) groupV1.Post("/assessment/submit", a.authMiddleware, h.SubmitAssessment)
// Course Management Routes // Course Management Routes
groupV1.Post("/course-categories", h.CreateCourseCategory) groupV1.Post("/course-categories", h.CreateCourseCategory)
@ -114,8 +114,8 @@ func (a *App) initAppRoutes() {
groupV1.Post("/levels", h.CreateLevel) groupV1.Post("/levels", h.CreateLevel)
// Auth Routes // Auth Routes
tenant.Post("/auth/customer-login", h.LoginUser) groupV1.Post("/auth/customer-login", h.LoginUser)
tenant.Post("/auth/admin-login", h.LoginAdmin) groupV1.Post("/auth/admin-login", h.LoginAdmin)
groupV1.Post("/auth/super-login", h.LoginSuper) groupV1.Post("/auth/super-login", h.LoginSuper)
groupV1.Post("/auth/refresh", h.RefreshToken) groupV1.Post("/auth/refresh", h.RefreshToken)
groupV1.Post("/auth/logout", a.authMiddleware, h.LogOutuser) groupV1.Post("/auth/logout", a.authMiddleware, h.LogOutuser)
@ -156,7 +156,8 @@ func (a *App) initAppRoutes() {
// groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler // groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler
// User Routes // User Routes
tenant.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel) groupV1.Put("/user", a.authMiddleware, h.UpdateUser)
groupV1.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel)
groupV1.Get("/user/:user_name/is-unique", h.CheckUserNameUnique) groupV1.Get("/user/:user_name/is-unique", h.CheckUserNameUnique)
groupV1.Get("/user/:user_name/is-pending", h.CheckUserPending) groupV1.Get("/user/:user_name/is-pending", h.CheckUserPending)
groupV1.Post("/user/resetPassword", h.ResetPassword) groupV1.Post("/user/resetPassword", h.ResetPassword)
@ -164,15 +165,15 @@ func (a *App) initAppRoutes() {
groupV1.Post("/user/verify-otp", h.VerifyOtp) groupV1.Post("/user/verify-otp", h.VerifyOtp)
groupV1.Post("/user/resend-otp", h.ResendOtp) groupV1.Post("/user/resend-otp", h.ResendOtp)
tenant.Post("/user/resetPassword", h.ResetTenantPassword) groupV1.Post("/user/resetPassword", h.ResetTenantPassword)
tenant.Post("/user/sendResetCode", h.SendTenantResetCode) groupV1.Post("/user/sendResetCode", h.SendTenantResetCode)
tenant.Post("/user/register", h.RegisterUser) groupV1.Post("/user/register", h.RegisterUser)
tenant.Post("/user/sendRegisterCode", h.SendRegisterCode) groupV1.Post("/user/sendRegisterCode", h.SendRegisterCode)
tenant.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist) groupV1.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist)
groupV1.Get("/user/admin-profile", a.authMiddleware, h.AdminProfile) groupV1.Get("/user/admin-profile", a.authMiddleware, h.AdminProfile)
tenant.Get("/user/user-profile", a.authMiddleware, h.GetUserProfile) groupV1.Get("/user/user-profile", a.authMiddleware, h.GetUserProfile)
groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID) groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID)
groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser) groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser)