fix registration and password reset

This commit is contained in:
lafetz 2025-03-31 00:25:50 +03:00
parent d1a33b18dc
commit ca7aa9d67c
30 changed files with 2409 additions and 332 deletions

View File

@ -7,8 +7,11 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email"
mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
@ -44,10 +47,14 @@ func main() {
store := repository.NewStore(db)
v := customvalidator.NewCustomValidator(validator.New())
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
mockSms := mocksms.NewMockSMS()
mockemail := mockemail.NewMockEmail()
userSvc := user.NewService(store, store, mockSms, mockemail)
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry,
})
}, userSvc,
)
logger.Info("Starting server", "port", cfg.Port)
if err := app.Run(); err != nil {
logger.Error("Failed to start server", "error", err)

View File

@ -4,18 +4,16 @@ CREATE TABLE users (
last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE ,
phone_number VARCHAR(20) UNIQUE,
password BYTEA NOT NULL,
role VARCHAR(50) NOT NULL,
password BYTEA NOT NULL,
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ ,
--
suspended_at TIMESTAMPTZ NULL, -- this can be NULL if the user is not suspended
suspended BOOLEAN NOT NULL DEFAULT FALSE,
CHECK (
(email IS NOT NULL AND phone_number IS NULL) OR
(email IS NULL AND phone_number IS NOT NULL)
)
CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
);
CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY,
@ -26,19 +24,38 @@ CREATE TABLE refresh_tokens (
revoked BOOLEAN DEFAULT FALSE NOT NULL,
CONSTRAINT unique_token UNIQUE (token)
);
-----
CREATE TABLE otps (
id BIGSERIAL PRIMARY KEY,
sent_to VARCHAR(255) NOT NULL,
medium VARCHAR(50) NOT NULL,
otp_for VARCHAR(50) NOT NULL,
otp VARCHAR(10) NOT NULL,
used BOOLEAN NOT NULL DEFAULT FALSE,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ NOT NULL
);
----------------------------------------------seed data-------------------------------------------------------------
-------------------------------------- DO NOT USE IN PRODUCTION-------------------------------------------------
CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO users (first_name, last_name, email, phone_number, password, role, created_at, updated_at)
VALUES (
INSERT INTO users (
first_name, last_name, email, phone_number, password, role,
email_verified, phone_verified, created_at, updated_at,
suspended_at, suspended
) VALUES (
'John',
'Doe',
'john.doe@example.com',
'1234567890',
NULL,
crypt('password123', gen_salt('bf'))::bytea,
'user',
'customer',
TRUE,
FALSE,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
CURRENT_TIMESTAMP,
NULL,
FALSE
);

14
db/query/otp.sql Normal file
View File

@ -0,0 +1,14 @@
-- name: CreateOtp :exec
INSERT INTO otps (sent_to, medium, otp_for, otp, used, created_at, expires_at)
VALUES ($1, $2, $3, $4, FALSE, CURRENT_TIMESTAMP, $5);
-- name: GetOtp :one
SELECT id, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
FROM otps
WHERE sent_to = $1 AND otp_for = $2 AND medium = $3
ORDER BY created_at DESC LIMIT 1;
-- name: MarkOtpAsUsed :exec
UPDATE otps
SET used = TRUE, used_at = CURRENT_TIMESTAMP
WHERE id = $1;

View File

@ -1,16 +1,42 @@
-- name: CreateUser :one
INSERT INTO users (first_name, last_name, email, phone_number, password, role, verified)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *;
INSERT INTO users (first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at;
-- name: GetUserByID :one
SELECT * FROM users WHERE id = $1;
SELECT *
FROM users
WHERE id = $1;
-- name: GetAllUsers :many
SELECT * FROM users;
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
FROM users;
-- name: UpdateUser :exec
UPDATE users SET first_name = $2, last_name = $3, email = $4, phone_number = $5, password = $6, role = $7, verified = $8, updated_at = CURRENT_TIMESTAMP WHERE id = $1;
UPDATE users
SET first_name = $1, last_name = $2, email = $3, phone_number = $4, role = $5, updated_at = CURRENT_TIMESTAMP
WHERE id = $6;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
DELETE FROM users
WHERE id = $1;
-- name: CheckPhoneEmailExist :one
SELECT
EXISTS (SELECT 1 FROM users WHERE users.phone_number = $1 AND users.phone_number IS NOT NULL) AS phone_exists,
EXISTS (SELECT 1 FROM users WHERE users.email = $2 AND users.email IS NOT NULL) AS email_exists;
-- name: GetUserByEmail :one
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
FROM users
WHERE email = $1;
-- name: GetUserByPhone :one
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
FROM users
WHERE phone_number = $1;
-- name: UpdatePassword :exec
UPDATE users
SET password = $1, updated_at = CURRENT_TIMESTAMP
WHERE (email = $2 OR phone_number = $3);

View File

@ -44,7 +44,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/httpserver.loginCustomerReq"
"$ref": "#/definitions/handlers.loginCustomerReq"
}
}
],
@ -52,7 +52,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/httpserver.loginCustomerRes"
"$ref": "#/definitions/handlers.loginCustomerRes"
}
},
"400": {
@ -96,7 +96,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/httpserver.logoutReq"
"$ref": "#/definitions/handlers.logoutReq"
}
}
],
@ -148,7 +148,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/httpserver.refreshToken"
"$ref": "#/definitions/handlers.refreshToken"
}
}
],
@ -156,7 +156,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/httpserver.loginCustomerRes"
"$ref": "#/definitions/handlers.loginCustomerRes"
}
},
"400": {
@ -179,10 +179,439 @@ const docTemplate = `{
}
}
}
},
"/user/checkPhoneEmailExist": {
"post": {
"description": "Check if phone number or email exist",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Check if phone number or email exist",
"parameters": [
{
"description": "Check phone number or email exist",
"name": "checkPhoneEmailExist",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.CheckPhoneEmailExistReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.CheckPhoneEmailExistRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/user/profile": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Get user profile",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Get user profile",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UserProfileRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/user/register": {
"post": {
"description": "Register user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Register user",
"parameters": [
{
"description": "Register user",
"name": "registerUser",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.RegisterUserReq"
}
}
],
"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"
}
}
}
}
},
"/user/resetPassword": {
"post": {
"description": "Reset password",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Reset password",
"parameters": [
{
"description": "Reset password",
"name": "resetPassword",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.ResetPasswordReq"
}
}
],
"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"
}
}
}
}
},
"/user/sendRegisterCode": {
"post": {
"description": "Send register code",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Send register code",
"parameters": [
{
"description": "Send register code",
"name": "registerCode",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.RegisterCodeReq"
}
}
],
"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"
}
}
}
}
},
"/user/sendResetCode": {
"post": {
"description": "Send reset code",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Send reset code",
"parameters": [
{
"description": "Send reset code",
"name": "resetCode",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.ResetCodeReq"
}
}
],
"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": {
"httpserver.loginCustomerReq": {
"domain.Role": {
"type": "string",
"enum": [
"admin",
"customer",
"super_admin",
"branch_manager",
"cashier"
],
"x-enum-varnames": [
"RoleAdmin",
"RoleCustomer",
"RoleSuperAdmin",
"RoleBranchManager",
"RoleCashier"
]
},
"handlers.CheckPhoneEmailExistReq": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"phone_number": {
"type": "string",
"example": "1234567890"
}
}
},
"handlers.CheckPhoneEmailExistRes": {
"type": "object",
"properties": {
"email_exist": {
"type": "boolean"
},
"phone_number_exist": {
"type": "boolean"
}
}
},
"handlers.RegisterCodeReq": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"phone_number": {
"type": "string",
"example": "1234567890"
}
}
},
"handlers.RegisterUserReq": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"otp": {
"description": "Role string",
"type": "string",
"example": "123456"
},
"password": {
"type": "string",
"example": "password123"
},
"phone_number": {
"type": "string",
"example": "1234567890"
},
"referal_code": {
"type": "string",
"example": "ABC123"
}
}
},
"handlers.ResetCodeReq": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"phone_number": {
"type": "string",
"example": "1234567890"
}
}
},
"handlers.ResetPasswordReq": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"otp": {
"type": "string"
},
"password": {
"type": "string"
},
"phoneNumber": {
"type": "string"
}
}
},
"handlers.UserProfileRes": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"email": {
"type": "string"
},
"email_verified": {
"type": "boolean"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"last_name": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"phone_verified": {
"type": "boolean"
},
"role": {
"$ref": "#/definitions/domain.Role"
},
"suspended": {
"type": "boolean"
},
"suspended_at": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"handlers.loginCustomerReq": {
"type": "object",
"properties": {
"email": {
@ -199,7 +628,7 @@ const docTemplate = `{
}
}
},
"httpserver.loginCustomerRes": {
"handlers.loginCustomerRes": {
"type": "object",
"properties": {
"access_token": {
@ -210,7 +639,7 @@ const docTemplate = `{
}
}
},
"httpserver.logoutReq": {
"handlers.logoutReq": {
"type": "object",
"properties": {
"refresh_token": {
@ -218,7 +647,7 @@ const docTemplate = `{
}
}
},
"httpserver.refreshToken": {
"handlers.refreshToken": {
"type": "object",
"properties": {
"access_token": {

View File

@ -36,7 +36,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/httpserver.loginCustomerReq"
"$ref": "#/definitions/handlers.loginCustomerReq"
}
}
],
@ -44,7 +44,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/httpserver.loginCustomerRes"
"$ref": "#/definitions/handlers.loginCustomerRes"
}
},
"400": {
@ -88,7 +88,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/httpserver.logoutReq"
"$ref": "#/definitions/handlers.logoutReq"
}
}
],
@ -140,7 +140,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/httpserver.refreshToken"
"$ref": "#/definitions/handlers.refreshToken"
}
}
],
@ -148,7 +148,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/httpserver.loginCustomerRes"
"$ref": "#/definitions/handlers.loginCustomerRes"
}
},
"400": {
@ -171,10 +171,439 @@
}
}
}
},
"/user/checkPhoneEmailExist": {
"post": {
"description": "Check if phone number or email exist",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Check if phone number or email exist",
"parameters": [
{
"description": "Check phone number or email exist",
"name": "checkPhoneEmailExist",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.CheckPhoneEmailExistReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.CheckPhoneEmailExistRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/user/profile": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Get user profile",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Get user profile",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UserProfileRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/user/register": {
"post": {
"description": "Register user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Register user",
"parameters": [
{
"description": "Register user",
"name": "registerUser",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.RegisterUserReq"
}
}
],
"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"
}
}
}
}
},
"/user/resetPassword": {
"post": {
"description": "Reset password",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Reset password",
"parameters": [
{
"description": "Reset password",
"name": "resetPassword",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.ResetPasswordReq"
}
}
],
"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"
}
}
}
}
},
"/user/sendRegisterCode": {
"post": {
"description": "Send register code",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Send register code",
"parameters": [
{
"description": "Send register code",
"name": "registerCode",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.RegisterCodeReq"
}
}
],
"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"
}
}
}
}
},
"/user/sendResetCode": {
"post": {
"description": "Send reset code",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Send reset code",
"parameters": [
{
"description": "Send reset code",
"name": "resetCode",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.ResetCodeReq"
}
}
],
"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": {
"httpserver.loginCustomerReq": {
"domain.Role": {
"type": "string",
"enum": [
"admin",
"customer",
"super_admin",
"branch_manager",
"cashier"
],
"x-enum-varnames": [
"RoleAdmin",
"RoleCustomer",
"RoleSuperAdmin",
"RoleBranchManager",
"RoleCashier"
]
},
"handlers.CheckPhoneEmailExistReq": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"phone_number": {
"type": "string",
"example": "1234567890"
}
}
},
"handlers.CheckPhoneEmailExistRes": {
"type": "object",
"properties": {
"email_exist": {
"type": "boolean"
},
"phone_number_exist": {
"type": "boolean"
}
}
},
"handlers.RegisterCodeReq": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"phone_number": {
"type": "string",
"example": "1234567890"
}
}
},
"handlers.RegisterUserReq": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"otp": {
"description": "Role string",
"type": "string",
"example": "123456"
},
"password": {
"type": "string",
"example": "password123"
},
"phone_number": {
"type": "string",
"example": "1234567890"
},
"referal_code": {
"type": "string",
"example": "ABC123"
}
}
},
"handlers.ResetCodeReq": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"phone_number": {
"type": "string",
"example": "1234567890"
}
}
},
"handlers.ResetPasswordReq": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"otp": {
"type": "string"
},
"password": {
"type": "string"
},
"phoneNumber": {
"type": "string"
}
}
},
"handlers.UserProfileRes": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"email": {
"type": "string"
},
"email_verified": {
"type": "boolean"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"last_name": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"phone_verified": {
"type": "boolean"
},
"role": {
"$ref": "#/definitions/domain.Role"
},
"suspended": {
"type": "boolean"
},
"suspended_at": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"handlers.loginCustomerReq": {
"type": "object",
"properties": {
"email": {
@ -191,7 +620,7 @@
}
}
},
"httpserver.loginCustomerRes": {
"handlers.loginCustomerRes": {
"type": "object",
"properties": {
"access_token": {
@ -202,7 +631,7 @@
}
}
},
"httpserver.logoutReq": {
"handlers.logoutReq": {
"type": "object",
"properties": {
"refresh_token": {
@ -210,7 +639,7 @@
}
}
},
"httpserver.refreshToken": {
"handlers.refreshToken": {
"type": "object",
"properties": {
"access_token": {

View File

@ -1,5 +1,116 @@
definitions:
httpserver.loginCustomerReq:
domain.Role:
enum:
- admin
- customer
- super_admin
- branch_manager
- cashier
type: string
x-enum-varnames:
- RoleAdmin
- RoleCustomer
- RoleSuperAdmin
- RoleBranchManager
- RoleCashier
handlers.CheckPhoneEmailExistReq:
properties:
email:
example: john.doe@example.com
type: string
phone_number:
example: "1234567890"
type: string
type: object
handlers.CheckPhoneEmailExistRes:
properties:
email_exist:
type: boolean
phone_number_exist:
type: boolean
type: object
handlers.RegisterCodeReq:
properties:
email:
example: john.doe@example.com
type: string
phone_number:
example: "1234567890"
type: string
type: object
handlers.RegisterUserReq:
properties:
email:
example: john.doe@example.com
type: string
first_name:
example: John
type: string
last_name:
example: Doe
type: string
otp:
description: Role string
example: "123456"
type: string
password:
example: password123
type: string
phone_number:
example: "1234567890"
type: string
referal_code:
example: ABC123
type: string
type: object
handlers.ResetCodeReq:
properties:
email:
example: john.doe@example.com
type: string
phone_number:
example: "1234567890"
type: string
type: object
handlers.ResetPasswordReq:
properties:
email:
type: string
otp:
type: string
password:
type: string
phoneNumber:
type: string
type: object
handlers.UserProfileRes:
properties:
created_at:
type: string
email:
type: string
email_verified:
type: boolean
first_name:
type: string
id:
type: integer
last_name:
type: string
phone_number:
type: string
phone_verified:
type: boolean
role:
$ref: '#/definitions/domain.Role'
suspended:
type: boolean
suspended_at:
type: string
updated_at:
type: string
type: object
handlers.loginCustomerReq:
properties:
email:
example: john.doe@example.com
@ -11,19 +122,19 @@ definitions:
example: "1234567890"
type: string
type: object
httpserver.loginCustomerRes:
handlers.loginCustomerRes:
properties:
access_token:
type: string
refresh_token:
type: string
type: object
httpserver.logoutReq:
handlers.logoutReq:
properties:
refresh_token:
type: string
type: object
httpserver.refreshToken:
handlers.refreshToken:
properties:
access_token:
type: string
@ -73,14 +184,14 @@ paths:
name: login
required: true
schema:
$ref: '#/definitions/httpserver.loginCustomerReq'
$ref: '#/definitions/handlers.loginCustomerReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/httpserver.loginCustomerRes'
$ref: '#/definitions/handlers.loginCustomerRes'
"400":
description: Bad Request
schema:
@ -107,7 +218,7 @@ paths:
name: logout
required: true
schema:
$ref: '#/definitions/httpserver.logoutReq'
$ref: '#/definitions/handlers.logoutReq'
produces:
- application/json
responses:
@ -141,14 +252,14 @@ paths:
name: refresh
required: true
schema:
$ref: '#/definitions/httpserver.refreshToken'
$ref: '#/definitions/handlers.refreshToken'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/httpserver.loginCustomerRes'
$ref: '#/definitions/handlers.loginCustomerRes'
"400":
description: Bad Request
schema:
@ -164,6 +275,181 @@ paths:
summary: Refresh token
tags:
- auth
/user/checkPhoneEmailExist:
post:
consumes:
- application/json
description: Check if phone number or email exist
parameters:
- description: Check phone number or email exist
in: body
name: checkPhoneEmailExist
required: true
schema:
$ref: '#/definitions/handlers.CheckPhoneEmailExistReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.CheckPhoneEmailExistRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Check if phone number or email exist
tags:
- user
/user/profile:
get:
consumes:
- application/json
description: Get user profile
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.UserProfileRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- Bearer: []
summary: Get user profile
tags:
- user
/user/register:
post:
consumes:
- application/json
description: Register user
parameters:
- description: Register user
in: body
name: registerUser
required: true
schema:
$ref: '#/definitions/handlers.RegisterUserReq'
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: Register user
tags:
- user
/user/resetPassword:
post:
consumes:
- application/json
description: Reset password
parameters:
- description: Reset password
in: body
name: resetPassword
required: true
schema:
$ref: '#/definitions/handlers.ResetPasswordReq'
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: Reset password
tags:
- user
/user/sendRegisterCode:
post:
consumes:
- application/json
description: Send register code
parameters:
- description: Send register code
in: body
name: registerCode
required: true
schema:
$ref: '#/definitions/handlers.RegisterCodeReq'
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: Send register code
tags:
- user
/user/sendResetCode:
post:
consumes:
- application/json
description: Send reset code
parameters:
- description: Send reset code
in: body
name: resetCode
required: true
schema:
$ref: '#/definitions/handlers.ResetCodeReq'
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: Send reset code
tags:
- user
securityDefinitions:
Bearer:
in: header

View File

@ -55,13 +55,13 @@ func (q *Queries) GetRefreshToken(ctx context.Context, token string) (RefreshTok
}
const GetUserByEmailPhone = `-- name: GetUserByEmailPhone :one
SELECT id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at FROM users
SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, suspended_at, suspended FROM users
WHERE email = $1 OR phone_number = $2
`
type GetUserByEmailPhoneParams struct {
Email string
PhoneNumber string
Email pgtype.Text
PhoneNumber pgtype.Text
}
func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPhoneParams) (User, error) {
@ -73,11 +73,14 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Password,
&i.Role,
&i.Verified,
&i.Password,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.SuspendedAt,
&i.Suspended,
)
return i, err
}

View File

@ -8,6 +8,18 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
type Otp struct {
ID int64
SentTo string
Medium string
OtpFor string
Otp string
Used bool
UsedAt pgtype.Timestamptz
CreatedAt pgtype.Timestamptz
ExpiresAt pgtype.Timestamptz
}
type RefreshToken struct {
ID int64
UserID int64
@ -18,14 +30,17 @@ type RefreshToken struct {
}
type User struct {
ID int64
FirstName string
LastName string
Email string
PhoneNumber string
Password []byte
Role string
Verified pgtype.Bool
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
ID int64
FirstName string
LastName string
Email pgtype.Text
PhoneNumber pgtype.Text
Role string
Password []byte
EmailVerified bool
PhoneVerified bool
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
SuspendedAt pgtype.Timestamptz
Suspended bool
}

77
gen/db/otp.sql.go Normal file
View File

@ -0,0 +1,77 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: otp.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CreateOtp = `-- name: CreateOtp :exec
INSERT INTO otps (sent_to, medium, otp_for, otp, used, created_at, expires_at)
VALUES ($1, $2, $3, $4, FALSE, CURRENT_TIMESTAMP, $5)
`
type CreateOtpParams struct {
SentTo string
Medium string
OtpFor string
Otp string
ExpiresAt pgtype.Timestamptz
}
func (q *Queries) CreateOtp(ctx context.Context, arg CreateOtpParams) error {
_, err := q.db.Exec(ctx, CreateOtp,
arg.SentTo,
arg.Medium,
arg.OtpFor,
arg.Otp,
arg.ExpiresAt,
)
return err
}
const GetOtp = `-- name: GetOtp :one
SELECT id, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
FROM otps
WHERE sent_to = $1 AND otp_for = $2 AND medium = $3
ORDER BY created_at DESC LIMIT 1
`
type GetOtpParams struct {
SentTo string
OtpFor string
Medium string
}
func (q *Queries) GetOtp(ctx context.Context, arg GetOtpParams) (Otp, error) {
row := q.db.QueryRow(ctx, GetOtp, arg.SentTo, arg.OtpFor, arg.Medium)
var i Otp
err := row.Scan(
&i.ID,
&i.SentTo,
&i.Medium,
&i.OtpFor,
&i.Otp,
&i.Used,
&i.UsedAt,
&i.CreatedAt,
&i.ExpiresAt,
)
return i, err
}
const MarkOtpAsUsed = `-- name: MarkOtpAsUsed :exec
UPDATE otps
SET used = TRUE, used_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) MarkOtpAsUsed(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, MarkOtpAsUsed, id)
return err
}

View File

@ -11,42 +11,81 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const CheckPhoneEmailExist = `-- name: CheckPhoneEmailExist :one
SELECT
EXISTS (SELECT 1 FROM users WHERE users.phone_number = $1 AND users.phone_number IS NOT NULL) AS phone_exists,
EXISTS (SELECT 1 FROM users WHERE users.email = $2 AND users.email IS NOT NULL) AS email_exists
`
type CheckPhoneEmailExistParams struct {
PhoneNumber pgtype.Text
Email pgtype.Text
}
type CheckPhoneEmailExistRow struct {
PhoneExists bool
EmailExists bool
}
func (q *Queries) CheckPhoneEmailExist(ctx context.Context, arg CheckPhoneEmailExistParams) (CheckPhoneEmailExistRow, error) {
row := q.db.QueryRow(ctx, CheckPhoneEmailExist, arg.PhoneNumber, arg.Email)
var i CheckPhoneEmailExistRow
err := row.Scan(&i.PhoneExists, &i.EmailExists)
return i, err
}
const CreateUser = `-- name: CreateUser :one
INSERT INTO users (first_name, last_name, email, phone_number, password, role, verified)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at
INSERT INTO users (first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
`
type CreateUserParams struct {
FirstName string
LastName string
Email string
PhoneNumber string
Password []byte
Role string
Verified pgtype.Bool
FirstName string
LastName string
Email pgtype.Text
PhoneNumber pgtype.Text
Role string
Password []byte
EmailVerified bool
PhoneVerified bool
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
type CreateUserRow struct {
ID int64
FirstName string
LastName string
Email pgtype.Text
PhoneNumber pgtype.Text
Role string
EmailVerified bool
PhoneVerified bool
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
row := q.db.QueryRow(ctx, CreateUser,
arg.FirstName,
arg.LastName,
arg.Email,
arg.PhoneNumber,
arg.Password,
arg.Role,
arg.Verified,
arg.Password,
arg.EmailVerified,
arg.PhoneVerified,
)
var i User
var i CreateUserRow
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Password,
&i.Role,
&i.Verified,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
)
@ -54,7 +93,8 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
}
const DeleteUser = `-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1
DELETE FROM users
WHERE id = $1
`
func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
@ -63,27 +103,41 @@ func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
}
const GetAllUsers = `-- name: GetAllUsers :many
SELECT id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at FROM users
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
FROM users
`
func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
type GetAllUsersRow struct {
ID int64
FirstName string
LastName string
Email pgtype.Text
PhoneNumber pgtype.Text
Role string
EmailVerified bool
PhoneVerified bool
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
func (q *Queries) GetAllUsers(ctx context.Context) ([]GetAllUsersRow, error) {
rows, err := q.db.Query(ctx, GetAllUsers)
if err != nil {
return nil, err
}
defer rows.Close()
var items []User
var items []GetAllUsersRow
for rows.Next() {
var i User
var i GetAllUsersRow
if err := rows.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Password,
&i.Role,
&i.Verified,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
@ -97,8 +151,47 @@ func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
return items, nil
}
const GetUserByEmail = `-- name: GetUserByEmail :one
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
FROM users
WHERE email = $1
`
type GetUserByEmailRow struct {
ID int64
FirstName string
LastName string
Email pgtype.Text
PhoneNumber pgtype.Text
Role string
EmailVerified bool
PhoneVerified bool
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
func (q *Queries) GetUserByEmail(ctx context.Context, email pgtype.Text) (GetUserByEmailRow, error) {
row := q.db.QueryRow(ctx, GetUserByEmail, email)
var i GetUserByEmailRow
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetUserByID = `-- name: GetUserByID :one
SELECT id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at FROM users WHERE id = $1
SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, suspended_at, suspended
FROM users
WHERE id = $1
`
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
@ -110,40 +203,95 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Password,
&i.Role,
&i.Verified,
&i.Password,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.SuspendedAt,
&i.Suspended,
)
return i, err
}
const GetUserByPhone = `-- name: GetUserByPhone :one
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
FROM users
WHERE phone_number = $1
`
type GetUserByPhoneRow struct {
ID int64
FirstName string
LastName string
Email pgtype.Text
PhoneNumber pgtype.Text
Role string
EmailVerified bool
PhoneVerified bool
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
func (q *Queries) GetUserByPhone(ctx context.Context, phoneNumber pgtype.Text) (GetUserByPhoneRow, error) {
row := q.db.QueryRow(ctx, GetUserByPhone, phoneNumber)
var i GetUserByPhoneRow
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const UpdatePassword = `-- name: UpdatePassword :exec
UPDATE users
SET password = $1, updated_at = CURRENT_TIMESTAMP
WHERE (email = $2 OR phone_number = $3)
`
type UpdatePasswordParams struct {
Password []byte
Email pgtype.Text
PhoneNumber pgtype.Text
}
func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error {
_, err := q.db.Exec(ctx, UpdatePassword, arg.Password, arg.Email, arg.PhoneNumber)
return err
}
const UpdateUser = `-- name: UpdateUser :exec
UPDATE users SET first_name = $2, last_name = $3, email = $4, phone_number = $5, password = $6, role = $7, verified = $8, updated_at = CURRENT_TIMESTAMP WHERE id = $1
UPDATE users
SET first_name = $1, last_name = $2, email = $3, phone_number = $4, role = $5, updated_at = CURRENT_TIMESTAMP
WHERE id = $6
`
type UpdateUserParams struct {
ID int64
FirstName string
LastName string
Email string
PhoneNumber string
Password []byte
Email pgtype.Text
PhoneNumber pgtype.Text
Role string
Verified pgtype.Bool
ID int64
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
_, err := q.db.Exec(ctx, UpdateUser,
arg.ID,
arg.FirstName,
arg.LastName,
arg.Email,
arg.PhoneNumber,
arg.Password,
arg.Role,
arg.Verified,
arg.ID,
)
return err
}

View File

@ -1,6 +1,16 @@
package domain
import "time"
import (
"errors"
"time"
)
var (
ErrOtpNotFound = errors.New("otp not found")
ErrOtpAlreadyUsed = errors.New("otp already used")
ErrInvalidOtp = errors.New("invalid otp")
ErrOtpExpired = errors.New("otp expired")
)
type OtpFor string

View File

@ -1,6 +1,13 @@
package domain
import "time"
import (
"errors"
"time"
)
var (
ErrUserNotFound = errors.New("user not found")
)
type User struct {
ID int64

View File

@ -0,0 +1,18 @@
package mockemail
import (
"context"
"fmt"
)
type MockEmail struct {
}
func NewMockEmail() *MockEmail {
return &MockEmail{}
}
func (m *MockEmail) SendEmailOTP(ctx context.Context, email string, otp string) error {
fmt.Println("MockEmail: Sending OTP to", email, "with OTP:", otp)
return nil
}

View File

@ -0,0 +1,19 @@
package mocksms
import (
"context"
"fmt"
)
type MockSMS struct {
}
func NewMockSMS() *MockSMS {
return &MockSMS{}
}
func (m *MockSMS) SendSMSOTP(ctx context.Context, phoneNumber, otp string) error {
fmt.Println("MockSMS: Sending OTP to", phoneNumber, "with OTP:", otp)
return nil
}
// func (m *MockSms){}

View File

@ -13,8 +13,14 @@ import (
func (s *Store) GetUserByEmailPhone(ctx context.Context, email, phone string) (domain.User, error) {
user, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{
Email: email,
PhoneNumber: phone,
Email: pgtype.Text{
String: email,
Valid: true,
},
PhoneNumber: pgtype.Text{
String: phone,
Valid: true,
},
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
@ -26,8 +32,8 @@ func (s *Store) GetUserByEmailPhone(ctx context.Context, email, phone string) (d
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Email: user.Email.String,
PhoneNumber: user.PhoneNumber.String,
Password: user.Password,
Role: domain.Role(user.Role),
}, nil

View File

@ -0,0 +1,50 @@
package repository
import (
"context"
"database/sql"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
func (s *Store) CreateOtp(ctx context.Context, otp domain.Otp) error {
return s.queries.CreateOtp(ctx, dbgen.CreateOtpParams{
SentTo: otp.SentTo,
Medium: string(otp.Medium),
OtpFor: string(otp.For),
Otp: otp.Otp,
ExpiresAt: pgtype.Timestamptz{
Time: otp.ExpiresAt,
Valid: true,
},
})
}
func (s *Store) GetOtp(ctx context.Context, sentTo string, sentfor domain.OtpFor, medium domain.OtpMedium) (domain.Otp, error) {
row, err := s.queries.GetOtp(ctx, dbgen.GetOtpParams{
SentTo: sentTo,
Medium: string(medium),
OtpFor: string(sentfor),
})
if err != nil {
if err == sql.ErrNoRows {
return domain.Otp{}, domain.ErrOtpNotFound
}
return domain.Otp{}, err
}
return domain.Otp{
ID: row.ID,
SentTo: row.SentTo,
Medium: domain.OtpMedium(row.Medium),
For: domain.OtpFor(row.OtpFor),
Otp: row.Otp,
Used: row.Used,
UsedAt: row.UsedAt.Time,
CreatedAt: row.CreatedAt.Time,
ExpiresAt: row.ExpiresAt.Time,
}, nil
}
func (s *Store) MarkOtpAsUsed(ctx context.Context, otp domain.Otp) error {
return s.queries.MarkOtpAsUsed(ctx, otp.ID)
}

View File

@ -2,47 +2,70 @@ package repository
import (
"context"
"database/sql"
"errors"
"fmt"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
func (s *Store) CreateUser(ctx context.Context, firstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error) {
user, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{
FirstName: firstName,
LastName: lastName,
Email: email,
PhoneNumber: phoneNumber,
// Password: password,
Role: role,
func (s *Store) CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error) {
err := s.queries.MarkOtpAsUsed(ctx, usedOtpId)
if err != nil {
return domain.User{}, err
}
userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{
FirstName: user.FirstName,
LastName: user.LastName,
Email: pgtype.Text{
String: user.Email,
Valid: user.Email != "",
},
PhoneNumber: pgtype.Text{
String: user.PhoneNumber,
Valid: user.PhoneNumber != "",
},
Password: user.Password,
Role: string(user.Role),
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
})
if err != nil {
return domain.User{}, err
}
return domain.User{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Password: user.Password,
// Role: user.Role,
ID: userRes.ID,
FirstName: userRes.FirstName,
LastName: userRes.LastName,
Email: userRes.Email.String,
PhoneNumber: userRes.PhoneNumber.String,
Role: domain.Role(userRes.Role),
}, nil
}
func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
user, err := s.queries.GetUserByID(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.User{}, domain.ErrUserNotFound
}
return domain.User{}, err
}
return domain.User{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Password: user.Password,
// Role: user.Role,
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email.String,
PhoneNumber: user.PhoneNumber.String,
Role: domain.Role(user.Role),
EmailVerified: user.EmailVerified,
Password: user.Password,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt.Time,
UpdatedAt: user.UpdatedAt.Time,
SuspendedAt: user.SuspendedAt.Time,
Suspended: user.Suspended,
}, nil
}
func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) {
@ -50,32 +73,118 @@ func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) {
if err != nil {
return nil, err
}
var result []domain.User
for _, user := range users {
result = append(result, domain.User{
userList := make([]domain.User, len(users))
for i, user := range users {
userList[i] = domain.User{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Password: user.Password,
// Role: user.Role,
})
Email: user.Email.String,
PhoneNumber: user.PhoneNumber.String,
Role: domain.Role(user.Role),
}
}
return result, nil
return userList, nil
}
func (s *Store) UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error {
func (s *Store) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error {
err := s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
ID: id,
FirstName: firstName,
LastName: lastName,
Email: email,
PhoneNumber: phoneNumber,
// Password: password,
Role: role,
// ID: user.ID,
// FirstName: user.FirstName,
// LastName: user.LastName,
// Email: user.Email,
// PhoneNumber: user.PhoneNumber,
})
return err
if err != nil {
return err
}
return nil
}
func (s *Store) DeleteUser(ctx context.Context, id int64) error {
return s.queries.DeleteUser(ctx, id)
err := s.queries.DeleteUser(ctx, id)
if err != nil {
return err
}
return nil
}
func (s *Store) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) {
fmt.Printf("phoneNum: %s, email: %s\n", phoneNum, email)
row, err := s.queries.CheckPhoneEmailExist(ctx, dbgen.CheckPhoneEmailExistParams{
PhoneNumber: pgtype.Text{
String: phoneNum,
Valid: phoneNum != "",
},
Email: pgtype.Text{
String: email,
Valid: email != "",
},
})
fmt.Printf("row: %+v\n", row)
if err != nil {
return false, false, err
}
return row.EmailExists, row.PhoneExists, nil
}
func (s *Store) GetUserByEmail(ctx context.Context, email string) (domain.User, error) {
user, err := s.queries.GetUserByEmail(ctx, pgtype.Text{
String: email,
Valid: true,
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.User{}, domain.ErrUserNotFound
}
return domain.User{}, err
}
return domain.User{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email.String,
PhoneNumber: user.PhoneNumber.String,
Role: domain.Role(user.Role),
}, nil
}
func (s *Store) GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error) {
user, err := s.queries.GetUserByPhone(ctx, pgtype.Text{
String: phoneNum,
Valid: true,
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.User{}, domain.ErrUserNotFound
}
return domain.User{}, err
}
return domain.User{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email.String,
PhoneNumber: user.PhoneNumber.String,
Role: domain.Role(user.Role),
}, nil
}
func (s *Store) UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error {
err := s.queries.MarkOtpAsUsed(ctx, usedOtpId)
if err != nil {
return err
}
err = s.queries.UpdatePassword(ctx, dbgen.UpdatePasswordParams{
Password: password,
Email: pgtype.Text{
String: identifier,
Valid: true,
},
PhoneNumber: pgtype.Text{
String: identifier,
Valid: true,
},
})
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,44 @@
package user
import (
"context"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"golang.org/x/crypto/bcrypt"
)
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium) error {
otpCode := "123456" // Generate OTP code
otp := domain.Otp{
SentTo: sentTo,
Medium: medium,
For: otpFor,
Otp: otpCode,
Used: false,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(OtpExpiry),
}
err := s.otpStore.CreateOtp(ctx, otp)
if err != nil {
return err
}
switch medium {
case domain.OtpMediumSms:
return s.smsGateway.SendSMSOTP(ctx, sentTo, otpCode)
case domain.OtpMediumEmail:
return s.emailGateway.SendEmailOTP(ctx, sentTo, otpCode)
}
return nil
}
func hashPassword(plaintextPassword string) ([]byte, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
if err != nil {
return []byte{}, err
}
return hash, nil
}

View File

@ -27,5 +27,4 @@ type EmailGateway interface {
type OtpStore interface {
CreateOtp(ctx context.Context, otp domain.Otp) error
GetOtp(ctx context.Context, sentTo string, sentfor domain.OtpFor, medium domain.OtpMedium) (domain.Otp, error)
MarkUsed(ctx context.Context, id int64) error
}

View File

@ -0,0 +1,79 @@
package user
import (
"context"
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
}
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error {
var err error
// check if user exists
switch medium {
case domain.OtpMediumEmail:
_, err = s.userStore.GetUserByEmail(ctx, sentTo)
case domain.OtpMediumSms:
_, err = s.userStore.GetUserByPhone(ctx, sentTo)
}
if err != nil && err != domain.ErrUserNotFound {
return err
}
// send otp based on the medium
return s.SendOtp(ctx, sentTo, domain.OtpRegister, medium)
}
func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) { // normal
// get otp
fmt.Printf("registerReq: %+v\n", registerReq)
var sentTo string
if registerReq.OtpMedium == domain.OtpMediumEmail {
sentTo = registerReq.Email
} else {
sentTo = registerReq.PhoneNumber
}
//
otp, err := s.otpStore.GetOtp(
ctx, sentTo,
domain.OtpRegister, registerReq.OtpMedium)
if err != nil {
return domain.User{}, err
}
// verify otp
if otp.Used {
return domain.User{}, domain.ErrOtpAlreadyUsed
}
if time.Now().After(otp.ExpiresAt) {
return domain.User{}, domain.ErrOtpExpired
}
if otp.Otp != registerReq.Otp {
return domain.User{}, domain.ErrInvalidOtp
}
hashedPassword, err := hashPassword(registerReq.Password)
if err != nil {
return domain.User{}, err
}
userR := domain.User{
FirstName: registerReq.FirstName,
LastName: registerReq.LastName,
Email: registerReq.Email,
PhoneNumber: registerReq.PhoneNumber,
Password: hashedPassword,
Role: "user",
EmailVerified: registerReq.OtpMedium == domain.OtpMediumEmail,
PhoneVerified: registerReq.OtpMedium == domain.OtpMediumSms,
}
// create the user and mark otp as used
user, err := s.userStore.CreateUser(ctx, userR, otp.ID)
if err != nil {
return domain.User{}, err
}
return user, nil
}

View File

@ -0,0 +1,63 @@
package user
import (
"context"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error {
var err error
// check if user exists
switch medium {
case domain.OtpMediumEmail:
_, err = s.userStore.GetUserByEmail(ctx, sentTo)
case domain.OtpMediumSms:
_, err = s.userStore.GetUserByPhone(ctx, sentTo)
}
if err != nil {
return err
}
return s.SendOtp(ctx, sentTo, domain.OtpReset, medium)
}
func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswordReq) error {
var sentTo string
if resetReq.OtpMedium == domain.OtpMediumEmail {
sentTo = resetReq.Email
} else {
sentTo = resetReq.PhoneNumber
}
otp, err := s.otpStore.GetOtp(
ctx, sentTo,
domain.OtpReset, resetReq.OtpMedium)
if err != nil {
return err
}
//
if otp.Used {
return domain.ErrOtpAlreadyUsed
}
if time.Now().After(otp.ExpiresAt) {
return domain.ErrOtpExpired
}
if otp.Otp != resetReq.Otp {
return domain.ErrInvalidOtp
}
// hash password
hashedPassword, err := hashPassword(resetReq.Password)
if err != nil {
return err
}
// reset pass and mark otp as used
err = s.userStore.UpdatePassword(ctx, sentTo, hashedPassword, otp.ID)
if err != nil {
return err
}
return nil
}

View File

@ -1,24 +1,13 @@
package user
import (
"context"
"errors"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"golang.org/x/crypto/bcrypt"
)
const (
OtpExpiry = 5 * time.Minute
)
var (
ErrOtpAlreadyUsed = errors.New("otp already used")
ErrInvalidOtp = errors.New("invalid otp")
ErrOtpExpired = errors.New("otp expired")
)
type Service struct {
userStore UserStore
otpStore OtpStore
@ -27,179 +16,14 @@ type Service struct {
}
func NewService(
userStore UserStore, RefreshExpiry int,
userStore UserStore,
otpStore OtpStore, smsGateway SmsGateway,
emailGateway EmailGateway,
) *Service {
return &Service{
userStore: userStore,
otpStore: otpStore,
userStore: userStore,
otpStore: otpStore,
smsGateway: smsGateway,
emailGateway: emailGateway,
}
}
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
}
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error {
var err error
// check if user exists
switch medium {
case domain.OtpMediumEmail:
_, err = s.userStore.GetUserByEmail(ctx, sentTo)
case domain.OtpMediumSms:
_, err = s.userStore.GetUserByPhone(ctx, sentTo)
}
if err != nil {
return err
}
// send otp based on the medium
return s.SendOtp(ctx, sentTo, domain.OtpReset, medium)
}
func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) { // normal
// get otp
var sentTo string
if registerReq.OtpMedium == domain.OtpMediumEmail {
sentTo = registerReq.Email
} else {
sentTo = registerReq.PhoneNumber
}
//
otp, err := s.otpStore.GetOtp(
ctx, sentTo,
domain.OtpRegister, registerReq.OtpMedium)
if err != nil {
return domain.User{}, err
}
// verify otp
if otp.Used {
return domain.User{}, ErrOtpAlreadyUsed
}
if time.Now().After(otp.ExpiresAt) {
return domain.User{}, ErrOtpExpired
}
if otp.Otp != registerReq.Otp {
return domain.User{}, ErrInvalidOtp
}
hashedPassword, err := hashPassword(registerReq.Password)
if err != nil {
return domain.User{}, err
}
userR := domain.User{
FirstName: registerReq.FirstName,
LastName: registerReq.LastName,
Email: registerReq.Email,
PhoneNumber: registerReq.PhoneNumber,
Password: hashedPassword,
Role: "user",
EmailVerified: registerReq.OtpMedium == domain.OtpMediumEmail,
PhoneVerified: registerReq.OtpMedium == domain.OtpMediumSms,
}
// create the user and mark otp as used
user, err := s.userStore.CreateUser(ctx, userR, otp.ID)
if err != nil {
return domain.User{}, err
}
return user, nil
}
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error {
var err error
// check if user exists
switch medium {
case domain.OtpMediumEmail:
_, err = s.userStore.GetUserByEmail(ctx, sentTo)
case domain.OtpMediumSms:
_, err = s.userStore.GetUserByPhone(ctx, sentTo)
}
if err != nil {
return err
}
return s.SendOtp(ctx, sentTo, domain.OtpReset, medium)
}
func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswordReq) error {
var sentTo string
if resetReq.OtpMedium == domain.OtpMediumEmail {
sentTo = resetReq.Email
} else {
sentTo = resetReq.PhoneNumber
}
otp, err := s.otpStore.GetOtp(
ctx, sentTo,
domain.OtpRegister, resetReq.OtpMedium)
if err != nil {
return err
}
//
if otp.Used {
return ErrOtpAlreadyUsed
}
if time.Now().After(otp.ExpiresAt) {
return ErrOtpExpired
}
if otp.Otp != resetReq.Otp {
return ErrInvalidOtp
}
// hash password
hashedPassword, err := hashPassword(resetReq.Password)
if err != nil {
return err
}
// reset pass and mark otp as used
err = s.userStore.UpdatePassword(ctx, sentTo, hashedPassword, otp.ID)
if err != nil {
return err
}
return nil
}
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium) error {
otpCode := "123456" // Generate OTP code
otp := domain.Otp{
SentTo: sentTo,
Medium: medium,
For: otpFor,
Otp: otpCode,
Used: false,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(OtpExpiry),
}
err := s.otpStore.CreateOtp(ctx, otp)
if err != nil {
return err
}
switch medium {
case domain.OtpMediumSms:
return s.smsGateway.SendSMSOTP(ctx, sentTo, otpCode)
case domain.OtpMediumEmail:
return s.emailGateway.SendEmailOTP(ctx, sentTo, otpCode)
}
return nil
}
func (s *Service) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error {
// update user
return s.userStore.UpdateUser(ctx, user)
}
func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
return s.userStore.GetUserByID(ctx, id)
}
func hashPassword(plaintextPassword string) ([]byte, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
if err != nil {
return []byte{}, err
}
return hash, nil
}

View File

@ -0,0 +1,16 @@
package user
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Service) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error {
// update user
return s.userStore.UpdateUser(ctx, user)
}
func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
return s.userStore.GetUserByID(ctx, id)
}

View File

@ -5,6 +5,7 @@ import (
"log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/bytedance/sonic"
@ -16,6 +17,7 @@ type App struct {
logger *slog.Logger
port int
authSvc *authentication.Service
userSvc *user.Service
validator *customvalidator.CustomValidator
JwtConfig jwtutil.JwtConfig
}
@ -25,6 +27,7 @@ func NewApp(
authSvc *authentication.Service,
logger *slog.Logger,
JwtConfig jwtutil.JwtConfig,
userSvc *user.Service,
) *App {
app := fiber.New(fiber.Config{
CaseSensitive: true,
@ -39,6 +42,7 @@ func NewApp(
validator: validator,
logger: logger,
JwtConfig: JwtConfig,
userSvc: userSvc,
}
s.initAppRoutes()

View File

@ -3,7 +3,6 @@ package handlers
import (
"errors"
"log/slog"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
@ -66,7 +65,7 @@ func LoginCustomer(
return nil
}
accessToken, err := jwtutil.CreateJwt(strconv.Itoa(int(successRes.UserId)), successRes.Role, JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry)
accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry)
res := loginCustomerRes{
AccessToken: accessToken,
RefreshToken: successRes.RfToken,
@ -119,7 +118,7 @@ func RefreshToken(logger *slog.Logger, authSvc *authentication.Service,
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
return nil
}
accessToken, err := jwtutil.CreateJwt("", "", JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry)
accessToken, err := jwtutil.CreateJwt(0, "", JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry)
if err != nil {
logger.Error("Create jwt failed", "error", err)
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)

View File

@ -0,0 +1,365 @@
package handlers
import (
"errors"
"log/slog"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2"
)
type CheckPhoneEmailExistReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
}
type CheckPhoneEmailExistRes struct {
EmailExist bool `json:"email_exist"`
PhoneNumberExist bool `json:"phone_number_exist"`
}
// CheckPhoneEmailExist godoc
// @Summary Check if phone number or email exist
// @Description Check if phone number or email exist
// @Tags user
// @Accept json
// @Produce json
// @Param checkPhoneEmailExist body CheckPhoneEmailExistReq true "Check phone number or email exist"
// @Success 200 {object} CheckPhoneEmailExistRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/checkPhoneEmailExist [post]
func CheckPhoneEmailExist(logger *slog.Logger, userSvc *user.Service,
validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
var req CheckPhoneEmailExistReq
if err := c.BodyParser(&req); err != nil {
logger.Error("CheckPhoneEmailExist failed", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request",
})
}
valErrs, ok := validator.Validate(c, req)
if !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
}
emailExist, phoneExist, err := userSvc.CheckPhoneEmailExist(c.Context(), req.PhoneNumber, req.Email)
if err != nil {
logger.Error("CheckPhoneEmailExist failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
res := CheckPhoneEmailExistRes{
EmailExist: emailExist,
PhoneNumberExist: phoneExist,
}
return response.WriteJSON(c, fiber.StatusOK, "Check Success", res, nil)
}
}
type RegisterCodeReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
}
// SendRegisterCode godoc
// @Summary Send register code
// @Description Send register code
// @Tags user
// @Accept json
// @Produce json
// @Param registerCode body RegisterCodeReq true "Send register code"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/sendRegisterCode [post]
func SendRegisterCode(logger *slog.Logger, userSvc *user.Service,
validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
var req RegisterCodeReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request",
})
}
valErrs, ok := validator.Validate(c, req)
if !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
}
var sentTo string
var medium domain.OtpMedium
if req.Email != "" {
sentTo = req.Email
medium = domain.OtpMediumEmail
}
if req.PhoneNumber != "" {
sentTo = req.PhoneNumber
medium = domain.OtpMediumSms
}
if err := userSvc.SendRegisterCode(c.Context(), medium, sentTo); err != nil {
logger.Error("SendRegisterCode failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil)
}
}
type RegisterUserReq struct {
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"`
//Role string
Otp string `json:"otp" example:"123456"`
ReferalCode string `json:"referal_code" example:"ABC123"`
//
}
// RegisterUser godoc
// @Summary Register user
// @Description Register user
// @Tags user
// @Accept json
// @Produce json
// @Param registerUser body RegisterUserReq true "Register user"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/register [post]
func RegisterUser(logger *slog.Logger, userSvc *user.Service,
validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
var req RegisterUserReq
if err := c.BodyParser(&req); err != nil {
logger.Error("RegisterUser failed", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request",
})
}
valErrs, ok := validator.Validate(c, req)
if !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
}
user := domain.RegisterUserReq{
FirstName: req.FirstName,
LastName: req.LastName,
Email: req.Email,
PhoneNumber: req.PhoneNumber,
Password: req.Password,
Otp: req.Otp,
ReferalCode: req.ReferalCode,
OtpMedium: domain.OtpMediumEmail,
}
medium, err := getMedium(req.Email, req.PhoneNumber)
if err != nil {
logger.Error("RegisterUser failed", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}
user.OtpMedium = medium
if _, err := userSvc.RegisterUser(c.Context(), user); err != nil {
if errors.Is(err, domain.ErrOtpAlreadyUsed) {
return response.WriteJSON(c, fiber.StatusBadRequest, "Otp already used", nil, nil)
}
if errors.Is(err, domain.ErrOtpExpired) {
return response.WriteJSON(c, fiber.StatusBadRequest, "Otp expired", nil, nil)
}
if errors.Is(err, domain.ErrInvalidOtp) {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid otp", nil, nil)
}
if errors.Is(err, domain.ErrOtpNotFound) {
return response.WriteJSON(c, fiber.StatusBadRequest, "User already exist", nil, nil)
}
logger.Error("RegisterUser failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
return response.WriteJSON(c, fiber.StatusOK, "Registration successful", nil, nil)
}
}
type ResetCodeReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
}
// SendResetCode godoc
// @Summary Send reset code
// @Description Send reset code
// @Tags user
// @Accept json
// @Produce json
// @Param resetCode body ResetCodeReq true "Send reset code"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/sendResetCode [post]
func SendResetCode(logger *slog.Logger, userSvc *user.Service,
validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
var req ResetCodeReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request",
})
}
valErrs, ok := validator.Validate(c, req)
if !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
}
var sentTo string
var medium domain.OtpMedium
if req.Email != "" {
sentTo = req.Email
medium = domain.OtpMediumEmail
}
if req.PhoneNumber != "" {
sentTo = req.PhoneNumber
medium = domain.OtpMediumSms
}
if err := userSvc.SendResetCode(c.Context(), medium, sentTo); err != nil {
logger.Error("SendResetCode failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil)
}
}
type ResetPasswordReq struct {
Email string
PhoneNumber string
Password string
Otp string
}
// ResetPassword godoc
// @Summary Reset password
// @Description Reset password
// @Tags user
// @Accept json
// @Produce json
// @Param resetPassword body ResetPasswordReq true "Reset password"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/resetPassword [post]
func ResetPassword(logger *slog.Logger, userSvc *user.Service,
validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
var req ResetPasswordReq
if err := c.BodyParser(&req); err != nil {
logger.Error("ResetPassword failed", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request",
})
}
valErrs, ok := validator.Validate(c, req)
if !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
}
user := domain.ResetPasswordReq{
Email: req.Email,
PhoneNumber: req.PhoneNumber,
Password: req.Password,
Otp: req.Otp,
}
medium, err := getMedium(req.Email, req.PhoneNumber)
if err != nil {
logger.Error("ResetPassword failed", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}
user.OtpMedium = medium
if err := userSvc.ResetPassword(c.Context(), user); err != nil {
logger.Error("ResetPassword failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
return response.WriteJSON(c, fiber.StatusOK, "Password reset successful", nil, nil)
}
}
type UserProfileRes struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
Role domain.Role `json:"role"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"`
}
// UserProfile godoc
// @Summary Get user profile
// @Description Get user profile
// @Tags user
// @Accept json
// @Produce json
// @Success 200 {object} UserProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security Bearer
// @Router /user/profile [get]
func UserProfile(logger *slog.Logger, userSvc *user.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
userId := c.Locals("user_id").(int64)
user, err := userSvc.GetUserByID(c.Context(), userId)
if err != nil {
logger.Error("GetUserProfile failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
res := UserProfileRes{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
SuspendedAt: user.SuspendedAt,
Suspended: user.Suspended,
}
return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil)
}
}
func getMedium(email, phoneNumber string) (domain.OtpMedium, error) {
if email != "" {
return domain.OtpMediumEmail, nil
}
if phoneNumber != "" {
return domain.OtpMediumSms, nil
}
return "", errors.New("both email and phone number are empty")
}

View File

@ -20,7 +20,7 @@ var (
type UserClaim struct {
jwt.RegisteredClaims
UserId string
UserId int64
Role domain.Role
}
type JwtConfig struct {
@ -28,7 +28,7 @@ type JwtConfig struct {
JwtAccessExpiry int
}
func CreateJwt(userId string, Role domain.Role, key string, expiry int) (string, error) {
func CreateJwt(userId int64, Role domain.Role, key string, expiry int) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{RegisteredClaims: jwt.RegisteredClaims{Issuer: "github.com/lafetz/snippitstash",
IssuedAt: jwt.NewNumericDate(time.Now()),
Audience: jwt.ClaimStrings{"fortune.com"},

View File

@ -34,7 +34,7 @@ func (a *App) authMiddleware(c *fiber.Ctx) error {
// refreshToken = c.Cookies("refresh_token", "")
return fiber.NewError(fiber.StatusUnauthorized, "Refresh token missing")
// return fiber.NewError(fiber.StatusUnauthorized, "Refresh token missing")
}
c.Locals("user_id", claim.UserId)
c.Locals("role", claim.Role)

View File

@ -20,5 +20,19 @@ func (a *App) initAppRoutes() {
a.logger.Info("Refresh Token: " + refreshToken.(string))
return c.SendString("Test endpoint")
})
a.fiber.Post("/user/resetPassword", handlers.ResetPassword(a.logger, a.userSvc, a.validator))
a.fiber.Post("/user/sendResetCode", handlers.SendResetCode(a.logger, a.userSvc, a.validator))
a.fiber.Post("/user/register", handlers.RegisterUser(a.logger, a.userSvc, a.validator))
a.fiber.Post("/user/sendRegisterCode", handlers.SendRegisterCode(a.logger, a.userSvc, a.validator))
a.fiber.Post("/user/checkPhoneEmailExist", handlers.CheckPhoneEmailExist(a.logger, a.userSvc, a.validator))
a.fiber.Get("/user/profile", a.authMiddleware, handlers.UserProfile(a.logger, a.userSvc))
// Swagger
a.fiber.Get("/swagger/*", fiberSwagger.WrapHandler)
}
///user/profile get
// @Router /user/resetPassword [post]
// @Router /user/sendResetCode [post]
// @Router /user/register [post]
// @Router /user/sendRegisterCode [post]
// @Router /user/checkPhoneEmailExist [post]