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

View File

@ -4,18 +4,16 @@ CREATE TABLE users (
last_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE , email VARCHAR(255) UNIQUE ,
phone_number VARCHAR(20) UNIQUE, phone_number VARCHAR(20) UNIQUE,
password BYTEA NOT NULL,
role VARCHAR(50) NOT NULL, role VARCHAR(50) NOT NULL,
password BYTEA NOT NULL,
email_verified BOOLEAN NOT NULL DEFAULT FALSE, email_verified BOOLEAN NOT NULL DEFAULT FALSE,
phone_verified BOOLEAN NOT NULL DEFAULT FALSE, phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, 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_at TIMESTAMPTZ NULL, -- this can be NULL if the user is not suspended
suspended BOOLEAN NOT NULL DEFAULT FALSE, suspended BOOLEAN NOT NULL DEFAULT FALSE,
CHECK ( CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
(email IS NOT NULL AND phone_number IS NULL) OR
(email IS NULL AND phone_number IS NOT NULL)
)
); );
CREATE TABLE refresh_tokens ( CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
@ -26,19 +24,38 @@ CREATE TABLE refresh_tokens (
revoked BOOLEAN DEFAULT FALSE NOT NULL, revoked BOOLEAN DEFAULT FALSE NOT NULL,
CONSTRAINT unique_token UNIQUE (token) 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------------------------------------------------------------- ----------------------------------------------seed data-------------------------------------------------------------
-------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- -------------------------------------- DO NOT USE IN PRODUCTION-------------------------------------------------
CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO users (first_name, last_name, email, phone_number, password, role, created_at, updated_at) INSERT INTO users (
VALUES ( first_name, last_name, email, phone_number, password, role,
email_verified, phone_verified, created_at, updated_at,
suspended_at, suspended
) VALUES (
'John', 'John',
'Doe', 'Doe',
'john.doe@example.com', 'john.doe@example.com',
'1234567890', NULL,
crypt('password123', gen_salt('bf'))::bytea, crypt('password123', gen_salt('bf'))::bytea,
'user', 'customer',
TRUE,
FALSE,
CURRENT_TIMESTAMP, 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 -- name: CreateUser :one
INSERT INTO users (first_name, last_name, email, phone_number, password, role, verified)
VALUES ($1, $2, $3, $4, $5, $6, $7) INSERT INTO users (first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at)
RETURNING *; 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 -- name: GetUserByID :one
SELECT * FROM users WHERE id = $1; SELECT *
FROM users
WHERE id = $1;
-- name: GetAllUsers :many -- 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 -- 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 -- 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", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/httpserver.loginCustomerReq" "$ref": "#/definitions/handlers.loginCustomerReq"
} }
} }
], ],
@ -52,7 +52,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/httpserver.loginCustomerRes" "$ref": "#/definitions/handlers.loginCustomerRes"
} }
}, },
"400": { "400": {
@ -96,7 +96,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/httpserver.logoutReq" "$ref": "#/definitions/handlers.logoutReq"
} }
} }
], ],
@ -148,7 +148,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/httpserver.refreshToken" "$ref": "#/definitions/handlers.refreshToken"
} }
} }
], ],
@ -156,7 +156,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/httpserver.loginCustomerRes" "$ref": "#/definitions/handlers.loginCustomerRes"
} }
}, },
"400": { "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": { "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", "type": "object",
"properties": { "properties": {
"email": { "email": {
@ -199,7 +628,7 @@ const docTemplate = `{
} }
} }
}, },
"httpserver.loginCustomerRes": { "handlers.loginCustomerRes": {
"type": "object", "type": "object",
"properties": { "properties": {
"access_token": { "access_token": {
@ -210,7 +639,7 @@ const docTemplate = `{
} }
} }
}, },
"httpserver.logoutReq": { "handlers.logoutReq": {
"type": "object", "type": "object",
"properties": { "properties": {
"refresh_token": { "refresh_token": {
@ -218,7 +647,7 @@ const docTemplate = `{
} }
} }
}, },
"httpserver.refreshToken": { "handlers.refreshToken": {
"type": "object", "type": "object",
"properties": { "properties": {
"access_token": { "access_token": {

View File

@ -36,7 +36,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/httpserver.loginCustomerReq" "$ref": "#/definitions/handlers.loginCustomerReq"
} }
} }
], ],
@ -44,7 +44,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/httpserver.loginCustomerRes" "$ref": "#/definitions/handlers.loginCustomerRes"
} }
}, },
"400": { "400": {
@ -88,7 +88,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/httpserver.logoutReq" "$ref": "#/definitions/handlers.logoutReq"
} }
} }
], ],
@ -140,7 +140,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/httpserver.refreshToken" "$ref": "#/definitions/handlers.refreshToken"
} }
} }
], ],
@ -148,7 +148,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/httpserver.loginCustomerRes" "$ref": "#/definitions/handlers.loginCustomerRes"
} }
}, },
"400": { "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": { "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", "type": "object",
"properties": { "properties": {
"email": { "email": {
@ -191,7 +620,7 @@
} }
} }
}, },
"httpserver.loginCustomerRes": { "handlers.loginCustomerRes": {
"type": "object", "type": "object",
"properties": { "properties": {
"access_token": { "access_token": {
@ -202,7 +631,7 @@
} }
} }
}, },
"httpserver.logoutReq": { "handlers.logoutReq": {
"type": "object", "type": "object",
"properties": { "properties": {
"refresh_token": { "refresh_token": {
@ -210,7 +639,7 @@
} }
} }
}, },
"httpserver.refreshToken": { "handlers.refreshToken": {
"type": "object", "type": "object",
"properties": { "properties": {
"access_token": { "access_token": {

View File

@ -1,5 +1,116 @@
definitions: 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: properties:
email: email:
example: john.doe@example.com example: john.doe@example.com
@ -11,19 +122,19 @@ definitions:
example: "1234567890" example: "1234567890"
type: string type: string
type: object type: object
httpserver.loginCustomerRes: handlers.loginCustomerRes:
properties: properties:
access_token: access_token:
type: string type: string
refresh_token: refresh_token:
type: string type: string
type: object type: object
httpserver.logoutReq: handlers.logoutReq:
properties: properties:
refresh_token: refresh_token:
type: string type: string
type: object type: object
httpserver.refreshToken: handlers.refreshToken:
properties: properties:
access_token: access_token:
type: string type: string
@ -73,14 +184,14 @@ paths:
name: login name: login
required: true required: true
schema: schema:
$ref: '#/definitions/httpserver.loginCustomerReq' $ref: '#/definitions/handlers.loginCustomerReq'
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/httpserver.loginCustomerRes' $ref: '#/definitions/handlers.loginCustomerRes'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -107,7 +218,7 @@ paths:
name: logout name: logout
required: true required: true
schema: schema:
$ref: '#/definitions/httpserver.logoutReq' $ref: '#/definitions/handlers.logoutReq'
produces: produces:
- application/json - application/json
responses: responses:
@ -141,14 +252,14 @@ paths:
name: refresh name: refresh
required: true required: true
schema: schema:
$ref: '#/definitions/httpserver.refreshToken' $ref: '#/definitions/handlers.refreshToken'
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/httpserver.loginCustomerRes' $ref: '#/definitions/handlers.loginCustomerRes'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -164,6 +275,181 @@ paths:
summary: Refresh token summary: Refresh token
tags: tags:
- auth - 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: securityDefinitions:
Bearer: Bearer:
in: header in: header

View File

@ -55,13 +55,13 @@ func (q *Queries) GetRefreshToken(ctx context.Context, token string) (RefreshTok
} }
const GetUserByEmailPhone = `-- name: GetUserByEmailPhone :one 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 WHERE email = $1 OR phone_number = $2
` `
type GetUserByEmailPhoneParams struct { type GetUserByEmailPhoneParams struct {
Email string Email pgtype.Text
PhoneNumber string PhoneNumber pgtype.Text
} }
func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPhoneParams) (User, error) { 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.LastName,
&i.Email, &i.Email,
&i.PhoneNumber, &i.PhoneNumber,
&i.Password,
&i.Role, &i.Role,
&i.Verified, &i.Password,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SuspendedAt,
&i.Suspended,
) )
return i, err return i, err
} }

View File

@ -8,6 +8,18 @@ import (
"github.com/jackc/pgx/v5/pgtype" "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 { type RefreshToken struct {
ID int64 ID int64
UserID int64 UserID int64
@ -21,11 +33,14 @@ type User struct {
ID int64 ID int64
FirstName string FirstName string
LastName string LastName string
Email string Email pgtype.Text
PhoneNumber string PhoneNumber pgtype.Text
Password []byte
Role string Role string
Verified pgtype.Bool Password []byte
EmailVerified bool
PhoneVerified bool
CreatedAt pgtype.Timestamptz CreatedAt pgtype.Timestamptz
UpdatedAt 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" "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 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) INSERT INTO users (first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at)
RETURNING id, first_name, last_name, email, phone_number, password, role, 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 { type CreateUserParams struct {
FirstName string FirstName string
LastName string LastName string
Email string Email pgtype.Text
PhoneNumber string PhoneNumber pgtype.Text
Password []byte
Role string Role string
Verified pgtype.Bool 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, row := q.db.QueryRow(ctx, CreateUser,
arg.FirstName, arg.FirstName,
arg.LastName, arg.LastName,
arg.Email, arg.Email,
arg.PhoneNumber, arg.PhoneNumber,
arg.Password,
arg.Role, arg.Role,
arg.Verified, arg.Password,
arg.EmailVerified,
arg.PhoneVerified,
) )
var i User var i CreateUserRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.FirstName, &i.FirstName,
&i.LastName, &i.LastName,
&i.Email, &i.Email,
&i.PhoneNumber, &i.PhoneNumber,
&i.Password,
&i.Role, &i.Role,
&i.Verified, &i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
) )
@ -54,7 +93,8 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
} }
const DeleteUser = `-- name: DeleteUser :exec 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 { 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 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) rows, err := q.db.Query(ctx, GetAllUsers)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []User var items []GetAllUsersRow
for rows.Next() { for rows.Next() {
var i User var i GetAllUsersRow
if err := rows.Scan( if err := rows.Scan(
&i.ID, &i.ID,
&i.FirstName, &i.FirstName,
&i.LastName, &i.LastName,
&i.Email, &i.Email,
&i.PhoneNumber, &i.PhoneNumber,
&i.Password,
&i.Role, &i.Role,
&i.Verified, &i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@ -97,8 +151,47 @@ func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
return items, nil 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 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) { 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.LastName,
&i.Email, &i.Email,
&i.PhoneNumber, &i.PhoneNumber,
&i.Password,
&i.Role, &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.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
) )
return i, err 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 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 { type UpdateUserParams struct {
ID int64
FirstName string FirstName string
LastName string LastName string
Email string Email pgtype.Text
PhoneNumber string PhoneNumber pgtype.Text
Password []byte
Role string Role string
Verified pgtype.Bool ID int64
} }
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
_, err := q.db.Exec(ctx, UpdateUser, _, err := q.db.Exec(ctx, UpdateUser,
arg.ID,
arg.FirstName, arg.FirstName,
arg.LastName, arg.LastName,
arg.Email, arg.Email,
arg.PhoneNumber, arg.PhoneNumber,
arg.Password,
arg.Role, arg.Role,
arg.Verified, arg.ID,
) )
return err return err
} }

View File

@ -1,6 +1,16 @@
package domain 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 type OtpFor string

View File

@ -1,6 +1,13 @@
package domain package domain
import "time" import (
"errors"
"time"
)
var (
ErrUserNotFound = errors.New("user not found")
)
type User struct { type User struct {
ID int64 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) { func (s *Store) GetUserByEmailPhone(ctx context.Context, email, phone string) (domain.User, error) {
user, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{ user, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{
Email: email, Email: pgtype.Text{
PhoneNumber: phone, String: email,
Valid: true,
},
PhoneNumber: pgtype.Text{
String: phone,
Valid: true,
},
}) })
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
@ -26,8 +32,8 @@ func (s *Store) GetUserByEmailPhone(ctx context.Context, email, phone string) (d
ID: user.ID, ID: user.ID,
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
Email: user.Email, Email: user.Email.String,
PhoneNumber: user.PhoneNumber, PhoneNumber: user.PhoneNumber.String,
Password: user.Password, Password: user.Password,
Role: domain.Role(user.Role), Role: domain.Role(user.Role),
}, nil }, 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 ( import (
"context" "context"
"database/sql"
"errors"
"fmt"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "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) { func (s *Store) CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error) {
user, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{ err := s.queries.MarkOtpAsUsed(ctx, usedOtpId)
FirstName: firstName, if err != nil {
LastName: lastName, return domain.User{}, err
Email: email, }
PhoneNumber: phoneNumber, userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{
// Password: password, FirstName: user.FirstName,
Role: role, 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 { if err != nil {
return domain.User{}, err return domain.User{}, err
} }
return domain.User{ return domain.User{
ID: user.ID, ID: userRes.ID,
FirstName: user.FirstName, FirstName: userRes.FirstName,
LastName: user.LastName, LastName: userRes.LastName,
Email: user.Email, Email: userRes.Email.String,
PhoneNumber: user.PhoneNumber, PhoneNumber: userRes.PhoneNumber.String,
Password: user.Password, Role: domain.Role(userRes.Role),
// Role: user.Role,
}, nil }, nil
} }
func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) { func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
user, err := s.queries.GetUserByID(ctx, id) user, err := s.queries.GetUserByID(ctx, id)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.User{}, domain.ErrUserNotFound
}
return domain.User{}, err return domain.User{}, err
} }
return domain.User{ return domain.User{
ID: user.ID, ID: user.ID,
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
Email: user.Email, Email: user.Email.String,
PhoneNumber: user.PhoneNumber, PhoneNumber: user.PhoneNumber.String,
Role: domain.Role(user.Role),
EmailVerified: user.EmailVerified,
Password: user.Password, Password: user.Password,
// Role: user.Role, PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt.Time,
UpdatedAt: user.UpdatedAt.Time,
SuspendedAt: user.SuspendedAt.Time,
Suspended: user.Suspended,
}, nil }, nil
} }
func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) { 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 { if err != nil {
return nil, err return nil, err
} }
var result []domain.User userList := make([]domain.User, len(users))
for _, user := range users { for i, user := range users {
result = append(result, domain.User{ userList[i] = domain.User{
ID: user.ID, ID: user.ID,
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
Email: user.Email, Email: user.Email.String,
PhoneNumber: user.PhoneNumber, PhoneNumber: user.PhoneNumber.String,
Password: user.Password, Role: domain.Role(user.Role),
// 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{ err := s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
ID: id, // ID: user.ID,
FirstName: firstName, // FirstName: user.FirstName,
LastName: lastName, // LastName: user.LastName,
Email: email, // Email: user.Email,
PhoneNumber: phoneNumber, // PhoneNumber: user.PhoneNumber,
// Password: password,
Role: role,
}) })
if err != nil {
return err return err
}
return nil
} }
func (s *Store) DeleteUser(ctx context.Context, id int64) error { 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 { type OtpStore interface {
CreateOtp(ctx context.Context, otp domain.Otp) error CreateOtp(ctx context.Context, otp domain.Otp) error
GetOtp(ctx context.Context, sentTo string, sentfor domain.OtpFor, medium domain.OtpMedium) (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 package user
import ( import (
"context"
"errors"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"golang.org/x/crypto/bcrypt"
) )
const ( const (
OtpExpiry = 5 * time.Minute OtpExpiry = 5 * time.Minute
) )
var (
ErrOtpAlreadyUsed = errors.New("otp already used")
ErrInvalidOtp = errors.New("invalid otp")
ErrOtpExpired = errors.New("otp expired")
)
type Service struct { type Service struct {
userStore UserStore userStore UserStore
otpStore OtpStore otpStore OtpStore
@ -27,179 +16,14 @@ type Service struct {
} }
func NewService( func NewService(
userStore UserStore, RefreshExpiry int, userStore UserStore,
otpStore OtpStore, smsGateway SmsGateway, otpStore OtpStore, smsGateway SmsGateway,
emailGateway EmailGateway, emailGateway EmailGateway,
) *Service { ) *Service {
return &Service{ return &Service{
userStore: userStore, userStore: userStore,
otpStore: otpStore, 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" "log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "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" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
@ -16,6 +17,7 @@ type App struct {
logger *slog.Logger logger *slog.Logger
port int port int
authSvc *authentication.Service authSvc *authentication.Service
userSvc *user.Service
validator *customvalidator.CustomValidator validator *customvalidator.CustomValidator
JwtConfig jwtutil.JwtConfig JwtConfig jwtutil.JwtConfig
} }
@ -25,6 +27,7 @@ func NewApp(
authSvc *authentication.Service, authSvc *authentication.Service,
logger *slog.Logger, logger *slog.Logger,
JwtConfig jwtutil.JwtConfig, JwtConfig jwtutil.JwtConfig,
userSvc *user.Service,
) *App { ) *App {
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
CaseSensitive: true, CaseSensitive: true,
@ -39,6 +42,7 @@ func NewApp(
validator: validator, validator: validator,
logger: logger, logger: logger,
JwtConfig: JwtConfig, JwtConfig: JwtConfig,
userSvc: userSvc,
} }
s.initAppRoutes() s.initAppRoutes()

View File

@ -3,7 +3,6 @@ package handlers
import ( import (
"errors" "errors"
"log/slog" "log/slog"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
@ -66,7 +65,7 @@ func LoginCustomer(
return nil 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{ res := loginCustomerRes{
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: successRes.RfToken, 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) response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
return nil return nil
} }
accessToken, err := jwtutil.CreateJwt("", "", JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry) accessToken, err := jwtutil.CreateJwt(0, "", JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry)
if err != nil { if err != nil {
logger.Error("Create jwt failed", "error", err) logger.Error("Create jwt failed", "error", err)
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil) 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 { type UserClaim struct {
jwt.RegisteredClaims jwt.RegisteredClaims
UserId string UserId int64
Role domain.Role Role domain.Role
} }
type JwtConfig struct { type JwtConfig struct {
@ -28,7 +28,7 @@ type JwtConfig struct {
JwtAccessExpiry int 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", token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{RegisteredClaims: jwt.RegisteredClaims{Issuer: "github.com/lafetz/snippitstash",
IssuedAt: jwt.NewNumericDate(time.Now()), IssuedAt: jwt.NewNumericDate(time.Now()),
Audience: jwt.ClaimStrings{"fortune.com"}, Audience: jwt.ClaimStrings{"fortune.com"},

View File

@ -34,7 +34,7 @@ func (a *App) authMiddleware(c *fiber.Ctx) error {
// refreshToken = c.Cookies("refresh_token", "") // 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("user_id", claim.UserId)
c.Locals("role", claim.Role) c.Locals("role", claim.Role)

View File

@ -20,5 +20,19 @@ func (a *App) initAppRoutes() {
a.logger.Info("Refresh Token: " + refreshToken.(string)) a.logger.Info("Refresh Token: " + refreshToken.(string))
return c.SendString("Test endpoint") 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) 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]