Merge branch 'main' into feature/notification

This commit is contained in:
Samuel Tariku 2025-04-02 10:55:56 +03:00
commit cfb1e2b7fe
65 changed files with 6926 additions and 222 deletions

View File

@ -8,18 +8,34 @@ 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"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"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"
"github.com/joho/godotenv" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/go-playground/validator/v10"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
) )
// @title FortuneBet API
// @version 1.0
// @description This is server for FortuneBet.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @SecurityDefinitions.apiKey Bearer
// @in header
// @name Authorization
// @BasePath /
func main() { func main() {
err := godotenv.Load()
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
cfg, err := config.NewConfig() cfg, err := config.NewConfig()
if err != nil { if err != nil {
slog.Error(err.Error()) slog.Error(err.Error())
@ -28,18 +44,34 @@ func main() {
db, _, err := repository.OpenDB(cfg.DbUrl) db, _, err := repository.OpenDB(cfg.DbUrl)
if err != nil { if err != nil {
fmt.Print(err) fmt.Print("db", err)
os.Exit(1) os.Exit(1)
} }
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
logger := customlogger.NewLogger("development", slog.LevelDebug, "1.0")
store := repository.NewStore(db) 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)
ticketSvc := ticket.NewService(store)
betSvc := bet.NewService(store)
notificationRepo := repository.NewNotificationRepository(store) notificationRepo := repository.NewNotificationRepository(store)
notificationSvc := notificationservice.New(notificationRepo, logger) notificationSvc := notificationservice.New(notificationRepo, logger)
app := httpserver.NewApp(cfg.Port, logger, notificationSvc) app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry,
}, userSvc, ticketSvc, betSvc, notificationSvc,
)
logger.Info("Starting server", "port", cfg.Port)
if err := app.Run(); err != nil { if err := app.Run(); err != nil {
log.Fatal("Failed to start server with error: ", err) logger.Error("Failed to start server", "error", err)
os.Exit(1)
} }
} }

View File

@ -72,3 +72,8 @@ DROP TABLE IF EXISTS ussd_account;
DROP TYPE IF EXISTS ua_pin_status; DROP TYPE IF EXISTS ua_pin_status;
DROP TYPE IF EXISTS ua_status; DROP TYPE IF EXISTS ua_status;
DROP TYPE IF EXISTS ua_registaration_type; DROP TYPE IF EXISTS ua_registaration_type;
-- Drop FortuneBet
DROP TABLE IF EXIST tickets;
DROP TABLE IF EXIST bets;

View File

@ -2,11 +2,101 @@ CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
first_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE ,
phone_number VARCHAR(20) UNIQUE NOT NULL, phone_number VARCHAR(20) UNIQUE,
password TEXT NOT NULL,
role VARCHAR(50) NOT NULL, role VARCHAR(50) NOT NULL,
verified BOOLEAN DEFAULT FALSE, password BYTEA NOT NULL,
created_at TIMESTAMP, 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 ,
--
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 OR phone_number IS NOT NULL)
);
CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
token TEXT NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
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
);
CREATE TABLE IF NOT EXISTS bets (
id BIGSERIAL PRIMARY KEY,
amount BIGINT NOT NULL,
total_odds REAL NOT NULL,
status INT NOT NULL,
full_name VARCHAR(255) NOT NULL,
phone_number VARCHAR(255) NOT NULL,
branch_id BIGINT,
user_id BIGINT,
cashed_out BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP,
updated_at TIMESTAMP,
is_shop_bet BOOLEAN NOT NULL,
CHECK (user_id IS NOT NULL OR branch_id IS NOT NULL)
);
CREATE TABLE IF NOT EXISTS tickets (
id BIGSERIAL PRIMARY KEY,
amount BIGINT NULL,
total_odds REAL NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
-- CREATE TABLE IF NOT EXISTS bet_outcomes (
-- id BIGSERIAL PRIMARY KEY,
-- bet_id BIGINT NOT NULL,
-- outcome_id BIGINT NOT NULL,
-- );
-- CREATE TABLE IF NOT EXISTS ticket_outcomes (
-- id BIGSERIAL PRIMARY KEY,
-- ticket_id BIGINT NOT NULL,
-- outcome_id BIGINT 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,
email_verified, phone_verified, created_at, updated_at,
suspended_at, suspended
) VALUES (
'John',
'Doe',
'john.doe@example.com',
NULL,
crypt('password123', gen_salt('bf'))::bytea,
'customer',
TRUE,
FALSE,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
NULL,
FALSE
);

16
db/query/auth.sql Normal file
View File

@ -0,0 +1,16 @@
-- name: GetUserByEmailPhone :one
SELECT * FROM users
WHERE email = $1 OR phone_number = $2;
-- name: CreateRefreshToken :exec
INSERT INTO refresh_tokens (user_id, token, expires_at, created_at, revoked)
VALUES ($1, $2, $3, $4, $5);
-- name: GetRefreshToken :one
SELECT * FROM refresh_tokens
WHERE token = $1;
-- name: RevokeRefreshToken :exec
UPDATE refresh_tokens
SET revoked = TRUE
WHERE token = $1;

16
db/query/bet.sql Normal file
View File

@ -0,0 +1,16 @@
-- name: CreateBet :one
INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id, is_shop_bet)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *;
-- name: GetAllBets :many
SELECT * FROM bets;
-- name: GetBetByID :one
SELECT * FROM bets WHERE id = $1;
-- name: UpdateCashOut :exec
UPDATE bets SET cashed_out = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1;
-- name: DeleteBet :exec
DELETE FROM bets WHERE id = $1;

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, $5, $6);
-- 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 = $2
WHERE id = $1;

16
db/query/ticket.sql Normal file
View File

@ -0,0 +1,16 @@
-- name: CreateTicket :one
INSERT INTO tickets (amount, total_odds)
VALUES ($1, $2)
RETURNING *;
-- name: GetAllTickets :many
SELECT * FROM tickets;
-- name: GetTicketByID :one
SELECT * FROM tickets WHERE id = $1;
-- name: DeleteTicket :exec
DELETE FROM tickets WHERE id = $1;
-- name: DeleteOldTickets :exec
Delete from tickets where created_at < now() - interval '1 day';

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, $9, $10)
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 = $6
WHERE id = $7;
-- 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 = $4
WHERE (email = $2 OR phone_number = $3);

1226
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

1200
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

801
docs/swagger.yaml Normal file
View File

@ -0,0 +1,801 @@
definitions:
domain.BetStatus:
enum:
- 0
- 1
- 2
- 3
type: integer
x-enum-varnames:
- BET_STATUS_PENDING
- BET_STATUS_WIN
- BET_STATUS_LOSS
- BET_STATUS_ERROR
domain.Outcome:
type: object
domain.Role:
enum:
- admin
- customer
- super_admin
- branch_manager
- cashier
type: string
x-enum-varnames:
- RoleAdmin
- RoleCustomer
- RoleSuperAdmin
- RoleBranchManager
- RoleCashier
handlers.BetRes:
properties:
amount:
example: 100
type: number
branch_id:
example: 2
type: integer
full_name:
example: John
type: string
id:
example: 1
type: integer
is_shop_bet:
example: false
type: boolean
outcomes:
items:
$ref: '#/definitions/domain.Outcome'
type: array
phone_number:
example: "1234567890"
type: string
status:
allOf:
- $ref: '#/definitions/domain.BetStatus'
example: 1
total_odds:
example: 4.22
type: number
user_id:
example: 2
type: integer
type: object
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.CreateBetReq:
properties:
amount:
example: 100
type: number
full_name:
example: John
type: string
is_shop_bet:
example: false
type: boolean
outcomes:
items:
type: integer
type: array
phone_number:
example: "1234567890"
type: string
status:
allOf:
- $ref: '#/definitions/domain.BetStatus'
example: 1
total_odds:
example: 4.22
type: number
type: object
handlers.CreateTicketReq:
properties:
amount:
example: 100
type: number
outcomes:
items:
type: integer
type: array
total_odds:
example: 4.22
type: number
type: object
handlers.CreateTicketRes:
properties:
fast_code:
example: 1234
type: integer
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.TicketRes:
properties:
amount:
example: 100
type: number
id:
example: 1
type: integer
outcomes:
items:
$ref: '#/definitions/domain.Outcome'
type: array
total_odds:
example: 4.22
type: number
type: object
handlers.UpdateCashOutReq:
properties:
cashedOut:
type: boolean
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
type: string
password:
example: password123
type: string
phone_number:
example: "1234567890"
type: string
type: object
handlers.loginCustomerRes:
properties:
access_token:
type: string
refresh_token:
type: string
type: object
handlers.logoutReq:
properties:
refresh_token:
type: string
type: object
handlers.refreshToken:
properties:
access_token:
type: string
refresh_token:
type: string
type: object
response.APIResponse:
properties:
data: {}
message:
type: string
metadata: {}
status:
$ref: '#/definitions/response.Status'
timestamp:
type: string
type: object
response.Status:
enum:
- error
- success
type: string
x-enum-varnames:
- Error
- Success
info:
contact:
email: support@swagger.io
name: API Support
url: http://www.swagger.io/support
description: This is server for FortuneBet.
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
termsOfService: http://swagger.io/terms/
title: FortuneBet API
version: "1.0"
paths:
/auth/login:
post:
consumes:
- application/json
description: Login customer
parameters:
- description: Login customer
in: body
name: login
required: true
schema:
$ref: '#/definitions/handlers.loginCustomerReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.loginCustomerRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Login customer
tags:
- auth
/auth/logout:
post:
consumes:
- application/json
description: Logout customer
parameters:
- description: Logout customer
in: body
name: logout
required: true
schema:
$ref: '#/definitions/handlers.logoutReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Logout customer
tags:
- auth
/auth/refresh:
post:
consumes:
- application/json
description: Refresh token
parameters:
- description: tokens
in: body
name: refresh
required: true
schema:
$ref: '#/definitions/handlers.refreshToken'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.loginCustomerRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Refresh token
tags:
- auth
/bet:
get:
consumes:
- application/json
description: Gets all the bets
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/handlers.BetRes'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Gets all bets
tags:
- bet
post:
consumes:
- application/json
description: Creates a bet
parameters:
- description: Creates bet
in: body
name: createBet
required: true
schema:
$ref: '#/definitions/handlers.CreateBetReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.BetRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Create a bet
tags:
- bet
/bet/{id}:
delete:
consumes:
- application/json
description: Deletes bet by id
parameters:
- description: Bet ID
in: path
name: id
required: true
type: integer
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: Deletes bet by id
tags:
- bet
get:
consumes:
- application/json
description: Gets a single bet by id
parameters:
- description: Bet ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.BetRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Gets bet by id
tags:
- bet
patch:
consumes:
- application/json
description: Updates the cashed out field
parameters:
- description: Bet ID
in: path
name: id
required: true
type: integer
- description: Updates Cashed Out
in: body
name: updateCashOut
required: true
schema:
$ref: '#/definitions/handlers.UpdateCashOutReq'
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: Updates the cashed out field
tags:
- bet
/ticket:
get:
consumes:
- application/json
description: Retrieve all tickets
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/handlers.TicketRes'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Get all tickets
tags:
- ticket
post:
consumes:
- application/json
description: Creates a temporary ticket
parameters:
- description: Creates ticket
in: body
name: createTicket
required: true
schema:
$ref: '#/definitions/handlers.CreateTicketReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.CreateTicketRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Create a temporary ticket
tags:
- ticket
/ticket/{id}:
get:
consumes:
- application/json
description: Retrieve ticket details by ticket ID
parameters:
- description: Ticket ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.TicketRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Get ticket by ID
tags:
- ticket
/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
name: Authorization
type: apiKey
swagger: "2.0"

97
gen/db/auth.sql.go Normal file
View File

@ -0,0 +1,97 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// source: auth.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CreateRefreshToken = `-- name: CreateRefreshToken :exec
INSERT INTO refresh_tokens (user_id, token, expires_at, created_at, revoked)
VALUES ($1, $2, $3, $4, $5)
`
type CreateRefreshTokenParams struct {
UserID int64
Token string
ExpiresAt pgtype.Timestamptz
CreatedAt pgtype.Timestamptz
Revoked bool
}
func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) error {
_, err := q.db.Exec(ctx, CreateRefreshToken,
arg.UserID,
arg.Token,
arg.ExpiresAt,
arg.CreatedAt,
arg.Revoked,
)
return err
}
const GetRefreshToken = `-- name: GetRefreshToken :one
SELECT id, user_id, token, expires_at, created_at, revoked FROM refresh_tokens
WHERE token = $1
`
func (q *Queries) GetRefreshToken(ctx context.Context, token string) (RefreshToken, error) {
row := q.db.QueryRow(ctx, GetRefreshToken, token)
var i RefreshToken
err := row.Scan(
&i.ID,
&i.UserID,
&i.Token,
&i.ExpiresAt,
&i.CreatedAt,
&i.Revoked,
)
return i, err
}
const GetUserByEmailPhone = `-- name: GetUserByEmailPhone :one
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 pgtype.Text
PhoneNumber pgtype.Text
}
func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPhoneParams) (User, error) {
row := q.db.QueryRow(ctx, GetUserByEmailPhone, arg.Email, arg.PhoneNumber)
var i User
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.Password,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.SuspendedAt,
&i.Suspended,
)
return i, err
}
const RevokeRefreshToken = `-- name: RevokeRefreshToken :exec
UPDATE refresh_tokens
SET revoked = TRUE
WHERE token = $1
`
func (q *Queries) RevokeRefreshToken(ctx context.Context, token string) error {
_, err := q.db.Exec(ctx, RevokeRefreshToken, token)
return err
}

142
gen/db/bet.sql.go Normal file
View File

@ -0,0 +1,142 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// source: bet.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CreateBet = `-- name: CreateBet :one
INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id, is_shop_bet)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at, is_shop_bet
`
type CreateBetParams struct {
Amount int64
TotalOdds float32
Status int32
FullName string
PhoneNumber string
BranchID pgtype.Int8
UserID pgtype.Int8
IsShopBet bool
}
func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, error) {
row := q.db.QueryRow(ctx, CreateBet,
arg.Amount,
arg.TotalOdds,
arg.Status,
arg.FullName,
arg.PhoneNumber,
arg.BranchID,
arg.UserID,
arg.IsShopBet,
)
var i Bet
err := row.Scan(
&i.ID,
&i.Amount,
&i.TotalOdds,
&i.Status,
&i.FullName,
&i.PhoneNumber,
&i.BranchID,
&i.UserID,
&i.CashedOut,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
)
return i, err
}
const DeleteBet = `-- name: DeleteBet :exec
DELETE FROM bets WHERE id = $1
`
func (q *Queries) DeleteBet(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteBet, id)
return err
}
const GetAllBets = `-- name: GetAllBets :many
SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at, is_shop_bet FROM bets
`
func (q *Queries) GetAllBets(ctx context.Context) ([]Bet, error) {
rows, err := q.db.Query(ctx, GetAllBets)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Bet
for rows.Next() {
var i Bet
if err := rows.Scan(
&i.ID,
&i.Amount,
&i.TotalOdds,
&i.Status,
&i.FullName,
&i.PhoneNumber,
&i.BranchID,
&i.UserID,
&i.CashedOut,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetBetByID = `-- name: GetBetByID :one
SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at, is_shop_bet FROM bets WHERE id = $1
`
func (q *Queries) GetBetByID(ctx context.Context, id int64) (Bet, error) {
row := q.db.QueryRow(ctx, GetBetByID, id)
var i Bet
err := row.Scan(
&i.ID,
&i.Amount,
&i.TotalOdds,
&i.Status,
&i.FullName,
&i.PhoneNumber,
&i.BranchID,
&i.UserID,
&i.CashedOut,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
)
return i, err
}
const UpdateCashOut = `-- name: UpdateCashOut :exec
UPDATE bets SET cashed_out = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1
`
type UpdateCashOutParams struct {
ID int64
CashedOut pgtype.Bool
}
func (q *Queries) UpdateCashOut(ctx context.Context, arg UpdateCashOutParams) error {
_, err := q.db.Exec(ctx, UpdateCashOut, arg.ID, arg.CashedOut)
return err
}

View File

@ -8,6 +8,21 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
type Bet struct {
ID int64
Amount int64
TotalOdds float32
Status int32
FullName string
PhoneNumber string
BranchID pgtype.Int8
UserID pgtype.Int8
CashedOut pgtype.Bool
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
IsShopBet bool
}
type Notification struct { type Notification struct {
ID string ID string
RecipientID string RecipientID string
@ -25,15 +40,47 @@ type Notification struct {
Metadata []byte Metadata []byte
} }
type User struct { type Otp struct {
ID int64 ID int64
FirstName string SentTo string
LastName string Medium string
Email string OtpFor string
PhoneNumber string Otp string
Password string Used bool
Role string UsedAt pgtype.Timestamptz
Verified pgtype.Bool CreatedAt pgtype.Timestamptz
CreatedAt pgtype.Timestamp ExpiresAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamp }
type RefreshToken struct {
ID int64
UserID int64
Token string
ExpiresAt pgtype.Timestamptz
CreatedAt pgtype.Timestamptz
Revoked bool
}
type Ticket struct {
ID int64
Amount pgtype.Int8
TotalOdds float32
CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp
}
type User struct {
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
} }

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

@ -0,0 +1,84 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.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, $5, $6)
`
type CreateOtpParams struct {
SentTo string
Medium string
OtpFor string
Otp string
CreatedAt pgtype.Timestamptz
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.CreatedAt,
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 = $2
WHERE id = $1
`
type MarkOtpAsUsedParams struct {
ID int64
UsedAt pgtype.Timestamptz
}
func (q *Queries) MarkOtpAsUsed(ctx context.Context, arg MarkOtpAsUsedParams) error {
_, err := q.db.Exec(ctx, MarkOtpAsUsed, arg.ID, arg.UsedAt)
return err
}

101
gen/db/ticket.sql.go Normal file
View File

@ -0,0 +1,101 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// source: ticket.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CreateTicket = `-- name: CreateTicket :one
INSERT INTO tickets (amount, total_odds)
VALUES ($1, $2)
RETURNING id, amount, total_odds, created_at, updated_at
`
type CreateTicketParams struct {
Amount pgtype.Int8
TotalOdds float32
}
func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Ticket, error) {
row := q.db.QueryRow(ctx, CreateTicket, arg.Amount, arg.TotalOdds)
var i Ticket
err := row.Scan(
&i.ID,
&i.Amount,
&i.TotalOdds,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const DeleteOldTickets = `-- name: DeleteOldTickets :exec
Delete from tickets where created_at < now() - interval '1 day'
`
func (q *Queries) DeleteOldTickets(ctx context.Context) error {
_, err := q.db.Exec(ctx, DeleteOldTickets)
return err
}
const DeleteTicket = `-- name: DeleteTicket :exec
DELETE FROM tickets WHERE id = $1
`
func (q *Queries) DeleteTicket(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteTicket, id)
return err
}
const GetAllTickets = `-- name: GetAllTickets :many
SELECT id, amount, total_odds, created_at, updated_at FROM tickets
`
func (q *Queries) GetAllTickets(ctx context.Context) ([]Ticket, error) {
rows, err := q.db.Query(ctx, GetAllTickets)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Ticket
for rows.Next() {
var i Ticket
if err := rows.Scan(
&i.ID,
&i.Amount,
&i.TotalOdds,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetTicketByID = `-- name: GetTicketByID :one
SELECT id, amount, total_odds, created_at, updated_at FROM tickets WHERE id = $1
`
func (q *Queries) GetTicketByID(ctx context.Context, id int64) (Ticket, error) {
row := q.db.QueryRow(ctx, GetTicketByID, id)
var i Ticket
err := row.Scan(
&i.ID,
&i.Amount,
&i.TotalOdds,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

View File

@ -11,42 +11,85 @@ 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, $9, $10)
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 string Role string
Role string Password []byte
Verified pgtype.Bool EmailVerified bool
PhoneVerified bool
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
} }
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,
arg.CreatedAt,
arg.UpdatedAt,
) )
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 +97,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 +107,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 +155,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 +207,103 @@ 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 = $4
WHERE (email = $2 OR phone_number = $3)
`
type UpdatePasswordParams struct {
Password []byte
Email pgtype.Text
PhoneNumber pgtype.Text
UpdatedAt pgtype.Timestamptz
}
func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error {
_, err := q.db.Exec(ctx, UpdatePassword,
arg.Password,
arg.Email,
arg.PhoneNumber,
arg.UpdatedAt,
)
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 = $6
WHERE id = $7
` `
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 string
Role string Role string
Verified pgtype.Bool UpdatedAt pgtype.Timestamptz
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.UpdatedAt,
arg.ID,
) )
return err return err
} }

40
go.mod
View File

@ -4,36 +4,54 @@ go 1.24.1
require ( require (
github.com/bytedance/sonic v1.13.2 github.com/bytedance/sonic v1.13.2
github.com/go-playground/validator/v10 v10.26.0
github.com/gofiber/fiber/v2 v2.52.6 github.com/gofiber/fiber/v2 v2.52.6
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/gofiber/websocket/v2 v2.2.1 github.com/gofiber/websocket/v2 v2.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.4 github.com/jackc/pgx/v5 v5.7.4
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/swaggo/fiber-swagger v1.3.0
github.com/swaggo/swag v1.16.4
golang.org/x/crypto v0.36.0
) )
require ( require (
github.com/andybalholm/brotli v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/fasthttp/websocket v1.5.3 // indirect github.com/fasthttp/websocket v1.5.3 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/fasthttp v1.59.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.32.0 // indirect golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

172
go.sum
View File

@ -1,5 +1,12 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
@ -8,17 +15,48 @@ github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFos
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY=
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek= github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs= github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w= github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU= github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@ -29,56 +67,146 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/fiber-swagger v1.3.0 h1:RMjIVDleQodNVdKuu7GRs25Eq8RVXK7MwY9f5jbobNg=
github.com/swaggo/fiber-swagger v1.3.0/go.mod h1:18MuDqBkYEiUmeM/cAAB8CI28Bi62d/mys39j1QqF9w=
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc=
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.35.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/fasthttp v1.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@ -2,18 +2,33 @@ package config
import ( import (
"errors" "errors"
"log/slog"
"os" "os"
"strconv" "strconv"
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
"github.com/joho/godotenv"
) )
var ( var (
ErrInvalidDbUrl = errors.New("db url is invalid") ErrInvalidDbUrl = errors.New("db url is invalid")
ErrInvalidPort = errors.New("port number is invalid") ErrInvalidPort = errors.New("port number is invalid")
ErrRefreshExpiry = errors.New("refresh token expiry is invalid")
ErrAccessExpiry = errors.New("access token expiry is invalid")
ErrInvalidJwtKey = errors.New("jwt key is invalid")
ErrLogLevel = errors.New("log level not set")
ErrInvalidLevel = errors.New("invalid log level")
ErrInvalidEnv = errors.New("env not set or invalid")
) )
type Config struct { type Config struct {
Port int Port int
DbUrl string DbUrl string
RefreshExpiry int
AccessExpiry int
JwtKey string
LogLevel slog.Level
Env string
} }
func NewConfig() (*Config, error) { func NewConfig() (*Config, error) {
@ -24,7 +39,16 @@ func NewConfig() (*Config, error) {
return config, nil return config, nil
} }
func (c *Config) loadEnv() error { func (c *Config) loadEnv() error {
err := godotenv.Load()
if err != nil {
return errors.New("failed to load env file")
}
// env
env := os.Getenv("ENV")
if env == "" {
return ErrInvalidEnv
}
c.Env = env
portStr := os.Getenv("PORT") portStr := os.Getenv("PORT")
port, err := strconv.Atoi(portStr) port, err := strconv.Atoi(portStr)
if err != nil { if err != nil {
@ -37,6 +61,33 @@ func (c *Config) loadEnv() error {
return ErrInvalidDbUrl return ErrInvalidDbUrl
} }
c.DbUrl = dbUrl c.DbUrl = dbUrl
refreshExpiryStr := os.Getenv("REFRESH_EXPIRY")
refreshExpiry, err := strconv.Atoi(refreshExpiryStr)
if err != nil {
return ErrRefreshExpiry
}
c.RefreshExpiry = refreshExpiry
jwtKey := os.Getenv("JWT_KEY")
if jwtKey == "" {
return ErrInvalidJwtKey
}
c.JwtKey = jwtKey
accessExpiryStr := os.Getenv("ACCESS_EXPIRY")
accessExpiry, err := strconv.Atoi(accessExpiryStr)
if err != nil {
return ErrAccessExpiry
}
c.AccessExpiry = accessExpiry
// log level
logLevel := os.Getenv("LOG_LEVEL")
if logLevel == "" {
return ErrLogLevel
}
lvl, ok := customlogger.LogLevels[logLevel]
if !ok {
return ErrInvalidLevel
}
c.LogLevel = lvl
return nil return nil
} }

View File

@ -1 +1,12 @@
package domain package domain
import "time"
type RefreshToken struct {
ID int64
UserID int64
Token string
ExpiresAt time.Time
CreatedAt time.Time
Revoked bool
}

44
internal/domain/bet.go Normal file
View File

@ -0,0 +1,44 @@
package domain
type BetStatus int
const (
BET_STATUS_PENDING BetStatus = iota
BET_STATUS_WIN
BET_STATUS_LOSS
BET_STATUS_ERROR
)
// If it is a ShopBet then UserID will be the cashier
// If it is a DigitalBet then UserID will be the user and the branchID will be 0 or nil
type Bet struct {
ID int64
Outcomes []Outcome
Amount Currency
TotalOdds float32
Status BetStatus
FullName string
PhoneNumber string
BranchID ValidInt64 // Can Be Nullable
UserID ValidInt64 // Can Be Nullable
IsShopBet bool
CashedOut bool
}
type CreateBet struct {
Outcomes []int64
Amount Currency
TotalOdds float32
Status BetStatus
FullName string
PhoneNumber string
BranchID ValidInt64 // Can Be Nullable
UserID ValidInt64 // Can Be Nullable
IsShopBet bool
}
func (b BetStatus) String() string {
return []string{"Pending", "Win", "Loss", "Error"}[b]
}
// func isBetStatusValid()

13
internal/domain/branch.go Normal file
View File

@ -0,0 +1,13 @@
package domain
type Branch struct {
ID int64
Name string
Location string
BranchManagerID int64
IsSelfOwned bool
IsSupportingSportBook bool
IsSupportingVirtual bool
IsSupportingGameZone bool
}

39
internal/domain/common.go Normal file
View File

@ -0,0 +1,39 @@
package domain
import "fmt"
type ValidInt64 struct {
Value int64
Valid bool
}
type ValidString struct {
Value string
Valid bool
}
type ValidBool struct {
Value bool
Valid bool
}
type Currency int64
// ToCurrency converts a float32 to Currency
func ToCurrency(f float32) Currency {
return Currency((f * 100) + 0.5)
}
// Float64 converts a Currency to float32
func (m Currency) Float64() float32 {
x := float32(m)
x = x / 100
return x
}
// String returns a formatted Currency value
func (m Currency) String() string {
x := float32(m)
x = x / 100
return fmt.Sprintf("$%.2f", x)
}

6
internal/domain/event.go Normal file
View File

@ -0,0 +1,6 @@
package domain
type Event struct {}
type Outcome struct {}

39
internal/domain/otp.go Normal file
View File

@ -0,0 +1,39 @@
package domain
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
const (
OtpReset OtpFor = "reset"
OtpRegister OtpFor = "register"
)
type OtpMedium string
const (
OtpMediumEmail OtpMedium = "email"
OtpMediumSms OtpMedium = "sms"
)
type Otp struct {
ID int64
SentTo string
Medium OtpMedium
For OtpFor
Otp string
Used bool
UsedAt time.Time
CreatedAt time.Time
ExpiresAt time.Time
}

11
internal/domain/role.go Normal file
View File

@ -0,0 +1,11 @@
package domain
type Role string
const (
RoleSuperAdmin Role = "super_admin"
RoleAdmin Role = "admin"
RoleBranchManager Role = "branch_manager"
RoleCustomer Role = "customer"
RoleCashier Role = "cashier"
)

15
internal/domain/ticket.go Normal file
View File

@ -0,0 +1,15 @@
package domain
// ID will serve as the fast code since this doesn't need to be secure
type Ticket struct {
ID int64
Outcomes []Outcome
Amount Currency
TotalOdds float32
}
type CreateTicket struct {
Outcomes []int64
Amount Currency
TotalOdds float32
}

View File

@ -1,12 +1,53 @@
package domain package domain
import (
"errors"
"time"
)
var (
ErrUserNotFound = errors.New("user not found")
)
type User struct { type User struct {
ID int64 ID int64
FirstName string FirstName string
LastName string LastName string
Email string Email string
PhoneNumber string PhoneNumber string
Password string Password []byte
Role string Role Role
Verified bool //
EmailVerified bool
PhoneVerified bool
//
CreatedAt time.Time
UpdatedAt time.Time
//
SuspendedAt time.Time
Suspended bool
}
type RegisterUserReq struct {
FirstName string
LastName string
Email string
PhoneNumber string
Password string
//Role string
Otp string
ReferalCode string
//
OtpMedium OtpMedium
}
type ResetPasswordReq struct {
Email string
PhoneNumber string
Password string
Otp string
OtpMedium OtpMedium
}
type UpdateUserReq struct {
FirstName ValidString
LastName ValidString
Suspended ValidBool
} }

View File

@ -11,8 +11,12 @@ var LogLevels = map[string]slog.Level{
"warn": slog.LevelWarn, "warn": slog.LevelWarn,
"error": slog.LevelError, "error": slog.LevelError,
} }
var Environment = map[string]string{
"dev": "development",
"prod": "production",
}
func NewLogger(env string, lvl slog.Level, version string) *slog.Logger { func NewLogger(env string, lvl slog.Level) *slog.Logger {
var logHandler slog.Handler var logHandler slog.Handler
switch env { switch env {
case "development": case "development":
@ -28,7 +32,6 @@ func NewLogger(env string, lvl slog.Level, version string) *slog.Logger {
logger := slog.New(logHandler).With(slog.Group( logger := slog.New(logHandler).With(slog.Group(
"service_info", "service_info",
slog.String("env", env), slog.String("env", env),
slog.String("version", version),
), ),
) )

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

@ -0,0 +1,77 @@
package repository
import (
"context"
"database/sql"
"errors"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/jackc/pgx/v5/pgtype"
)
func (s *Store) GetUserByEmailPhone(ctx context.Context, email, phone string) (domain.User, error) {
user, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{
Email: pgtype.Text{
String: email,
Valid: true,
},
PhoneNumber: pgtype.Text{
String: phone,
Valid: true,
},
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.User{}, authentication.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,
Password: user.Password,
Role: domain.Role(user.Role),
}, nil
}
func (s *Store) CreateRefreshToken(ctx context.Context, rt domain.RefreshToken) error {
return s.queries.CreateRefreshToken(ctx, dbgen.CreateRefreshTokenParams{
UserID: rt.UserID,
Token: rt.Token,
CreatedAt: pgtype.Timestamptz{
Time: rt.CreatedAt,
Valid: true,
},
ExpiresAt: pgtype.Timestamptz{
Time: rt.ExpiresAt,
Valid: true,
},
Revoked: rt.Revoked,
})
}
func (s *Store) GetRefreshToken(ctx context.Context, token string) (domain.RefreshToken, error) {
rf, err := s.queries.GetRefreshToken(ctx, token)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return domain.RefreshToken{}, authentication.ErrRefreshTokenNotFound
}
return domain.RefreshToken{}, err
}
return domain.RefreshToken{
Token: rf.Token,
UserID: rf.UserID,
CreatedAt: rf.CreatedAt.Time,
ExpiresAt: rf.ExpiresAt.Time,
Revoked: rf.Revoked,
}, nil
}
func (s *Store) RevokeRefreshToken(ctx context.Context, token string) error {
return s.queries.RevokeRefreshToken(ctx, token)
}

View File

@ -0,0 +1,96 @@
package repository
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
func convertDBBet(bet dbgen.Bet) domain.Bet {
return domain.Bet{
ID: bet.ID,
Amount: domain.Currency(bet.Amount),
TotalOdds: bet.TotalOdds,
Status: domain.BetStatus(bet.Status),
FullName: bet.FullName,
PhoneNumber: bet.PhoneNumber,
BranchID: domain.ValidInt64{
Value: bet.BranchID.Int64,
Valid: bet.BranchID.Valid,
},
UserID: domain.ValidInt64{
Value: bet.UserID.Int64,
Valid: bet.UserID.Valid,
},
IsShopBet: bet.IsShopBet,
}
}
func convertCreateBet(bet domain.CreateBet) dbgen.CreateBetParams {
return dbgen.CreateBetParams{
Amount: int64(bet.Amount),
TotalOdds: bet.TotalOdds,
Status: int32(bet.Status),
FullName: bet.FullName,
PhoneNumber: bet.PhoneNumber,
BranchID: pgtype.Int8{
Int64: bet.BranchID.Value,
Valid: bet.BranchID.Valid,
},
UserID: pgtype.Int8{
Int64: bet.UserID.Value,
Valid: bet.UserID.Valid,
},
IsShopBet: bet.IsShopBet,
}
}
func (s *Store) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) {
newBet, err := s.queries.CreateBet(ctx, convertCreateBet(bet))
if err != nil {
return domain.Bet{}, err
}
return convertDBBet(newBet), err
}
func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) {
bet, err := s.queries.GetBetByID(ctx, id)
if err != nil {
return domain.Bet{}, err
}
return convertDBBet(bet), nil
}
func (s *Store) GetAllBets(ctx context.Context) ([]domain.Bet, error) {
bets, err := s.queries.GetAllBets(ctx)
if err != nil {
return nil, err
}
var result []domain.Bet
for _, bet := range bets {
result = append(result, convertDBBet(bet))
}
return result, nil
}
func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error {
err := s.queries.UpdateCashOut(ctx, dbgen.UpdateCashOutParams{
ID: id,
CashedOut: pgtype.Bool{
Bool: cashedOut,
},
})
return err
}
func (s *Store) DeleteBet(ctx context.Context, id int64) error {
return s.queries.DeleteBet(ctx, id)
}

View File

@ -0,0 +1,60 @@
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,
},
CreatedAt: pgtype.Timestamptz{
Time: otp.CreatedAt,
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, dbgen.MarkOtpAsUsedParams{
ID: otp.ID,
UsedAt: pgtype.Timestamptz{
Time: otp.UsedAt,
Valid: true,
},
})
}

View File

@ -0,0 +1,68 @@
package repository
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
func convertDBTicket(ticket dbgen.Ticket) domain.Ticket {
return domain.Ticket{
ID: ticket.ID,
Amount: domain.Currency(ticket.Amount.Int64),
TotalOdds: ticket.TotalOdds,
}
}
func convertCreateTicket(ticket domain.CreateTicket) dbgen.CreateTicketParams {
return dbgen.CreateTicketParams{
Amount: pgtype.Int8{
Int64: int64(ticket.Amount),
},
TotalOdds: ticket.TotalOdds,
}
}
func (s *Store) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) {
newTicket, err := s.queries.CreateTicket(ctx, convertCreateTicket(ticket))
if err != nil {
return domain.Ticket{}, err
}
return convertDBTicket(newTicket), err
}
func (s *Store) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error) {
ticket, err := s.queries.GetTicketByID(ctx, id)
if err != nil {
return domain.Ticket{}, err
}
return convertDBTicket(ticket), nil
}
func (s *Store) GetAllTickets(ctx context.Context) ([]domain.Ticket, error) {
tickets, err := s.queries.GetAllTickets(ctx)
if err != nil {
return nil, err
}
var result []domain.Ticket
for _, ticket := range tickets {
result = append(result, convertDBTicket(ticket))
}
return result, nil
}
func (s *Store) DeleteOldTickets(ctx context.Context) error {
return s.queries.DeleteOldTickets(ctx)
}
func (s *Store) DeleteTicket(ctx context.Context, id int64) error {
return s.queries.DeleteTicket(ctx, id)
}

View File

@ -2,47 +2,84 @@ package repository
import ( import (
"context" "context"
"database/sql"
"errors"
"time"
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, dbgen.MarkOtpAsUsedParams{
FirstName: firstName, ID: usedOtpId,
LastName: lastName, UsedAt: pgtype.Timestamptz{
Email: email, Time: time.Now(),
PhoneNumber: phoneNumber, Valid: true,
Password: password, },
Role: role, })
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,
CreatedAt: pgtype.Timestamptz{
Time: time.Now(),
Valid: true,
},
UpdatedAt: pgtype.Timestamptz{
Time: time.Now(),
Valid: true,
},
}) })
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,
Password: user.Password, Role: domain.Role(user.Role),
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 }, nil
} }
func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) { func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) {
@ -50,32 +87,125 @@ 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,
}) })
return err if err != nil {
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) {
row, err := s.queries.CheckPhoneEmailExist(ctx, dbgen.CheckPhoneEmailExistParams{
PhoneNumber: pgtype.Text{
String: phoneNum,
Valid: phoneNum != "",
},
Email: pgtype.Text{
String: email,
Valid: email != "",
},
})
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, dbgen.MarkOtpAsUsedParams{
ID: usedOtpId,
UsedAt: pgtype.Timestamptz{
Time: time.Now(),
Valid: true,
},
})
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,122 @@
package authentication
import (
"context"
"crypto/rand"
"encoding/base32"
"errors"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"golang.org/x/crypto/bcrypt"
)
var (
ErrInvalidPassword = errors.New("incorrect password")
ErrUserNotFound = errors.New("user not found")
ErrExpiredToken = errors.New("token expired")
ErrRefreshTokenNotFound = errors.New("refresh token not found") // i.e login again
)
type LoginSuccess struct {
UserId int64
Role domain.Role
RfToken string
}
func (s *Service) Login(ctx context.Context, email, phone string, password string) (LoginSuccess, error) {
user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone)
if err != nil {
return LoginSuccess{}, err
}
err = matchPassword(password, user.Password)
if err != nil {
return LoginSuccess{}, err
}
refreshToken, err := generateRefreshToken()
if err != nil {
return LoginSuccess{}, err
}
err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
Token: refreshToken,
UserID: user.ID,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
})
if err != nil {
return LoginSuccess{}, err
}
return LoginSuccess{
UserId: user.ID,
Role: user.Role,
RfToken: refreshToken,
}, nil
}
func (s *Service) RefreshToken(ctx context.Context, refToken string) (string, error) {
token, err := s.tokenStore.GetRefreshToken(ctx, refToken)
if err != nil {
return "", err
}
if token.Revoked {
return "", ErrRefreshTokenNotFound
}
if token.ExpiresAt.Before(time.Now()) {
return "", ErrExpiredToken
}
newRefToken, err := generateRefreshToken()
if err != nil {
return "", err
}
err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
Token: newRefToken,
UserID: token.UserID,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
})
if err != nil {
return "", err
}
return newRefToken, nil
}
func (s *Service) Logout(ctx context.Context, refToken string) error {
token, err := s.tokenStore.GetRefreshToken(ctx, refToken)
if err != nil {
return err
}
if token.Revoked {
return ErrRefreshTokenNotFound
}
if token.ExpiresAt.Before(time.Now()) {
return ErrExpiredToken
}
return s.tokenStore.RevokeRefreshToken(ctx, refToken)
}
func matchPassword(plaintextPassword string, hash []byte) error {
err := bcrypt.CompareHashAndPassword(hash, []byte(plaintextPassword))
if err != nil {
switch {
case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword):
return ErrInvalidPassword
default:
return err
}
}
return err
}
func generateRefreshToken() (string, error) {
randomBytes := make([]byte, 32)
_, err := rand.Read(randomBytes)
if err != nil {
return "", err
}
plaintext := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randomBytes)
return plaintext, nil
}

View File

@ -0,0 +1,16 @@
package authentication
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type UserStore interface {
GetUserByEmailPhone(ctx context.Context, email, phone string) (domain.User, error)
}
type TokenStore interface {
CreateRefreshToken(ctx context.Context, rt domain.RefreshToken) error
GetRefreshToken(ctx context.Context, token string) (domain.RefreshToken, error)
RevokeRefreshToken(ctx context.Context, token string) error
}

View File

@ -0,0 +1,28 @@
package authentication
// type EmailPhone struct {
// Email ValidString
// PhoneNumber ValidString
// Password ValidString
// }
type ValidString struct {
Value string
Valid bool
}
type Tokens struct {
AccessToken string
RefreshToken string
}
type Service struct {
userStore UserStore
tokenStore TokenStore
RefreshExpiry int
}
func NewService(userStore UserStore, tokenStore TokenStore, RefreshExpiry int) *Service {
return &Service{
userStore: userStore,
tokenStore: tokenStore,
RefreshExpiry: RefreshExpiry,
}
}

View File

@ -0,0 +1,15 @@
package bet
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type BetStore interface {
CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error)
GetBetByID(ctx context.Context, id int64) (domain.Bet, error)
GetAllBets(ctx context.Context) ([]domain.Bet, error)
UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error
DeleteBet(ctx context.Context, id int64) error
}

View File

@ -0,0 +1,35 @@
package bet
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type Service struct {
betStore BetStore
}
func NewService(betStore BetStore) *Service {
return &Service{
betStore: betStore,
}
}
func (s *Service) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) {
return s.betStore.CreateBet(ctx, bet)
}
func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) {
return s.betStore.GetBetByID(ctx, id)
}
func (s *Service) GetAllBets(ctx context.Context) ([]domain.Bet, error) {
return s.betStore.GetAllBets(ctx)
}
func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error {
return s.betStore.UpdateCashOut(ctx, id, cashedOut)
}
func (s *Service) DeleteBet(ctx context.Context, id int64) error {
return s.betStore.DeleteBet(ctx, id)
}

View File

@ -0,0 +1 @@
package sportsbook

View File

@ -0,0 +1 @@
package sportsbook

View File

@ -0,0 +1 @@
package sportsbook

View File

@ -0,0 +1,15 @@
package ticket
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type TicketStore interface {
CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error)
GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error)
GetAllTickets(ctx context.Context) ([]domain.Ticket, error)
DeleteOldTickets(ctx context.Context) error
DeleteTicket(ctx context.Context, id int64) error
}

View File

@ -0,0 +1,30 @@
package ticket
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type Service struct {
ticketStore TicketStore
}
func NewService(ticketStore TicketStore) *Service {
return &Service{
ticketStore: ticketStore,
}
}
func (s *Service) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) {
return s.ticketStore.CreateTicket(ctx, ticket)
}
func (s *Service) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error) {
return s.ticketStore.GetTicketByID(ctx, id)
}
func (s *Service) GetAllTickets(ctx context.Context) ([]domain.Ticket, error) {
return s.ticketStore.GetAllTickets(ctx)
}
func (s *Service) DeleteTicket(ctx context.Context, id int64) error {
return s.ticketStore.DeleteTicket(ctx, id)
}

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

@ -7,9 +7,24 @@ import (
) )
type UserStore interface { type UserStore interface {
CreateUser(ctx context.Context, CfirstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error) CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error)
GetUserByID(ctx context.Context, id int64) (domain.User, error) GetUserByID(ctx context.Context, id int64) (domain.User, error)
GetAllUsers(ctx context.Context) ([]domain.User, error) GetAllUsers(ctx context.Context) ([]domain.User, error)
UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error UpdateUser(ctx context.Context, user domain.UpdateUserReq) error
DeleteUser(ctx context.Context, id int64) error DeleteUser(ctx context.Context, id int64) error
CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error)
GetUserByEmail(ctx context.Context, email string) (domain.User, error)
GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error)
//
UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone
}
type SmsGateway interface {
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
}
type EmailGateway interface {
SendEmailOTP(ctx context.Context, email string, otp string) error
}
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)
} }

View File

@ -0,0 +1,78 @@
package user
import (
"context"
"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
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,33 +1,29 @@
package user package user
import ( import (
"context" "time"
)
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" const (
OtpExpiry = 5 * time.Minute
) )
type Service struct { type Service struct {
userStore UserStore userStore UserStore
otpStore OtpStore
smsGateway SmsGateway
emailGateway EmailGateway
} }
func NewService(userStore UserStore) *Service { func NewService(
userStore UserStore,
otpStore OtpStore, smsGateway SmsGateway,
emailGateway EmailGateway,
) *Service {
return &Service{ return &Service{
userStore: userStore, userStore: userStore,
otpStore: otpStore,
smsGateway: smsGateway,
emailGateway: emailGateway,
} }
} }
func (s *Service) CreateUser(ctx context.Context, firstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error) {
return s.userStore.CreateUser(ctx, firstName, lastName, email, phoneNumber, password, role, verified)
}
func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
return s.userStore.GetUserByID(ctx, id)
}
func (s *Service) GetAllUsers(ctx context.Context) ([]domain.User, error) {
return s.userStore.GetAllUsers(ctx)
}
func (s *Service) UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error {
return s.userStore.UpdateUser(ctx, id, firstName, lastName, email, phoneNumber, password, role, verified)
}
func (s *Service) DeleteUser(ctx context.Context, id int64) error {
return s.userStore.DeleteUser(ctx, id)
}

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

@ -4,19 +4,41 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"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" "log/slog"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type App struct { type App struct {
fiber *fiber.App fiber *fiber.App
logger *slog.Logger
NotidicationStore notificationservice.NotificationStore NotidicationStore notificationservice.NotificationStore
port int port int
authSvc *authentication.Service
userSvc *user.Service
ticketSvc *ticket.Service
betSvc *bet.Service
validator *customvalidator.CustomValidator
JwtConfig jwtutil.JwtConfig
Logger *slog.Logger Logger *slog.Logger
} }
func NewApp(port int, logger *slog.Logger, notidicationStore notificationservice.NotificationStore) *App { func NewApp(
port int, validator *customvalidator.CustomValidator,
authSvc *authentication.Service,
logger *slog.Logger,
JwtConfig jwtutil.JwtConfig,
userSvc *user.Service,
ticketSvc *ticket.Service,
betSvc *bet.Service,
, notidicationStore notificationservice.NotificationStore) *App {
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
CaseSensitive: true, CaseSensitive: true,
DisableHeaderNormalizing: true, DisableHeaderNormalizing: true,
@ -24,8 +46,15 @@ func NewApp(port int, logger *slog.Logger, notidicationStore notificationservice
JSONDecoder: sonic.Unmarshal, JSONDecoder: sonic.Unmarshal,
}) })
s := &App{ s := &App{
fiber: app, fiber: app,
port: port, port: port,
authSvc: authSvc,
validator: validator,
logger: logger,
JwtConfig: JwtConfig,
userSvc: userSvc,
ticketSvc: ticketSvc,
betSvc: betSvc,
NotidicationStore: notidicationStore, NotidicationStore: notidicationStore,
Logger: logger, Logger: logger,
} }

View File

@ -1,35 +0,0 @@
package httpserver
import (
"context"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/websocket/v2"
)
func (a *App) initAppRoutes() {
// a.fiber.Group("/users", users.CreateAccount(a.userAPI))
a.fiber.Get("/ws/:recipientID", func(c *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(c) {
c.Locals("allowed", true)
return c.Next()
}
return fiber.ErrUpgradeRequired
}, websocket.New(func(c *websocket.Conn) {
recipientID := c.Params("recipientID")
a.NotidicationStore.ConnectWebSocket(context.Background(), recipientID, c)
defer a.NotidicationStore.DisconnectWebSocket(recipientID)
for {
_, _, err := c.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
a.Logger.Error("WebSocket error", "recipientID", recipientID, "error", err)
}
return
}
}
}))
}

View File

@ -0,0 +1,182 @@
package handlers
import (
"errors"
"log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
"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 loginCustomerReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"`
}
type loginCustomerRes struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
// LoginCustomer godoc
// @Summary Login customer
// @Description Login customer
// @Tags auth
// @Accept json
// @Produce json
// @Param login body loginCustomerReq true "Login customer"
// @Success 200 {object} loginCustomerRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /auth/login [post]
func LoginCustomer(
logger *slog.Logger, authSvc *authentication.Service,
validator *customvalidator.CustomValidator, JwtConfig jwtutil.JwtConfig) fiber.Handler {
return func(c *fiber.Ctx) error {
var req loginCustomerReq
if err := c.BodyParser(&req); err != nil {
logger.Error("Login failed", "error", err)
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
}
valErrs, ok := validator.Validate(c, req)
if !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
}
successRes, err := authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password)
if err != nil {
logger.Info("Login failed", "error", err)
if errors.Is(err, authentication.ErrInvalidPassword) {
response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid password or not registered", nil, nil)
return nil
}
if errors.Is(err, authentication.ErrUserNotFound) {
response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid password or not registered", nil, nil)
return nil
}
logger.Error("Login failed", "error", err)
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
return nil
}
accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry)
res := loginCustomerRes{
AccessToken: accessToken,
RefreshToken: successRes.RfToken,
}
return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil)
}
}
type refreshToken struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
// RefreshToken godoc
// @Summary Refresh token
// @Description Refresh token
// @Tags auth
// @Accept json
// @Produce json
// @Param refresh body refreshToken true "tokens"
// @Success 200 {object} loginCustomerRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /auth/refresh [post]
func RefreshToken(logger *slog.Logger, authSvc *authentication.Service,
validator *customvalidator.CustomValidator, JwtConfig jwtutil.JwtConfig) fiber.Handler {
return func(c *fiber.Ctx) error {
var req refreshToken
if err := c.BodyParser(&req); err != nil {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
}
valErrs, ok := validator.Validate(c, req)
if !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
}
rf, err := authSvc.RefreshToken(c.Context(), req.RefreshToken)
if err != nil {
logger.Info("Refresh token failed", "error", err)
if errors.Is(err, authentication.ErrExpiredToken) {
response.WriteJSON(c, fiber.StatusUnauthorized, "The refresh token has expired", nil, nil)
return nil
}
if errors.Is(err, authentication.ErrRefreshTokenNotFound) {
response.WriteJSON(c, fiber.StatusUnauthorized, "Refresh token not found", nil, nil)
return nil
}
logger.Error("Refresh token failed", "error", err)
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
return nil
}
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)
return nil
}
res := loginCustomerRes{
AccessToken: accessToken,
RefreshToken: rf,
}
return response.WriteJSON(c, fiber.StatusOK, "refresh successful", res, nil)
}
}
type logoutReq struct {
RefreshToken string `json:"refresh_token"`
}
// LogOutCustomer godoc
// @Summary Logout customer
// @Description Logout customer
// @Tags auth
// @Accept json
// @Produce json
// @Param logout body logoutReq true "Logout customer"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /auth/logout [post]
func LogOutCustomer(
logger *slog.Logger, authSvc *authentication.Service,
validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
var req logoutReq
if err := c.BodyParser(&req); err != nil {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
}
valErrs, ok := validator.Validate(c, req)
if !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
}
err := authSvc.Logout(c.Context(), req.RefreshToken)
if err != nil {
logger.Info("Logout failed", "error", err)
if errors.Is(err, authentication.ErrExpiredToken) {
response.WriteJSON(c, fiber.StatusUnauthorized, "The refresh token has expired", nil, nil)
return nil
}
if errors.Is(err, authentication.ErrRefreshTokenNotFound) {
response.WriteJSON(c, fiber.StatusUnauthorized, "Refresh token not found", nil, nil)
return nil
}
logger.Error("Logout failed", "error", err)
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
return nil
}
return response.WriteJSON(c, fiber.StatusOK, "Logout successful", nil, nil)
}
}

View File

@ -0,0 +1,266 @@
package handlers
import (
"log/slog"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"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 CreateBetReq struct {
Outcomes []int64 `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
Status domain.BetStatus `json:"status" example:"1"`
FullName string `json:"full_name" example:"John"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
}
type BetRes struct {
ID int64 `json:"id" example:"1"`
Outcomes []domain.Outcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
Status domain.BetStatus `json:"status" example:"1"`
FullName string `json:"full_name" example:"John"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
BranchID int64 `json:"branch_id" example:"2"`
UserID int64 `json:"user_id" example:"2"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
}
func convertBet(bet domain.Bet) BetRes {
return BetRes{
ID: bet.ID,
Outcomes: bet.Outcomes,
Amount: bet.Amount.Float64(),
TotalOdds: bet.TotalOdds,
Status: bet.Status,
FullName: bet.FullName,
PhoneNumber: bet.PhoneNumber,
BranchID: bet.BranchID.Value,
UserID: bet.UserID.Value,
}
}
// CreateBet godoc
// @Summary Create a bet
// @Description Creates a bet
// @Tags bet
// @Accept json
// @Produce json
// @Param createBet body CreateBetReq true "Creates bet"
// @Success 200 {object} BetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /bet [post]
func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
// TODO: Check the token, and find the role and get the branch id from there
// TODO Reduce amount from the branch wallet
var isShopBet bool = true
var branchID int64 = 1
var userID int64
var req CreateBetReq
if err := c.BodyParser(&req); err != nil {
logger.Error("CreateBetReq 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
}
// TODO Validate Outcomes Here and make sure they didn't expire
bet, err := betSvc.CreateBet(c.Context(), domain.CreateBet{
Outcomes: req.Outcomes,
Amount: domain.Currency(req.Amount),
TotalOdds: req.TotalOdds,
Status: req.Status,
FullName: req.FullName,
PhoneNumber: req.PhoneNumber,
BranchID: domain.ValidInt64{
Value: branchID,
Valid: isShopBet,
},
UserID: domain.ValidInt64{
Value: userID,
Valid: !isShopBet,
},
IsShopBet: req.IsShopBet,
})
if err != nil {
logger.Error("CreateBetReq failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil)
}
res := convertBet(bet)
return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil)
}
}
// GetAllBet godoc
// @Summary Gets all bets
// @Description Gets all the bets
// @Tags bet
// @Accept json
// @Produce json
// @Success 200 {array} BetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /bet [get]
func GetAllBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
bets, err := betSvc.GetAllBets(c.Context())
if err != nil {
logger.Error("Failed to get bets", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bets", err, nil)
}
var res []BetRes
for _, bet := range bets {
res = append(res, convertBet(bet))
}
return response.WriteJSON(c, fiber.StatusOK, "All Bets Retrieved", res, nil)
}
}
// GetBetByID godoc
// @Summary Gets bet by id
// @Description Gets a single bet by id
// @Tags bet
// @Accept json
// @Produce json
// @Param id path int true "Bet ID"
// @Success 200 {object} BetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [get]
func GetBetByID(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
betID := c.Params("id")
id, err := strconv.ParseInt(betID, 10, 64)
if err != nil {
logger.Error("Invalid bet ID", "betID", betID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil)
}
bet, err := betSvc.GetBetByID(c.Context(), id)
if err != nil {
logger.Error("Failed to get bet by ID", "betID", id, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil)
}
res := convertBet(bet)
return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)
}
}
type UpdateCashOutReq struct {
CashedOut bool
}
// UpdateCashOut godoc
// @Summary Updates the cashed out field
// @Description Updates the cashed out field
// @Tags bet
// @Accept json
// @Produce json
// @Param id path int true "Bet ID"
// @Param updateCashOut body UpdateCashOutReq true "Updates Cashed Out"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [patch]
func UpdateCashOut(logger *slog.Logger, betSvc *bet.Service,
validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
betID := c.Params("id")
id, err := strconv.ParseInt(betID, 10, 64)
if err != nil {
logger.Error("Invalid bet ID", "betID", betID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil)
}
var req UpdateCashOutReq
if err := c.BodyParser(&req); err != nil {
logger.Error("UpdateCashOutReq 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
}
err = betSvc.UpdateCashOut(c.Context(), id, req.CashedOut)
if err != nil {
logger.Error("Failed to update cash out bet", "betID", id, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update cash out bet", err, nil)
}
return response.WriteJSON(c, fiber.StatusOK, "Bet updated successfully", nil, nil)
}
}
// DeleteBet godoc
// @Summary Deletes bet by id
// @Description Deletes bet by id
// @Tags bet
// @Accept json
// @Produce json
// @Param id path int true "Bet ID"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [delete]
func DeleteBet(logger *slog.Logger, betSvc *bet.Service,
validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
betID := c.Params("id")
id, err := strconv.ParseInt(betID, 10, 64)
if err != nil {
logger.Error("Invalid bet ID", "betID", betID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil)
}
err = betSvc.DeleteBet(c.Context(), id)
if err != nil {
logger.Error("Failed to delete by ID", "betID", id, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to delete bet", err, nil)
}
return response.WriteJSON(c, fiber.StatusOK, "Bet removed successfully", nil, nil)
}
}

View File

@ -0,0 +1,153 @@
package handlers
import (
"log/slog"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"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 CreateTicketReq struct {
Outcomes []int64 `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
}
type CreateTicketRes struct {
FastCode int64 `json:"fast_code" example:"1234"`
}
// CreateTicket godoc
// @Summary Create a temporary ticket
// @Description Creates a temporary ticket
// @Tags ticket
// @Accept json
// @Produce json
// @Param createTicket body CreateTicketReq true "Creates ticket"
// @Success 200 {object} CreateTicketRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /ticket [post]
func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service,
validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
var req CreateTicketReq
if err := c.BodyParser(&req); err != nil {
logger.Error("CreateTicketReq 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
}
// TODO Validate Outcomes Here and make sure they didn't expire
ticket, err := ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{
Outcomes: req.Outcomes,
Amount: domain.Currency(req.Amount),
TotalOdds: req.TotalOdds,
})
if err != nil {
logger.Error("CreateTicketReq failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
res := CreateTicketRes{
FastCode: ticket.ID,
}
return response.WriteJSON(c, fiber.StatusOK, "Ticket Created", res, nil)
}
}
type TicketRes struct {
ID int64 `json:"id" example:"1"`
Outcomes []domain.Outcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
}
// GetTicketByID godoc
// @Summary Get ticket by ID
// @Description Retrieve ticket details by ticket ID
// @Tags ticket
// @Accept json
// @Produce json
// @Param id path int true "Ticket ID"
// @Success 200 {object} TicketRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /ticket/{id} [get]
func GetTicketByID(logger *slog.Logger, ticketSvc *ticket.Service,
validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
ticketID := c.Params("id")
id, err := strconv.ParseInt(ticketID, 10, 64)
if err != nil {
logger.Error("Invalid ticket ID", "ticketID", ticketID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid ticket ID", err, nil)
}
ticket, err := ticketSvc.GetTicketByID(c.Context(), id)
if err != nil {
logger.Error("Failed to get ticket by ID", "ticketID", id, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve ticket", err, nil)
}
res := TicketRes{
ID: ticket.ID,
Outcomes: ticket.Outcomes,
Amount: ticket.Amount.Float64(),
TotalOdds: ticket.TotalOdds,
}
return response.WriteJSON(c, fiber.StatusOK, "Ticket retrieved successfully", res, nil)
}
}
// GetAllTickets godoc
// @Summary Get all tickets
// @Description Retrieve all tickets
// @Tags ticket
// @Accept json
// @Produce json
// @Success 200 {array} TicketRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /ticket [get]
func GetAllTickets(logger *slog.Logger, ticketSvc *ticket.Service,
validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
tickets, err := ticketSvc.GetAllTickets(c.Context())
if err != nil {
logger.Error("Failed to get tickets", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve tickets", err, nil)
}
var res []TicketRes
for _, ticket := range tickets {
res = append(res, TicketRes{
ID: ticket.ID,
Outcomes: ticket.Outcomes,
Amount: ticket.Amount.Float64(),
TotalOdds: ticket.TotalOdds,
})
}
return response.WriteJSON(c, fiber.StatusOK, "All Tickets retrieved", res, 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

@ -0,0 +1,60 @@
package jwtutil
import (
"errors"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/golang-jwt/jwt/v5"
)
// type UserToken struct {
// UserId string
// }
var (
ErrExpiredToken = errors.New("token expired")
ErrMalformedToken = errors.New("token malformed")
ErrTokenNotExpired = errors.New("token not expired")
ErrRefreshTokenNotFound = errors.New("refresh token not found") // i.e login again
)
type UserClaim struct {
jwt.RegisteredClaims
UserId int64
Role domain.Role
}
type JwtConfig struct {
JwtAccessKey string
JwtAccessExpiry int
}
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"},
NotBefore: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiry) * time.Second))},
UserId: userId,
Role: Role,
})
jwtToken, err := token.SignedString([]byte(key)) //
return jwtToken, err
}
func ParseJwt(jwtToken string, key string) (*UserClaim, error) {
token, err := jwt.ParseWithClaims(jwtToken, &UserClaim{}, func(token *jwt.Token) (interface{}, error) {
return []byte(key), nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrExpiredToken
}
if errors.Is(err, jwt.ErrTokenMalformed) {
return nil, ErrMalformedToken
}
return nil, err
}
if claims, ok := token.Claims.(*UserClaim); ok && token.Valid {
return claims, nil
}
return nil, err
}

View File

@ -0,0 +1,89 @@
package jwtutil
// func TestCreateJwt(t *testing.T) {
// // Define a user to test
// user := &domain.User{
// ID: 123,
// }
// // Secret key used for signing the JWT
// secretKey := "secret"
// // Token expiry time (in seconds)
// expiry := 3600 // 1 hour
// // Call CreateJwt function
// tokenString, err := CreateJwt(user, secretKey, expiry)
// // Assertions
// assert.NoError(t, err, "Error should be nil when creating a JWT")
// assert.NotEmpty(t, tokenString, "Token string should not be empty")
// // Parse the token back and verify its claims
// claims, err := ParseJwt(tokenString, secretKey)
// assert.NoError(t, err, "Error should be nil when parsing the JWT")
// assert.Equal(t, strconv.Itoa(int(user.ID)), claims.UserId, "User ID should match")
// assert.Equal(t, "github.com/lafetz/snippitstash", claims.Issuer, "Issuer should match")
// assert.True(t, claims.ExpiresAt.Time.After(time.Now()), "Token should not be expired yet")
// expectedExpiryTime := time.Now().Add(time.Duration(expiry) * time.Second)
// // Allow for a small margin of error due to the time delay in generating the token
// assert.True(t, claims.ExpiresAt.Time.Before(expectedExpiryTime.Add(1*time.Second)),
// "Token expiry time should be within the expected range")
// assert.True(t, claims.ExpiresAt.Time.After(expectedExpiryTime.Add(-1*time.Second)),
// "Token expiry time should be within the expected range")
// }
// func TestParseJwt(t *testing.T) {
// // Define a user to test
// user := &domain.User{
// ID: 123,
// }
// // Secret key used for signing the JWT
// secretKey := "secret"
// // Token expiry time (in seconds)
// expiry := 3600 // 1 hour
// // Generate a token using the CreateJwt function
// tokenString, err := CreateJwt(user, secretKey, expiry)
// assert.NoError(t, err, "Error should be nil when creating a JWT")
// assert.NotEmpty(t, tokenString, "Token string should not be empty")
// // Now, we will parse the token
// claims, err := ParseJwt(tokenString, secretKey)
// assert.NoError(t, err, "Error should be nil when parsing the JWT")
// assert.NotNil(t, claims, "Claims should not be nil")
// // Verify that the claims match the user and other values
// assert.Equal(t, strconv.Itoa(int(user.ID)), claims.UserId, "User ID should match")
// assert.Equal(t, "github.com/lafetz/snippitstash", claims.Issuer, "Issuer should match")
// assert.Equal(t, "fortune.com", claims.Audience[0], "Audience should match")
// assert.True(t, claims.ExpiresAt.Time.After(time.Now()), "Token should not be expired yet")
// // Ensure the parsing fails when using an invalid token
// invalidToken := tokenString + "invalid"
// _, err = ParseJwt(invalidToken, secretKey)
// assert.Error(t, err, "Parsing an invalid token should return an error")
// }
// func TestParseJwte(t *testing.T) {
// // Define user and key
// user := &domain.User{ID: 1}
// key := "secretkey"
// // Test valid token (not expired)
// validJwt, err := CreateJwt(user, key, 4) // Set expiry to 10 seconds
// assert.NoError(t, err)
// // Test if the token is parsed correctly
// claims, err := ParseJwt(validJwt, key)
// assert.NoError(t, err)
// assert.Equal(t, "1", claims.UserId)
// // Wait for token to expire
// time.Sleep(5 * time.Second) // Wait longer than the expiry time to test expiration
// // Test expired token
// _, err = ParseJwt(validJwt, key)
// assert.Error(t, jwt.ErrTokenExpired) // Expect an error because the token should be expired
// }

View File

@ -0,0 +1,43 @@
package httpserver
import (
"errors"
"strings"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
"github.com/gofiber/fiber/v2"
)
func (a *App) authMiddleware(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" {
return fiber.NewError(fiber.StatusUnauthorized, "Authorization header missing")
}
if !strings.HasPrefix(authHeader, "Bearer ") {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid authorization header format")
}
accessToken := strings.TrimPrefix(authHeader, "Bearer ")
c.Locals("access_token", accessToken)
claim, err := jwtutil.ParseJwt(accessToken, a.JwtConfig.JwtAccessKey)
if err != nil {
if errors.Is(err, jwtutil.ErrExpiredToken) {
return fiber.NewError(fiber.StatusUnauthorized, "Access token expired")
}
return fiber.NewError(fiber.StatusUnauthorized, "Invalid access token")
}
refreshToken := c.Get("Refresh-Token")
if refreshToken == "" {
// refreshToken = c.Cookies("refresh_token", "")
// return fiber.NewError(fiber.StatusUnauthorized, "Refresh token missing")
}
c.Locals("user_id", claim.UserId)
c.Locals("role", claim.Role)
c.Locals("refresh_token", refreshToken)
return c.Next()
}

View File

@ -0,0 +1,47 @@
package response
import (
"time"
"github.com/gofiber/fiber/v2"
)
type Status string
const (
Error Status = "error"
Success Status = "success"
)
type APIResponse struct {
Status Status `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
func NewAPIResponse(
status Status, message string,
data interface{}, metadata interface{},
) APIResponse {
return APIResponse{
Status: status,
Message: message,
Data: data,
Metadata: metadata,
Timestamp: time.Now(),
}
}
func WriteJSON(c *fiber.Ctx, status int, message string, data, metadata interface{}) error {
var apiStatus Status
if status >= 200 && status <= 299 {
apiStatus = Success
} else {
apiStatus = Error
}
apiRes := NewAPIResponse(apiStatus, message, data, metadata)
return c.Status(status).JSON(apiRes)
}

View File

@ -1 +1,65 @@
package validator package customvalidator
import (
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type CustomValidator struct {
validate *validator.Validate
}
func NewCustomValidator(validate *validator.Validate) *CustomValidator {
return &CustomValidator{
validate: validate,
}
}
func (v *CustomValidator) Validate(c *fiber.Ctx, input interface{}) (map[string]string, bool) {
err := v.validate.Struct(input)
if err != nil {
if validationErrors, ok := err.(validator.ValidationErrors); ok {
errors := ValidateModel(validationErrors)
return errors, false
}
}
return nil, true
}
type ValidationErrorResponse struct {
StatusCode int `json:"statusCode"`
Errors interface{} `json:"errors"`
}
func ValidateModel(err validator.ValidationErrors) map[string]string {
errors := make(map[string]string)
for _, err := range err {
errors[strings.ToLower(err.Field())] = errorMsgs(err.Tag(), err.Param())
}
return errors
}
func errorMsgs(tag string, value string) string {
switch tag {
case "required":
return "This field is required"
case "numeric":
return "must be numeric " + value
case "lte":
return "can not be greater than " + value
case "gte":
return "can not be less than " + value
case "len":
return "length should be equal to " + value
case "email":
return "must be a valid email address"
}
return ""
}

View File

@ -30,7 +30,12 @@ migrations/up:
.PHONY: swagger .PHONY: swagger
swagger: swagger:
@swag init -g cmd/main.go @swag init -g cmd/main.go
.PHONY: db-up
db-up:
docker compose -f compose.db.yaml up
.PHONY: db-down
db-down:
docker compose -f compose.db.yaml down
.PHONY: sqlc-gen .PHONY: sqlc-gen
sqlc-gen: sqlc-gen:
@sqlc generate @sqlc generate