Merge branch 'main' into feature/notification
This commit is contained in:
commit
cfb1e2b7fe
58
cmd/main.go
58
cmd/main.go
|
|
@ -8,18 +8,34 @@ import (
|
|||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||
mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email"
|
||||
mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||
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"
|
||||
"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() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
slog.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg, err := config.NewConfig()
|
||||
if err != nil {
|
||||
slog.Error(err.Error())
|
||||
|
|
@ -28,18 +44,34 @@ func main() {
|
|||
|
||||
db, _, err := repository.OpenDB(cfg.DbUrl)
|
||||
if err != nil {
|
||||
fmt.Print(err)
|
||||
fmt.Print("db", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger := customlogger.NewLogger("development", slog.LevelDebug, "1.0")
|
||||
|
||||
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
|
||||
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)
|
||||
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 {
|
||||
log.Fatal("Failed to start server with error: ", err)
|
||||
logger.Error("Failed to start server", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,3 +72,8 @@ DROP TABLE IF EXISTS ussd_account;
|
|||
DROP TYPE IF EXISTS ua_pin_status;
|
||||
DROP TYPE IF EXISTS ua_status;
|
||||
DROP TYPE IF EXISTS ua_registaration_type;
|
||||
|
||||
-- Drop FortuneBet
|
||||
DROP TABLE IF EXIST tickets;
|
||||
DROP TABLE IF EXIST bets;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,101 @@ CREATE TABLE IF NOT EXISTS users (
|
|||
id BIGSERIAL PRIMARY KEY,
|
||||
first_name VARCHAR(255) NOT NULL,
|
||||
last_name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
phone_number VARCHAR(20) UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
email VARCHAR(255) UNIQUE ,
|
||||
phone_number VARCHAR(20) UNIQUE,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP
|
||||
password BYTEA NOT NULL,
|
||||
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ ,
|
||||
--
|
||||
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
|
||||
);
|
||||
|
||||
|
||||
-- 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
16
db/query/auth.sql
Normal 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
16
db/query/bet.sql
Normal 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
14
db/query/otp.sql
Normal 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
16
db/query/ticket.sql
Normal 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';
|
||||
|
|
@ -1,16 +1,42 @@
|
|||
-- name: CreateUser :one
|
||||
INSERT INTO users (first_name, last_name, email, phone_number, password, role, verified)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *;
|
||||
|
||||
INSERT INTO users (first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at;
|
||||
|
||||
-- name: GetUserByID :one
|
||||
SELECT * FROM users WHERE id = $1;
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetAllUsers :many
|
||||
SELECT * FROM users;
|
||||
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
|
||||
FROM users;
|
||||
|
||||
-- name: UpdateUser :exec
|
||||
UPDATE users SET first_name = $2, last_name = $3, email = $4, phone_number = $5, password = $6, role = $7, verified = $8, updated_at = CURRENT_TIMESTAMP WHERE id = $1;
|
||||
UPDATE users
|
||||
SET first_name = $1, last_name = $2, email = $3, phone_number = $4, role = $5, updated_at = $6
|
||||
WHERE id = $7;
|
||||
|
||||
-- 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
1226
docs/docs.go
Normal file
File diff suppressed because it is too large
Load Diff
1200
docs/swagger.json
Normal file
1200
docs/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
801
docs/swagger.yaml
Normal file
801
docs/swagger.yaml
Normal 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
97
gen/db/auth.sql.go
Normal 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
142
gen/db/bet.sql.go
Normal 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
|
||||
}
|
||||
|
|
@ -8,6 +8,21 @@ import (
|
|||
"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 {
|
||||
ID string
|
||||
RecipientID string
|
||||
|
|
@ -25,15 +40,47 @@ type Notification struct {
|
|||
Metadata []byte
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
PhoneNumber string
|
||||
Password string
|
||||
Role string
|
||||
Verified pgtype.Bool
|
||||
CreatedAt pgtype.Timestamp
|
||||
UpdatedAt pgtype.Timestamp
|
||||
type Otp struct {
|
||||
ID int64
|
||||
SentTo string
|
||||
Medium string
|
||||
OtpFor string
|
||||
Otp string
|
||||
Used bool
|
||||
UsedAt pgtype.Timestamptz
|
||||
CreatedAt pgtype.Timestamptz
|
||||
ExpiresAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type RefreshToken struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
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
84
gen/db/otp.sql.go
Normal 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
101
gen/db/ticket.sql.go
Normal 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
|
||||
}
|
||||
|
|
@ -11,42 +11,85 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const CheckPhoneEmailExist = `-- name: CheckPhoneEmailExist :one
|
||||
SELECT
|
||||
EXISTS (SELECT 1 FROM users WHERE users.phone_number = $1 AND users.phone_number IS NOT NULL) AS phone_exists,
|
||||
EXISTS (SELECT 1 FROM users WHERE users.email = $2 AND users.email IS NOT NULL) AS email_exists
|
||||
`
|
||||
|
||||
type CheckPhoneEmailExistParams struct {
|
||||
PhoneNumber pgtype.Text
|
||||
Email pgtype.Text
|
||||
}
|
||||
|
||||
type CheckPhoneEmailExistRow struct {
|
||||
PhoneExists bool
|
||||
EmailExists bool
|
||||
}
|
||||
|
||||
func (q *Queries) CheckPhoneEmailExist(ctx context.Context, arg CheckPhoneEmailExistParams) (CheckPhoneEmailExistRow, error) {
|
||||
row := q.db.QueryRow(ctx, CheckPhoneEmailExist, arg.PhoneNumber, arg.Email)
|
||||
var i CheckPhoneEmailExistRow
|
||||
err := row.Scan(&i.PhoneExists, &i.EmailExists)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (first_name, last_name, email, phone_number, password, role, verified)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at
|
||||
|
||||
INSERT INTO users (first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
PhoneNumber string
|
||||
Password string
|
||||
Role string
|
||||
Verified pgtype.Bool
|
||||
FirstName string
|
||||
LastName string
|
||||
Email pgtype.Text
|
||||
PhoneNumber pgtype.Text
|
||||
Role string
|
||||
Password []byte
|
||||
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,
|
||||
arg.FirstName,
|
||||
arg.LastName,
|
||||
arg.Email,
|
||||
arg.PhoneNumber,
|
||||
arg.Password,
|
||||
arg.Role,
|
||||
arg.Verified,
|
||||
arg.Password,
|
||||
arg.EmailVerified,
|
||||
arg.PhoneVerified,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
var i User
|
||||
var i CreateUserRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.PhoneNumber,
|
||||
&i.Password,
|
||||
&i.Role,
|
||||
&i.Verified,
|
||||
&i.EmailVerified,
|
||||
&i.PhoneVerified,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
|
|
@ -54,7 +97,8 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
|||
}
|
||||
|
||||
const DeleteUser = `-- name: DeleteUser :exec
|
||||
DELETE FROM users WHERE id = $1
|
||||
DELETE FROM users
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
|
||||
|
|
@ -63,27 +107,41 @@ func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
|
|||
}
|
||||
|
||||
const GetAllUsers = `-- name: GetAllUsers :many
|
||||
SELECT id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at FROM users
|
||||
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
|
||||
FROM users
|
||||
`
|
||||
|
||||
func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
|
||||
type GetAllUsersRow struct {
|
||||
ID int64
|
||||
FirstName string
|
||||
LastName string
|
||||
Email pgtype.Text
|
||||
PhoneNumber pgtype.Text
|
||||
Role string
|
||||
EmailVerified bool
|
||||
PhoneVerified bool
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
func (q *Queries) GetAllUsers(ctx context.Context) ([]GetAllUsersRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetAllUsers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []User
|
||||
var items []GetAllUsersRow
|
||||
for rows.Next() {
|
||||
var i User
|
||||
var i GetAllUsersRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.PhoneNumber,
|
||||
&i.Password,
|
||||
&i.Role,
|
||||
&i.Verified,
|
||||
&i.EmailVerified,
|
||||
&i.PhoneVerified,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
|
|
@ -97,8 +155,47 @@ func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const GetUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
|
||||
FROM users
|
||||
WHERE email = $1
|
||||
`
|
||||
|
||||
type GetUserByEmailRow struct {
|
||||
ID int64
|
||||
FirstName string
|
||||
LastName string
|
||||
Email pgtype.Text
|
||||
PhoneNumber pgtype.Text
|
||||
Role string
|
||||
EmailVerified bool
|
||||
PhoneVerified bool
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserByEmail(ctx context.Context, email pgtype.Text) (GetUserByEmailRow, error) {
|
||||
row := q.db.QueryRow(ctx, GetUserByEmail, email)
|
||||
var i GetUserByEmailRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.PhoneNumber,
|
||||
&i.Role,
|
||||
&i.EmailVerified,
|
||||
&i.PhoneVerified,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at FROM users WHERE id = $1
|
||||
SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, suspended_at, suspended
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||
|
|
@ -110,40 +207,103 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
|||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.PhoneNumber,
|
||||
&i.Password,
|
||||
&i.Role,
|
||||
&i.Verified,
|
||||
&i.Password,
|
||||
&i.EmailVerified,
|
||||
&i.PhoneVerified,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SuspendedAt,
|
||||
&i.Suspended,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetUserByPhone = `-- name: GetUserByPhone :one
|
||||
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
|
||||
FROM users
|
||||
WHERE phone_number = $1
|
||||
`
|
||||
|
||||
type GetUserByPhoneRow struct {
|
||||
ID int64
|
||||
FirstName string
|
||||
LastName string
|
||||
Email pgtype.Text
|
||||
PhoneNumber pgtype.Text
|
||||
Role string
|
||||
EmailVerified bool
|
||||
PhoneVerified bool
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserByPhone(ctx context.Context, phoneNumber pgtype.Text) (GetUserByPhoneRow, error) {
|
||||
row := q.db.QueryRow(ctx, GetUserByPhone, phoneNumber)
|
||||
var i GetUserByPhoneRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.PhoneNumber,
|
||||
&i.Role,
|
||||
&i.EmailVerified,
|
||||
&i.PhoneVerified,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const UpdatePassword = `-- name: UpdatePassword :exec
|
||||
UPDATE users
|
||||
SET password = $1, updated_at = $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
|
||||
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 {
|
||||
ID int64
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
PhoneNumber string
|
||||
Password string
|
||||
Email pgtype.Text
|
||||
PhoneNumber pgtype.Text
|
||||
Role string
|
||||
Verified pgtype.Bool
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
ID int64
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateUser,
|
||||
arg.ID,
|
||||
arg.FirstName,
|
||||
arg.LastName,
|
||||
arg.Email,
|
||||
arg.PhoneNumber,
|
||||
arg.Password,
|
||||
arg.Role,
|
||||
arg.Verified,
|
||||
arg.UpdatedAt,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
40
go.mod
40
go.mod
|
|
@ -4,36 +4,54 @@ go 1.24.1
|
|||
|
||||
require (
|
||||
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/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/gofiber/websocket/v2 v2.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.7.4
|
||||
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 (
|
||||
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/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/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/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-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/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/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.59.0 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.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
172
go.sum
|
|
@ -1,5 +1,12 @@
|
|||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
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/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
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/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/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/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/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/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/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
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/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
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/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/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/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.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/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
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.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
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/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/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/fasthttp v1.35.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||
github.com/valyala/fasthttp v1.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||
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/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/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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 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-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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
|
|
|||
|
|
@ -2,18 +2,33 @@ package config
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidDbUrl = errors.New("db url is invalid")
|
||||
ErrInvalidPort = errors.New("port number is invalid")
|
||||
ErrInvalidDbUrl = errors.New("db url 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 {
|
||||
Port int
|
||||
DbUrl string
|
||||
Port int
|
||||
DbUrl string
|
||||
RefreshExpiry int
|
||||
AccessExpiry int
|
||||
JwtKey string
|
||||
LogLevel slog.Level
|
||||
Env string
|
||||
}
|
||||
|
||||
func NewConfig() (*Config, error) {
|
||||
|
|
@ -24,7 +39,16 @@ func NewConfig() (*Config, error) {
|
|||
return config, nil
|
||||
}
|
||||
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")
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
|
|
@ -37,6 +61,33 @@ func (c *Config) loadEnv() error {
|
|||
return ErrInvalidDbUrl
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,12 @@
|
|||
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
44
internal/domain/bet.go
Normal 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
13
internal/domain/branch.go
Normal 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
39
internal/domain/common.go
Normal 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
6
internal/domain/event.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package domain
|
||||
|
||||
type Event struct {}
|
||||
|
||||
type Outcome struct {}
|
||||
|
||||
39
internal/domain/otp.go
Normal file
39
internal/domain/otp.go
Normal 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
11
internal/domain/role.go
Normal 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
15
internal/domain/ticket.go
Normal 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
|
||||
}
|
||||
|
|
@ -1,12 +1,53 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
FirstName string
|
||||
LastName string
|
||||
Email string
|
||||
PhoneNumber string
|
||||
Password string
|
||||
Role string
|
||||
Verified bool
|
||||
Password []byte
|
||||
Role Role
|
||||
//
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,12 @@ var LogLevels = map[string]slog.Level{
|
|||
"warn": slog.LevelWarn,
|
||||
"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
|
||||
switch env {
|
||||
case "development":
|
||||
|
|
@ -28,7 +32,6 @@ func NewLogger(env string, lvl slog.Level, version string) *slog.Logger {
|
|||
logger := slog.New(logHandler).With(slog.Group(
|
||||
"service_info",
|
||||
slog.String("env", env),
|
||||
slog.String("version", version),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
18
internal/mocks/mock_email/email.go
Normal file
18
internal/mocks/mock_email/email.go
Normal 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
|
||||
}
|
||||
19
internal/mocks/mock_sms/sms.go
Normal file
19
internal/mocks/mock_sms/sms.go
Normal 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){}
|
||||
77
internal/repository/auth.go
Normal file
77
internal/repository/auth.go
Normal 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)
|
||||
}
|
||||
96
internal/repository/bet.go
Normal file
96
internal/repository/bet.go
Normal 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)
|
||||
}
|
||||
60
internal/repository/otp.go
Normal file
60
internal/repository/otp.go
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
68
internal/repository/ticket.go
Normal file
68
internal/repository/ticket.go
Normal 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)
|
||||
}
|
||||
|
|
@ -2,47 +2,84 @@ package repository
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func (s *Store) CreateUser(ctx context.Context, firstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error) {
|
||||
user, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: email,
|
||||
PhoneNumber: phoneNumber,
|
||||
Password: password,
|
||||
Role: role,
|
||||
func (s *Store) CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error) {
|
||||
err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{
|
||||
ID: usedOtpId,
|
||||
UsedAt: pgtype.Timestamptz{
|
||||
Time: time.Now(),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
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 {
|
||||
return domain.User{}, err
|
||||
}
|
||||
return domain.User{
|
||||
ID: user.ID,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Password: user.Password,
|
||||
Role: user.Role,
|
||||
ID: userRes.ID,
|
||||
FirstName: userRes.FirstName,
|
||||
LastName: userRes.LastName,
|
||||
Email: userRes.Email.String,
|
||||
PhoneNumber: userRes.PhoneNumber.String,
|
||||
Role: domain.Role(userRes.Role),
|
||||
}, nil
|
||||
|
||||
}
|
||||
func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
|
||||
user, err := s.queries.GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.User{}, domain.ErrUserNotFound
|
||||
}
|
||||
return domain.User{}, err
|
||||
}
|
||||
return domain.User{
|
||||
ID: user.ID,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Password: user.Password,
|
||||
Role: user.Role,
|
||||
ID: user.ID,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Email: user.Email.String,
|
||||
PhoneNumber: user.PhoneNumber.String,
|
||||
Role: domain.Role(user.Role),
|
||||
EmailVerified: user.EmailVerified,
|
||||
Password: user.Password,
|
||||
PhoneVerified: user.PhoneVerified,
|
||||
CreatedAt: user.CreatedAt.Time,
|
||||
UpdatedAt: user.UpdatedAt.Time,
|
||||
SuspendedAt: user.SuspendedAt.Time,
|
||||
Suspended: user.Suspended,
|
||||
}, nil
|
||||
}
|
||||
func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||
|
|
@ -50,32 +87,125 @@ func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result []domain.User
|
||||
for _, user := range users {
|
||||
result = append(result, domain.User{
|
||||
userList := make([]domain.User, len(users))
|
||||
for i, user := range users {
|
||||
userList[i] = domain.User{
|
||||
ID: user.ID,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Password: user.Password,
|
||||
Role: user.Role,
|
||||
})
|
||||
Email: user.Email.String,
|
||||
PhoneNumber: user.PhoneNumber.String,
|
||||
Role: domain.Role(user.Role),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
return userList, nil
|
||||
}
|
||||
func (s *Store) UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error {
|
||||
func (s *Store) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error {
|
||||
err := s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
|
||||
ID: id,
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: email,
|
||||
PhoneNumber: phoneNumber,
|
||||
Password: password,
|
||||
Role: role,
|
||||
// ID: user.ID,
|
||||
// FirstName: user.FirstName,
|
||||
// LastName: user.LastName,
|
||||
// Email: user.Email,
|
||||
// PhoneNumber: user.PhoneNumber,
|
||||
|
||||
})
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *Store) DeleteUser(ctx context.Context, id int64) error {
|
||||
return s.queries.DeleteUser(ctx, id)
|
||||
err := s.queries.DeleteUser(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *Store) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) {
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
122
internal/services/authentication/impl.go
Normal file
122
internal/services/authentication/impl.go
Normal 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
|
||||
}
|
||||
16
internal/services/authentication/port.go
Normal file
16
internal/services/authentication/port.go
Normal 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
|
||||
}
|
||||
28
internal/services/authentication/service.go
Normal file
28
internal/services/authentication/service.go
Normal 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,
|
||||
}
|
||||
}
|
||||
15
internal/services/bet/port.go
Normal file
15
internal/services/bet/port.go
Normal 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
|
||||
}
|
||||
35
internal/services/bet/service.go
Normal file
35
internal/services/bet/service.go
Normal 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)
|
||||
}
|
||||
1
internal/services/sportsbook/events.go
Normal file
1
internal/services/sportsbook/events.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package sportsbook
|
||||
1
internal/services/sportsbook/odds.go
Normal file
1
internal/services/sportsbook/odds.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package sportsbook
|
||||
1
internal/services/sportsbook/service.go
Normal file
1
internal/services/sportsbook/service.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package sportsbook
|
||||
15
internal/services/ticket/port.go
Normal file
15
internal/services/ticket/port.go
Normal 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
|
||||
}
|
||||
30
internal/services/ticket/service.go
Normal file
30
internal/services/ticket/service.go
Normal 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)
|
||||
}
|
||||
44
internal/services/user/common.go
Normal file
44
internal/services/user/common.go
Normal 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
|
||||
}
|
||||
|
|
@ -7,9 +7,24 @@ import (
|
|||
)
|
||||
|
||||
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)
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
78
internal/services/user/register.go
Normal file
78
internal/services/user/register.go
Normal 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
|
||||
}
|
||||
63
internal/services/user/reset.go
Normal file
63
internal/services/user/reset.go
Normal 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
|
||||
}
|
||||
|
|
@ -1,33 +1,29 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
const (
|
||||
OtpExpiry = 5 * time.Minute
|
||||
)
|
||||
|
||||
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{
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
16
internal/services/user/user.go
Normal file
16
internal/services/user/user.go
Normal 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)
|
||||
}
|
||||
|
|
@ -4,19 +4,41 @@ import (
|
|||
"fmt"
|
||||
"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"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
fiber *fiber.App
|
||||
fiber *fiber.App
|
||||
logger *slog.Logger
|
||||
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
|
||||
}
|
||||
|
||||
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{
|
||||
CaseSensitive: true,
|
||||
DisableHeaderNormalizing: true,
|
||||
|
|
@ -24,8 +46,15 @@ func NewApp(port int, logger *slog.Logger, notidicationStore notificationservice
|
|||
JSONDecoder: sonic.Unmarshal,
|
||||
})
|
||||
s := &App{
|
||||
fiber: app,
|
||||
port: port,
|
||||
fiber: app,
|
||||
port: port,
|
||||
authSvc: authSvc,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
JwtConfig: JwtConfig,
|
||||
userSvc: userSvc,
|
||||
ticketSvc: ticketSvc,
|
||||
betSvc: betSvc,
|
||||
NotidicationStore: notidicationStore,
|
||||
Logger: logger,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
182
internal/web_server/handlers/auth_handler.go
Normal file
182
internal/web_server/handlers/auth_handler.go
Normal 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)
|
||||
}
|
||||
}
|
||||
266
internal/web_server/handlers/bet_handler.go
Normal file
266
internal/web_server/handlers/bet_handler.go
Normal 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)
|
||||
}
|
||||
}
|
||||
153
internal/web_server/handlers/ticket_handler.go
Normal file
153
internal/web_server/handlers/ticket_handler.go
Normal 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)
|
||||
|
||||
}
|
||||
}
|
||||
365
internal/web_server/handlers/user.go
Normal file
365
internal/web_server/handlers/user.go
Normal 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")
|
||||
}
|
||||
60
internal/web_server/jwt/jwt.go
Normal file
60
internal/web_server/jwt/jwt.go
Normal 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
|
||||
}
|
||||
89
internal/web_server/jwt/jwt_test.go
Normal file
89
internal/web_server/jwt/jwt_test.go
Normal 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
|
||||
// }
|
||||
43
internal/web_server/middleware.go
Normal file
43
internal/web_server/middleware.go
Normal 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()
|
||||
}
|
||||
47
internal/web_server/response/res.go
Normal file
47
internal/web_server/response/res.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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 ""
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user