Merge branch 'main' into ticket-bet
This commit is contained in:
commit
0c38426549
51
cmd/main.go
51
cmd/main.go
|
|
@ -6,17 +6,33 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
|
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||||
|
mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email"
|
||||||
|
mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||||
// httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||||
"github.com/joho/godotenv"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
|
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
||||||
|
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||||
|
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @title FortuneBet API
|
||||||
|
// @version 1.0
|
||||||
|
// @description This is server for FortuneBet.
|
||||||
|
// @termsOfService http://swagger.io/terms/
|
||||||
|
// @contact.name API Support
|
||||||
|
// @contact.url http://www.swagger.io/support
|
||||||
|
// @contact.email support@swagger.io
|
||||||
|
// @license.name Apache 2.0
|
||||||
|
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
// @SecurityDefinitions.apiKey Bearer
|
||||||
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
// @BasePath /
|
||||||
func main() {
|
func main() {
|
||||||
err := godotenv.Load()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error(err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
cfg, err := config.NewConfig()
|
cfg, err := config.NewConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error(err.Error())
|
slog.Error(err.Error())
|
||||||
|
|
@ -24,12 +40,25 @@ func main() {
|
||||||
}
|
}
|
||||||
db, _, err := repository.OpenDB(cfg.DbUrl)
|
db, _, err := repository.OpenDB(cfg.DbUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Print(err)
|
fmt.Print("db", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
|
||||||
store := repository.NewStore(db)
|
store := repository.NewStore(db)
|
||||||
fmt.Println(store)
|
v := customvalidator.NewCustomValidator(validator.New())
|
||||||
|
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
|
||||||
|
mockSms := mocksms.NewMockSMS()
|
||||||
|
mockemail := mockemail.NewMockEmail()
|
||||||
|
userSvc := user.NewService(store, store, mockSms, mockemail)
|
||||||
|
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
|
||||||
|
JwtAccessKey: cfg.JwtKey,
|
||||||
|
JwtAccessExpiry: cfg.AccessExpiry,
|
||||||
|
}, userSvc,
|
||||||
|
)
|
||||||
|
logger.Info("Starting server", "port", cfg.Port)
|
||||||
|
if err := app.Run(); err != nil {
|
||||||
|
logger.Error("Failed to start server", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// app := httpserver.NewApp(cfg.Port)
|
|
||||||
// app.Run()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,62 @@ CREATE TABLE users (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
first_name VARCHAR(255) NOT NULL,
|
first_name VARCHAR(255) NOT NULL,
|
||||||
last_name VARCHAR(255) NOT NULL,
|
last_name VARCHAR(255) NOT NULL,
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
email VARCHAR(255) UNIQUE ,
|
||||||
phone_number VARCHAR(20) UNIQUE NOT NULL,
|
phone_number VARCHAR(20) UNIQUE,
|
||||||
password TEXT NOT NULL,
|
|
||||||
role VARCHAR(50) NOT NULL,
|
role VARCHAR(50) NOT NULL,
|
||||||
verified BOOLEAN DEFAULT FALSE,
|
password BYTEA NOT NULL,
|
||||||
created_at TIMESTAMP,
|
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
updated_at TIMESTAMP
|
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
|
||||||
|
);
|
||||||
|
----------------------------------------------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;
|
||||||
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, CURRENT_TIMESTAMP, $5);
|
||||||
|
|
||||||
|
-- name: GetOtp :one
|
||||||
|
SELECT id, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
|
||||||
|
FROM otps
|
||||||
|
WHERE sent_to = $1 AND otp_for = $2 AND medium = $3
|
||||||
|
ORDER BY created_at DESC LIMIT 1;
|
||||||
|
|
||||||
|
-- name: MarkOtpAsUsed :exec
|
||||||
|
UPDATE otps
|
||||||
|
SET used = TRUE, used_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $1;
|
||||||
|
|
@ -1,16 +1,42 @@
|
||||||
-- name: CreateUser :one
|
-- name: CreateUser :one
|
||||||
INSERT INTO users (first_name, last_name, email, phone_number, password, role, verified)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
INSERT INTO users (first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at)
|
||||||
RETURNING *;
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
RETURNING id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at;
|
||||||
|
|
||||||
-- name: GetUserByID :one
|
-- name: GetUserByID :one
|
||||||
SELECT * FROM users WHERE id = $1;
|
SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: GetAllUsers :many
|
-- name: GetAllUsers :many
|
||||||
SELECT * FROM users;
|
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
|
||||||
|
FROM users;
|
||||||
|
|
||||||
-- name: UpdateUser :exec
|
-- name: UpdateUser :exec
|
||||||
UPDATE users SET first_name = $2, last_name = $3, email = $4, phone_number = $5, password = $6, role = $7, verified = $8, updated_at = CURRENT_TIMESTAMP WHERE id = $1;
|
UPDATE users
|
||||||
|
SET first_name = $1, last_name = $2, email = $3, phone_number = $4, role = $5, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $6;
|
||||||
|
|
||||||
-- name: DeleteUser :exec
|
-- name: DeleteUser :exec
|
||||||
DELETE FROM users WHERE id = $1;
|
DELETE FROM users
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: CheckPhoneEmailExist :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (SELECT 1 FROM users WHERE users.phone_number = $1 AND users.phone_number IS NOT NULL) AS phone_exists,
|
||||||
|
EXISTS (SELECT 1 FROM users WHERE users.email = $2 AND users.email IS NOT NULL) AS email_exists;
|
||||||
|
-- name: GetUserByEmail :one
|
||||||
|
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE email = $1;
|
||||||
|
|
||||||
|
-- name: GetUserByPhone :one
|
||||||
|
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE phone_number = $1;
|
||||||
|
|
||||||
|
-- name: UpdatePassword :exec
|
||||||
|
UPDATE users
|
||||||
|
SET password = $1, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE (email = $2 OR phone_number = $3);
|
||||||
714
docs/docs.go
Normal file
714
docs/docs.go
Normal file
|
|
@ -0,0 +1,714 @@
|
||||||
|
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
||||||
|
package docs
|
||||||
|
|
||||||
|
import "github.com/swaggo/swag"
|
||||||
|
|
||||||
|
const docTemplate = `{
|
||||||
|
"schemes": {{ marshal .Schemes }},
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "{{escape .Description}}",
|
||||||
|
"title": "{{.Title}}",
|
||||||
|
"termsOfService": "http://swagger.io/terms/",
|
||||||
|
"contact": {
|
||||||
|
"name": "API Support",
|
||||||
|
"url": "http://www.swagger.io/support",
|
||||||
|
"email": "support@swagger.io"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "Apache 2.0",
|
||||||
|
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||||
|
},
|
||||||
|
"version": "{{.Version}}"
|
||||||
|
},
|
||||||
|
"host": "{{.Host}}",
|
||||||
|
"basePath": "{{.BasePath}}",
|
||||||
|
"paths": {
|
||||||
|
"/auth/login": {
|
||||||
|
"post": {
|
||||||
|
"description": "Login customer",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Login customer",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Login customer",
|
||||||
|
"name": "login",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.loginCustomerReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/logout": {
|
||||||
|
"post": {
|
||||||
|
"description": "Logout customer",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Logout customer",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Logout customer",
|
||||||
|
"name": "logout",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.logoutReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/refresh": {
|
||||||
|
"post": {
|
||||||
|
"description": "Refresh token",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Refresh token",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "tokens",
|
||||||
|
"name": "refresh",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.refreshToken"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/checkPhoneEmailExist": {
|
||||||
|
"post": {
|
||||||
|
"description": "Check if phone number or email exist",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Check if phone number or email exist",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Check phone number or email exist",
|
||||||
|
"name": "checkPhoneEmailExist",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.CheckPhoneEmailExistReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.CheckPhoneEmailExistRes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/profile": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get user profile",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Get user profile",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.UserProfileRes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/register": {
|
||||||
|
"post": {
|
||||||
|
"description": "Register user",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Register user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Register user",
|
||||||
|
"name": "registerUser",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.RegisterUserReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/resetPassword": {
|
||||||
|
"post": {
|
||||||
|
"description": "Reset password",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Reset password",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Reset password",
|
||||||
|
"name": "resetPassword",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.ResetPasswordReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/sendRegisterCode": {
|
||||||
|
"post": {
|
||||||
|
"description": "Send register code",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Send register code",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Send register code",
|
||||||
|
"name": "registerCode",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.RegisterCodeReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/sendResetCode": {
|
||||||
|
"post": {
|
||||||
|
"description": "Send reset code",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Send reset code",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Send reset code",
|
||||||
|
"name": "resetCode",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.ResetCodeReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"domain.Role": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"admin",
|
||||||
|
"customer",
|
||||||
|
"super_admin",
|
||||||
|
"branch_manager",
|
||||||
|
"cashier"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"RoleAdmin",
|
||||||
|
"RoleCustomer",
|
||||||
|
"RoleSuperAdmin",
|
||||||
|
"RoleBranchManager",
|
||||||
|
"RoleCashier"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"handlers.CheckPhoneEmailExistReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john.doe@example.com"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.CheckPhoneEmailExistRes": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email_exist": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"phone_number_exist": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.RegisterCodeReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john.doe@example.com"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.RegisterUserReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john.doe@example.com"
|
||||||
|
},
|
||||||
|
"first_name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "John"
|
||||||
|
},
|
||||||
|
"last_name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Doe"
|
||||||
|
},
|
||||||
|
"otp": {
|
||||||
|
"description": "Role string",
|
||||||
|
"type": "string",
|
||||||
|
"example": "123456"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "password123"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1234567890"
|
||||||
|
},
|
||||||
|
"referal_code": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "ABC123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.ResetCodeReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john.doe@example.com"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.ResetPasswordReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"otp": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phoneNumber": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.UserProfileRes": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"first_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"last_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone_verified": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"$ref": "#/definitions/domain.Role"
|
||||||
|
},
|
||||||
|
"suspended": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"suspended_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.loginCustomerReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john.doe@example.com"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "password123"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.loginCustomerRes": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.logoutReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.refreshToken": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response.APIResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"status": {
|
||||||
|
"$ref": "#/definitions/response.Status"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response.Status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"error",
|
||||||
|
"success"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"Error",
|
||||||
|
"Success"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securityDefinitions": {
|
||||||
|
"Bearer": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||||
|
var SwaggerInfo = &swag.Spec{
|
||||||
|
Version: "1.0",
|
||||||
|
Host: "",
|
||||||
|
BasePath: "",
|
||||||
|
Schemes: []string{},
|
||||||
|
Title: "FortuneBet API",
|
||||||
|
Description: "This is server for FortuneBet.",
|
||||||
|
InfoInstanceName: "swagger",
|
||||||
|
SwaggerTemplate: docTemplate,
|
||||||
|
LeftDelim: "{{",
|
||||||
|
RightDelim: "}}",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||||
|
}
|
||||||
688
docs/swagger.json
Normal file
688
docs/swagger.json
Normal file
|
|
@ -0,0 +1,688 @@
|
||||||
|
{
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "This is server for FortuneBet.",
|
||||||
|
"title": "FortuneBet API",
|
||||||
|
"termsOfService": "http://swagger.io/terms/",
|
||||||
|
"contact": {
|
||||||
|
"name": "API Support",
|
||||||
|
"url": "http://www.swagger.io/support",
|
||||||
|
"email": "support@swagger.io"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "Apache 2.0",
|
||||||
|
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||||
|
},
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/auth/login": {
|
||||||
|
"post": {
|
||||||
|
"description": "Login customer",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Login customer",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Login customer",
|
||||||
|
"name": "login",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.loginCustomerReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/logout": {
|
||||||
|
"post": {
|
||||||
|
"description": "Logout customer",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Logout customer",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Logout customer",
|
||||||
|
"name": "logout",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.logoutReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/refresh": {
|
||||||
|
"post": {
|
||||||
|
"description": "Refresh token",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Refresh token",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "tokens",
|
||||||
|
"name": "refresh",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.refreshToken"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/checkPhoneEmailExist": {
|
||||||
|
"post": {
|
||||||
|
"description": "Check if phone number or email exist",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Check if phone number or email exist",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Check phone number or email exist",
|
||||||
|
"name": "checkPhoneEmailExist",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.CheckPhoneEmailExistReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.CheckPhoneEmailExistRes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/profile": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get user profile",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Get user profile",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.UserProfileRes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/register": {
|
||||||
|
"post": {
|
||||||
|
"description": "Register user",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Register user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Register user",
|
||||||
|
"name": "registerUser",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.RegisterUserReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/resetPassword": {
|
||||||
|
"post": {
|
||||||
|
"description": "Reset password",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Reset password",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Reset password",
|
||||||
|
"name": "resetPassword",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.ResetPasswordReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/sendRegisterCode": {
|
||||||
|
"post": {
|
||||||
|
"description": "Send register code",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Send register code",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Send register code",
|
||||||
|
"name": "registerCode",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.RegisterCodeReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/sendResetCode": {
|
||||||
|
"post": {
|
||||||
|
"description": "Send reset code",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"summary": "Send reset code",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Send reset code",
|
||||||
|
"name": "resetCode",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.ResetCodeReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"domain.Role": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"admin",
|
||||||
|
"customer",
|
||||||
|
"super_admin",
|
||||||
|
"branch_manager",
|
||||||
|
"cashier"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"RoleAdmin",
|
||||||
|
"RoleCustomer",
|
||||||
|
"RoleSuperAdmin",
|
||||||
|
"RoleBranchManager",
|
||||||
|
"RoleCashier"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"handlers.CheckPhoneEmailExistReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john.doe@example.com"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.CheckPhoneEmailExistRes": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email_exist": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"phone_number_exist": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.RegisterCodeReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john.doe@example.com"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.RegisterUserReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john.doe@example.com"
|
||||||
|
},
|
||||||
|
"first_name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "John"
|
||||||
|
},
|
||||||
|
"last_name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Doe"
|
||||||
|
},
|
||||||
|
"otp": {
|
||||||
|
"description": "Role string",
|
||||||
|
"type": "string",
|
||||||
|
"example": "123456"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "password123"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1234567890"
|
||||||
|
},
|
||||||
|
"referal_code": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "ABC123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.ResetCodeReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john.doe@example.com"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.ResetPasswordReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"otp": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phoneNumber": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.UserProfileRes": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"first_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"last_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone_verified": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"$ref": "#/definitions/domain.Role"
|
||||||
|
},
|
||||||
|
"suspended": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"suspended_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.loginCustomerReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john.doe@example.com"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "password123"
|
||||||
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.loginCustomerRes": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.logoutReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.refreshToken": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response.APIResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"status": {
|
||||||
|
"$ref": "#/definitions/response.Status"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response.Status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"error",
|
||||||
|
"success"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"Error",
|
||||||
|
"Success"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securityDefinitions": {
|
||||||
|
"Bearer": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
458
docs/swagger.yaml
Normal file
458
docs/swagger.yaml
Normal file
|
|
@ -0,0 +1,458 @@
|
||||||
|
definitions:
|
||||||
|
domain.Role:
|
||||||
|
enum:
|
||||||
|
- admin
|
||||||
|
- customer
|
||||||
|
- super_admin
|
||||||
|
- branch_manager
|
||||||
|
- cashier
|
||||||
|
type: string
|
||||||
|
x-enum-varnames:
|
||||||
|
- RoleAdmin
|
||||||
|
- RoleCustomer
|
||||||
|
- RoleSuperAdmin
|
||||||
|
- RoleBranchManager
|
||||||
|
- RoleCashier
|
||||||
|
handlers.CheckPhoneEmailExistReq:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
example: john.doe@example.com
|
||||||
|
type: string
|
||||||
|
phone_number:
|
||||||
|
example: "1234567890"
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handlers.CheckPhoneEmailExistRes:
|
||||||
|
properties:
|
||||||
|
email_exist:
|
||||||
|
type: boolean
|
||||||
|
phone_number_exist:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
handlers.RegisterCodeReq:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
example: john.doe@example.com
|
||||||
|
type: string
|
||||||
|
phone_number:
|
||||||
|
example: "1234567890"
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handlers.RegisterUserReq:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
example: john.doe@example.com
|
||||||
|
type: string
|
||||||
|
first_name:
|
||||||
|
example: John
|
||||||
|
type: string
|
||||||
|
last_name:
|
||||||
|
example: Doe
|
||||||
|
type: string
|
||||||
|
otp:
|
||||||
|
description: Role string
|
||||||
|
example: "123456"
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
example: password123
|
||||||
|
type: string
|
||||||
|
phone_number:
|
||||||
|
example: "1234567890"
|
||||||
|
type: string
|
||||||
|
referal_code:
|
||||||
|
example: ABC123
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handlers.ResetCodeReq:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
example: john.doe@example.com
|
||||||
|
type: string
|
||||||
|
phone_number:
|
||||||
|
example: "1234567890"
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handlers.ResetPasswordReq:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
otp:
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
phoneNumber:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handlers.UserProfileRes:
|
||||||
|
properties:
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
email_verified:
|
||||||
|
type: boolean
|
||||||
|
first_name:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
last_name:
|
||||||
|
type: string
|
||||||
|
phone_number:
|
||||||
|
type: string
|
||||||
|
phone_verified:
|
||||||
|
type: boolean
|
||||||
|
role:
|
||||||
|
$ref: '#/definitions/domain.Role'
|
||||||
|
suspended:
|
||||||
|
type: boolean
|
||||||
|
suspended_at:
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handlers.loginCustomerReq:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
example: john.doe@example.com
|
||||||
|
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
|
||||||
|
/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.26.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
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,27 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Otp struct {
|
||||||
|
ID int64
|
||||||
|
SentTo string
|
||||||
|
Medium string
|
||||||
|
OtpFor string
|
||||||
|
Otp string
|
||||||
|
Used bool
|
||||||
|
UsedAt pgtype.Timestamptz
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
ExpiresAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshToken struct {
|
||||||
|
ID int64
|
||||||
|
UserID int64
|
||||||
|
Token string
|
||||||
|
ExpiresAt pgtype.Timestamptz
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
Revoked bool
|
||||||
|
}
|
||||||
|
|
||||||
type Bet struct {
|
type Bet struct {
|
||||||
ID int64
|
ID int64
|
||||||
Amount int64
|
Amount int64
|
||||||
|
|
@ -34,11 +55,14 @@ type User struct {
|
||||||
ID int64
|
ID int64
|
||||||
FirstName string
|
FirstName string
|
||||||
LastName string
|
LastName string
|
||||||
Email string
|
Email pgtype.Text
|
||||||
PhoneNumber string
|
PhoneNumber pgtype.Text
|
||||||
Password string
|
|
||||||
Role string
|
Role string
|
||||||
Verified pgtype.Bool
|
Password []byte
|
||||||
CreatedAt pgtype.Timestamp
|
EmailVerified bool
|
||||||
UpdatedAt pgtype.Timestamp
|
PhoneVerified bool
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
SuspendedAt pgtype.Timestamptz
|
||||||
|
Suspended bool
|
||||||
}
|
}
|
||||||
|
|
|
||||||
77
gen/db/otp.sql.go
Normal file
77
gen/db/otp.sql.go
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.26.0
|
||||||
|
// source: otp.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateOtp = `-- name: CreateOtp :exec
|
||||||
|
INSERT INTO otps (sent_to, medium, otp_for, otp, used, created_at, expires_at)
|
||||||
|
VALUES ($1, $2, $3, $4, FALSE, CURRENT_TIMESTAMP, $5)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateOtpParams struct {
|
||||||
|
SentTo string
|
||||||
|
Medium string
|
||||||
|
OtpFor string
|
||||||
|
Otp string
|
||||||
|
ExpiresAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateOtp(ctx context.Context, arg CreateOtpParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, CreateOtp,
|
||||||
|
arg.SentTo,
|
||||||
|
arg.Medium,
|
||||||
|
arg.OtpFor,
|
||||||
|
arg.Otp,
|
||||||
|
arg.ExpiresAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetOtp = `-- name: GetOtp :one
|
||||||
|
SELECT id, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
|
||||||
|
FROM otps
|
||||||
|
WHERE sent_to = $1 AND otp_for = $2 AND medium = $3
|
||||||
|
ORDER BY created_at DESC LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetOtpParams struct {
|
||||||
|
SentTo string
|
||||||
|
OtpFor string
|
||||||
|
Medium string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetOtp(ctx context.Context, arg GetOtpParams) (Otp, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetOtp, arg.SentTo, arg.OtpFor, arg.Medium)
|
||||||
|
var i Otp
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SentTo,
|
||||||
|
&i.Medium,
|
||||||
|
&i.OtpFor,
|
||||||
|
&i.Otp,
|
||||||
|
&i.Used,
|
||||||
|
&i.UsedAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const MarkOtpAsUsed = `-- name: MarkOtpAsUsed :exec
|
||||||
|
UPDATE otps
|
||||||
|
SET used = TRUE, used_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) MarkOtpAsUsed(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, MarkOtpAsUsed, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -11,42 +11,81 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const CheckPhoneEmailExist = `-- name: CheckPhoneEmailExist :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (SELECT 1 FROM users WHERE users.phone_number = $1 AND users.phone_number IS NOT NULL) AS phone_exists,
|
||||||
|
EXISTS (SELECT 1 FROM users WHERE users.email = $2 AND users.email IS NOT NULL) AS email_exists
|
||||||
|
`
|
||||||
|
|
||||||
|
type CheckPhoneEmailExistParams struct {
|
||||||
|
PhoneNumber pgtype.Text
|
||||||
|
Email pgtype.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckPhoneEmailExistRow struct {
|
||||||
|
PhoneExists bool
|
||||||
|
EmailExists bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CheckPhoneEmailExist(ctx context.Context, arg CheckPhoneEmailExistParams) (CheckPhoneEmailExistRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CheckPhoneEmailExist, arg.PhoneNumber, arg.Email)
|
||||||
|
var i CheckPhoneEmailExistRow
|
||||||
|
err := row.Scan(&i.PhoneExists, &i.EmailExists)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const CreateUser = `-- name: CreateUser :one
|
const CreateUser = `-- name: CreateUser :one
|
||||||
INSERT INTO users (first_name, last_name, email, phone_number, password, role, verified)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
INSERT INTO users (first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at)
|
||||||
RETURNING id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
RETURNING id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateUserParams struct {
|
type CreateUserParams struct {
|
||||||
FirstName string
|
FirstName string
|
||||||
LastName string
|
LastName string
|
||||||
Email string
|
Email pgtype.Text
|
||||||
PhoneNumber string
|
PhoneNumber pgtype.Text
|
||||||
Password string
|
|
||||||
Role string
|
Role string
|
||||||
Verified pgtype.Bool
|
Password []byte
|
||||||
|
EmailVerified bool
|
||||||
|
PhoneVerified bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
type CreateUserRow struct {
|
||||||
|
ID int64
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
Email pgtype.Text
|
||||||
|
PhoneNumber pgtype.Text
|
||||||
|
Role string
|
||||||
|
EmailVerified bool
|
||||||
|
PhoneVerified bool
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
|
||||||
row := q.db.QueryRow(ctx, CreateUser,
|
row := q.db.QueryRow(ctx, CreateUser,
|
||||||
arg.FirstName,
|
arg.FirstName,
|
||||||
arg.LastName,
|
arg.LastName,
|
||||||
arg.Email,
|
arg.Email,
|
||||||
arg.PhoneNumber,
|
arg.PhoneNumber,
|
||||||
arg.Password,
|
|
||||||
arg.Role,
|
arg.Role,
|
||||||
arg.Verified,
|
arg.Password,
|
||||||
|
arg.EmailVerified,
|
||||||
|
arg.PhoneVerified,
|
||||||
)
|
)
|
||||||
var i User
|
var i CreateUserRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.FirstName,
|
&i.FirstName,
|
||||||
&i.LastName,
|
&i.LastName,
|
||||||
&i.Email,
|
&i.Email,
|
||||||
&i.PhoneNumber,
|
&i.PhoneNumber,
|
||||||
&i.Password,
|
|
||||||
&i.Role,
|
&i.Role,
|
||||||
&i.Verified,
|
&i.EmailVerified,
|
||||||
|
&i.PhoneVerified,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
|
|
@ -54,7 +93,8 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeleteUser = `-- name: DeleteUser :exec
|
const DeleteUser = `-- name: DeleteUser :exec
|
||||||
DELETE FROM users WHERE id = $1
|
DELETE FROM users
|
||||||
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
|
func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
|
||||||
|
|
@ -63,27 +103,41 @@ func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetAllUsers = `-- name: GetAllUsers :many
|
const GetAllUsers = `-- name: GetAllUsers :many
|
||||||
SELECT id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at FROM users
|
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
|
||||||
|
FROM users
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
|
type GetAllUsersRow struct {
|
||||||
|
ID int64
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
Email pgtype.Text
|
||||||
|
PhoneNumber pgtype.Text
|
||||||
|
Role string
|
||||||
|
EmailVerified bool
|
||||||
|
PhoneVerified bool
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAllUsers(ctx context.Context) ([]GetAllUsersRow, error) {
|
||||||
rows, err := q.db.Query(ctx, GetAllUsers)
|
rows, err := q.db.Query(ctx, GetAllUsers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []User
|
var items []GetAllUsersRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i User
|
var i GetAllUsersRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.FirstName,
|
&i.FirstName,
|
||||||
&i.LastName,
|
&i.LastName,
|
||||||
&i.Email,
|
&i.Email,
|
||||||
&i.PhoneNumber,
|
&i.PhoneNumber,
|
||||||
&i.Password,
|
|
||||||
&i.Role,
|
&i.Role,
|
||||||
&i.Verified,
|
&i.EmailVerified,
|
||||||
|
&i.PhoneVerified,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
@ -97,8 +151,47 @@ func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetUserByEmail = `-- name: GetUserByEmail :one
|
||||||
|
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE email = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetUserByEmailRow struct {
|
||||||
|
ID int64
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
Email pgtype.Text
|
||||||
|
PhoneNumber pgtype.Text
|
||||||
|
Role string
|
||||||
|
EmailVerified bool
|
||||||
|
PhoneVerified bool
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByEmail(ctx context.Context, email pgtype.Text) (GetUserByEmailRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetUserByEmail, email)
|
||||||
|
var i GetUserByEmailRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.FirstName,
|
||||||
|
&i.LastName,
|
||||||
|
&i.Email,
|
||||||
|
&i.PhoneNumber,
|
||||||
|
&i.Role,
|
||||||
|
&i.EmailVerified,
|
||||||
|
&i.PhoneVerified,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const GetUserByID = `-- name: GetUserByID :one
|
const GetUserByID = `-- name: GetUserByID :one
|
||||||
SELECT id, first_name, last_name, email, phone_number, password, role, verified, created_at, updated_at FROM users WHERE id = $1
|
SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, suspended_at, suspended
|
||||||
|
FROM users
|
||||||
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||||
|
|
@ -110,40 +203,95 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||||
&i.LastName,
|
&i.LastName,
|
||||||
&i.Email,
|
&i.Email,
|
||||||
&i.PhoneNumber,
|
&i.PhoneNumber,
|
||||||
&i.Password,
|
|
||||||
&i.Role,
|
&i.Role,
|
||||||
&i.Verified,
|
&i.Password,
|
||||||
|
&i.EmailVerified,
|
||||||
|
&i.PhoneVerified,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SuspendedAt,
|
||||||
|
&i.Suspended,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetUserByPhone = `-- name: GetUserByPhone :one
|
||||||
|
SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE phone_number = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetUserByPhoneRow struct {
|
||||||
|
ID int64
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
Email pgtype.Text
|
||||||
|
PhoneNumber pgtype.Text
|
||||||
|
Role string
|
||||||
|
EmailVerified bool
|
||||||
|
PhoneVerified bool
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByPhone(ctx context.Context, phoneNumber pgtype.Text) (GetUserByPhoneRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetUserByPhone, phoneNumber)
|
||||||
|
var i GetUserByPhoneRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.FirstName,
|
||||||
|
&i.LastName,
|
||||||
|
&i.Email,
|
||||||
|
&i.PhoneNumber,
|
||||||
|
&i.Role,
|
||||||
|
&i.EmailVerified,
|
||||||
|
&i.PhoneVerified,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UpdatePassword = `-- name: UpdatePassword :exec
|
||||||
|
UPDATE users
|
||||||
|
SET password = $1, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE (email = $2 OR phone_number = $3)
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdatePasswordParams struct {
|
||||||
|
Password []byte
|
||||||
|
Email pgtype.Text
|
||||||
|
PhoneNumber pgtype.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, UpdatePassword, arg.Password, arg.Email, arg.PhoneNumber)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const UpdateUser = `-- name: UpdateUser :exec
|
const UpdateUser = `-- name: UpdateUser :exec
|
||||||
UPDATE users SET first_name = $2, last_name = $3, email = $4, phone_number = $5, password = $6, role = $7, verified = $8, updated_at = CURRENT_TIMESTAMP WHERE id = $1
|
UPDATE users
|
||||||
|
SET first_name = $1, last_name = $2, email = $3, phone_number = $4, role = $5, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $6
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateUserParams struct {
|
type UpdateUserParams struct {
|
||||||
ID int64
|
|
||||||
FirstName string
|
FirstName string
|
||||||
LastName string
|
LastName string
|
||||||
Email string
|
Email pgtype.Text
|
||||||
PhoneNumber string
|
PhoneNumber pgtype.Text
|
||||||
Password string
|
|
||||||
Role string
|
Role string
|
||||||
Verified pgtype.Bool
|
ID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
||||||
_, err := q.db.Exec(ctx, UpdateUser,
|
_, err := q.db.Exec(ctx, UpdateUser,
|
||||||
arg.ID,
|
|
||||||
arg.FirstName,
|
arg.FirstName,
|
||||||
arg.LastName,
|
arg.LastName,
|
||||||
arg.Email,
|
arg.Email,
|
||||||
arg.PhoneNumber,
|
arg.PhoneNumber,
|
||||||
arg.Password,
|
|
||||||
arg.Role,
|
arg.Role,
|
||||||
arg.Verified,
|
arg.ID,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
go.mod
40
go.mod
|
|
@ -4,33 +4,51 @@ go 1.24.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.13.2
|
github.com/bytedance/sonic v1.13.2
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0
|
||||||
github.com/gofiber/fiber/v2 v2.52.6
|
github.com/gofiber/fiber/v2 v2.52.6
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/jackc/pgx/v5 v5.7.4
|
github.com/jackc/pgx/v5 v5.7.4
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/swaggo/fiber-swagger v1.3.0
|
||||||
|
github.com/swaggo/swag v1.16.4
|
||||||
|
golang.org/x/crypto v0.36.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.1 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/stretchr/testify v1.8.4 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
github.com/valyala/fasthttp v1.59.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||||
golang.org/x/crypto v0.32.0 // indirect
|
golang.org/x/net v0.38.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.12.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
golang.org/x/tools v0.31.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
172
go.sum
172
go.sum
|
|
@ -1,5 +1,12 @@
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
|
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||||
|
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
|
@ -8,13 +15,44 @@ github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFos
|
||||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||||
|
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||||
|
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||||
|
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||||
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
|
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||||
|
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY=
|
||||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
|
@ -25,54 +63,144 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
||||||
|
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||||
|
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||||
|
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||||
|
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/swaggo/fiber-swagger v1.3.0 h1:RMjIVDleQodNVdKuu7GRs25Eq8RVXK7MwY9f5jbobNg=
|
||||||
|
github.com/swaggo/fiber-swagger v1.3.0/go.mod h1:18MuDqBkYEiUmeM/cAAB8CI28Bi62d/mys39j1QqF9w=
|
||||||
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc=
|
||||||
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||||
|
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
||||||
|
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||||
|
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
github.com/valyala/fasthttp v1.35.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
github.com/valyala/fasthttp v1.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
|
||||||
|
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||||
|
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
|
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||||
|
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||||
|
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,33 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidDbUrl = errors.New("db url is invalid")
|
ErrInvalidDbUrl = errors.New("db url is invalid")
|
||||||
ErrInvalidPort = errors.New("port number is invalid")
|
ErrInvalidPort = errors.New("port number is invalid")
|
||||||
|
ErrRefreshExpiry = errors.New("refresh token expiry is invalid")
|
||||||
|
ErrAccessExpiry = errors.New("access token expiry is invalid")
|
||||||
|
ErrInvalidJwtKey = errors.New("jwt key is invalid")
|
||||||
|
ErrLogLevel = errors.New("log level not set")
|
||||||
|
ErrInvalidLevel = errors.New("invalid log level")
|
||||||
|
ErrInvalidEnv = errors.New("env not set or invalid")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port int
|
Port int
|
||||||
DbUrl string
|
DbUrl string
|
||||||
|
RefreshExpiry int
|
||||||
|
AccessExpiry int
|
||||||
|
JwtKey string
|
||||||
|
LogLevel slog.Level
|
||||||
|
Env string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() (*Config, error) {
|
func NewConfig() (*Config, error) {
|
||||||
|
|
@ -24,7 +39,16 @@ func NewConfig() (*Config, error) {
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
func (c *Config) loadEnv() error {
|
func (c *Config) loadEnv() error {
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to load env file")
|
||||||
|
}
|
||||||
|
// env
|
||||||
|
env := os.Getenv("ENV")
|
||||||
|
if env == "" {
|
||||||
|
return ErrInvalidEnv
|
||||||
|
}
|
||||||
|
c.Env = env
|
||||||
portStr := os.Getenv("PORT")
|
portStr := os.Getenv("PORT")
|
||||||
port, err := strconv.Atoi(portStr)
|
port, err := strconv.Atoi(portStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -37,6 +61,33 @@ func (c *Config) loadEnv() error {
|
||||||
return ErrInvalidDbUrl
|
return ErrInvalidDbUrl
|
||||||
}
|
}
|
||||||
c.DbUrl = dbUrl
|
c.DbUrl = dbUrl
|
||||||
|
refreshExpiryStr := os.Getenv("REFRESH_EXPIRY")
|
||||||
|
refreshExpiry, err := strconv.Atoi(refreshExpiryStr)
|
||||||
|
if err != nil {
|
||||||
|
return ErrRefreshExpiry
|
||||||
|
}
|
||||||
|
c.RefreshExpiry = refreshExpiry
|
||||||
|
jwtKey := os.Getenv("JWT_KEY")
|
||||||
|
if jwtKey == "" {
|
||||||
|
return ErrInvalidJwtKey
|
||||||
|
}
|
||||||
|
c.JwtKey = jwtKey
|
||||||
|
accessExpiryStr := os.Getenv("ACCESS_EXPIRY")
|
||||||
|
accessExpiry, err := strconv.Atoi(accessExpiryStr)
|
||||||
|
if err != nil {
|
||||||
|
return ErrAccessExpiry
|
||||||
|
}
|
||||||
|
c.AccessExpiry = accessExpiry
|
||||||
|
// log level
|
||||||
|
logLevel := os.Getenv("LOG_LEVEL")
|
||||||
|
if logLevel == "" {
|
||||||
|
return ErrLogLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
lvl, ok := customlogger.LogLevels[logLevel]
|
||||||
|
if !ok {
|
||||||
|
return ErrInvalidLevel
|
||||||
|
}
|
||||||
|
c.LogLevel = lvl
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,12 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type RefreshToken struct {
|
||||||
|
ID int64
|
||||||
|
UserID int64
|
||||||
|
Token string
|
||||||
|
ExpiresAt time.Time
|
||||||
|
CreatedAt time.Time
|
||||||
|
Revoked bool
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,13 @@ func (m Currency) String() string {
|
||||||
x := float32(m)
|
x := float32(m)
|
||||||
x = x / 100
|
x = x / 100
|
||||||
return fmt.Sprintf("$%.2f", x)
|
return fmt.Sprintf("$%.2f", x)
|
||||||
|
|
||||||
|
}
|
||||||
|
type ValidString struct {
|
||||||
|
Value string
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
type ValidBool struct {
|
||||||
|
Value bool
|
||||||
|
Valid bool
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 (
|
||||||
|
RoleAdmin Role = "admin"
|
||||||
|
RoleCustomer Role = "customer"
|
||||||
|
RoleSuperAdmin Role = "super_admin"
|
||||||
|
RoleBranchManager Role = "branch_manager"
|
||||||
|
RoleCashier Role = "cashier"
|
||||||
|
)
|
||||||
|
|
@ -1,12 +1,53 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUserNotFound = errors.New("user not found")
|
||||||
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64
|
ID int64
|
||||||
FirstName string
|
FirstName string
|
||||||
LastName string
|
LastName string
|
||||||
Email string
|
Email string
|
||||||
PhoneNumber string
|
PhoneNumber string
|
||||||
Password string
|
Password []byte
|
||||||
Role string
|
Role Role
|
||||||
Verified bool
|
//
|
||||||
|
EmailVerified bool
|
||||||
|
PhoneVerified bool
|
||||||
|
//
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
//
|
||||||
|
SuspendedAt time.Time
|
||||||
|
Suspended bool
|
||||||
|
}
|
||||||
|
type RegisterUserReq struct {
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
Email string
|
||||||
|
PhoneNumber string
|
||||||
|
Password string
|
||||||
|
//Role string
|
||||||
|
Otp string
|
||||||
|
ReferalCode string
|
||||||
|
//
|
||||||
|
OtpMedium OtpMedium
|
||||||
|
}
|
||||||
|
type ResetPasswordReq struct {
|
||||||
|
Email string
|
||||||
|
PhoneNumber string
|
||||||
|
Password string
|
||||||
|
Otp string
|
||||||
|
OtpMedium OtpMedium
|
||||||
|
}
|
||||||
|
type UpdateUserReq struct {
|
||||||
|
FirstName ValidString
|
||||||
|
LastName ValidString
|
||||||
|
Suspended ValidBool
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,12 @@ var LogLevels = map[string]slog.Level{
|
||||||
"warn": slog.LevelWarn,
|
"warn": slog.LevelWarn,
|
||||||
"error": slog.LevelError,
|
"error": slog.LevelError,
|
||||||
}
|
}
|
||||||
|
var Environment = map[string]string{
|
||||||
|
"dev": "development",
|
||||||
|
"prod": "production",
|
||||||
|
}
|
||||||
|
|
||||||
func NewLogger(env string, lvl slog.Level, version string) *slog.Logger {
|
func NewLogger(env string, lvl slog.Level) *slog.Logger {
|
||||||
var logHandler slog.Handler
|
var logHandler slog.Handler
|
||||||
switch env {
|
switch env {
|
||||||
case "development":
|
case "development":
|
||||||
|
|
@ -28,7 +32,6 @@ func NewLogger(env string, lvl slog.Level, version string) *slog.Logger {
|
||||||
logger := slog.New(logHandler).With(slog.Group(
|
logger := slog.New(logHandler).With(slog.Group(
|
||||||
"service_info",
|
"service_info",
|
||||||
slog.String("env", env),
|
slog.String("env", env),
|
||||||
slog.String("version", version),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
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)
|
||||||
|
}
|
||||||
50
internal/repository/otp.go
Normal file
50
internal/repository/otp.go
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Store) CreateOtp(ctx context.Context, otp domain.Otp) error {
|
||||||
|
return s.queries.CreateOtp(ctx, dbgen.CreateOtpParams{
|
||||||
|
SentTo: otp.SentTo,
|
||||||
|
Medium: string(otp.Medium),
|
||||||
|
OtpFor: string(otp.For),
|
||||||
|
Otp: otp.Otp,
|
||||||
|
ExpiresAt: pgtype.Timestamptz{
|
||||||
|
Time: otp.ExpiresAt,
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func (s *Store) GetOtp(ctx context.Context, sentTo string, sentfor domain.OtpFor, medium domain.OtpMedium) (domain.Otp, error) {
|
||||||
|
row, err := s.queries.GetOtp(ctx, dbgen.GetOtpParams{
|
||||||
|
SentTo: sentTo,
|
||||||
|
Medium: string(medium),
|
||||||
|
OtpFor: string(sentfor),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return domain.Otp{}, domain.ErrOtpNotFound
|
||||||
|
}
|
||||||
|
return domain.Otp{}, err
|
||||||
|
}
|
||||||
|
return domain.Otp{
|
||||||
|
ID: row.ID,
|
||||||
|
SentTo: row.SentTo,
|
||||||
|
Medium: domain.OtpMedium(row.Medium),
|
||||||
|
For: domain.OtpFor(row.OtpFor),
|
||||||
|
Otp: row.Otp,
|
||||||
|
Used: row.Used,
|
||||||
|
UsedAt: row.UsedAt.Time,
|
||||||
|
CreatedAt: row.CreatedAt.Time,
|
||||||
|
ExpiresAt: row.ExpiresAt.Time,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
func (s *Store) MarkOtpAsUsed(ctx context.Context, otp domain.Otp) error {
|
||||||
|
return s.queries.MarkOtpAsUsed(ctx, otp.ID)
|
||||||
|
}
|
||||||
|
|
@ -2,47 +2,70 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Store) CreateUser(ctx context.Context, firstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error) {
|
func (s *Store) CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error) {
|
||||||
user, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{
|
err := s.queries.MarkOtpAsUsed(ctx, usedOtpId)
|
||||||
FirstName: firstName,
|
if err != nil {
|
||||||
LastName: lastName,
|
return domain.User{}, err
|
||||||
Email: email,
|
}
|
||||||
PhoneNumber: phoneNumber,
|
userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{
|
||||||
Password: password,
|
FirstName: user.FirstName,
|
||||||
Role: role,
|
LastName: user.LastName,
|
||||||
|
Email: pgtype.Text{
|
||||||
|
String: user.Email,
|
||||||
|
Valid: user.Email != "",
|
||||||
|
},
|
||||||
|
PhoneNumber: pgtype.Text{
|
||||||
|
String: user.PhoneNumber,
|
||||||
|
Valid: user.PhoneNumber != "",
|
||||||
|
},
|
||||||
|
Password: user.Password,
|
||||||
|
Role: string(user.Role),
|
||||||
|
EmailVerified: user.EmailVerified,
|
||||||
|
PhoneVerified: user.PhoneVerified,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.User{}, err
|
return domain.User{}, err
|
||||||
}
|
}
|
||||||
return domain.User{
|
return domain.User{
|
||||||
ID: user.ID,
|
ID: userRes.ID,
|
||||||
FirstName: user.FirstName,
|
FirstName: userRes.FirstName,
|
||||||
LastName: user.LastName,
|
LastName: userRes.LastName,
|
||||||
Email: user.Email,
|
Email: userRes.Email.String,
|
||||||
PhoneNumber: user.PhoneNumber,
|
PhoneNumber: userRes.PhoneNumber.String,
|
||||||
Password: user.Password,
|
Role: domain.Role(userRes.Role),
|
||||||
Role: user.Role,
|
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
|
func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
|
||||||
user, err := s.queries.GetUserByID(ctx, id)
|
user, err := s.queries.GetUserByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return domain.User{}, domain.ErrUserNotFound
|
||||||
|
}
|
||||||
return domain.User{}, err
|
return domain.User{}, err
|
||||||
}
|
}
|
||||||
return domain.User{
|
return domain.User{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
FirstName: user.FirstName,
|
FirstName: user.FirstName,
|
||||||
LastName: user.LastName,
|
LastName: user.LastName,
|
||||||
Email: user.Email,
|
Email: user.Email.String,
|
||||||
PhoneNumber: user.PhoneNumber,
|
PhoneNumber: user.PhoneNumber.String,
|
||||||
|
Role: domain.Role(user.Role),
|
||||||
|
EmailVerified: user.EmailVerified,
|
||||||
Password: user.Password,
|
Password: user.Password,
|
||||||
Role: user.Role,
|
PhoneVerified: user.PhoneVerified,
|
||||||
|
CreatedAt: user.CreatedAt.Time,
|
||||||
|
UpdatedAt: user.UpdatedAt.Time,
|
||||||
|
SuspendedAt: user.SuspendedAt.Time,
|
||||||
|
Suspended: user.Suspended,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||||
|
|
@ -50,32 +73,118 @@ func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var result []domain.User
|
userList := make([]domain.User, len(users))
|
||||||
for _, user := range users {
|
for i, user := range users {
|
||||||
result = append(result, domain.User{
|
userList[i] = domain.User{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
FirstName: user.FirstName,
|
FirstName: user.FirstName,
|
||||||
LastName: user.LastName,
|
LastName: user.LastName,
|
||||||
Email: user.Email,
|
Email: user.Email.String,
|
||||||
PhoneNumber: user.PhoneNumber,
|
PhoneNumber: user.PhoneNumber.String,
|
||||||
Password: user.Password,
|
Role: domain.Role(user.Role),
|
||||||
Role: user.Role,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return result, nil
|
}
|
||||||
|
return userList, nil
|
||||||
}
|
}
|
||||||
func (s *Store) UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error {
|
func (s *Store) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error {
|
||||||
err := s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
|
err := s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
|
||||||
ID: id,
|
// ID: user.ID,
|
||||||
FirstName: firstName,
|
// FirstName: user.FirstName,
|
||||||
LastName: lastName,
|
// LastName: user.LastName,
|
||||||
Email: email,
|
// Email: user.Email,
|
||||||
PhoneNumber: phoneNumber,
|
// PhoneNumber: user.PhoneNumber,
|
||||||
Password: password,
|
|
||||||
Role: role,
|
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
func (s *Store) DeleteUser(ctx context.Context, id int64) error {
|
func (s *Store) DeleteUser(ctx context.Context, id int64) error {
|
||||||
return s.queries.DeleteUser(ctx, id)
|
err := s.queries.DeleteUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *Store) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) {
|
||||||
|
fmt.Printf("phoneNum: %s, email: %s\n", phoneNum, email)
|
||||||
|
row, err := s.queries.CheckPhoneEmailExist(ctx, dbgen.CheckPhoneEmailExistParams{
|
||||||
|
PhoneNumber: pgtype.Text{
|
||||||
|
String: phoneNum,
|
||||||
|
Valid: phoneNum != "",
|
||||||
|
},
|
||||||
|
Email: pgtype.Text{
|
||||||
|
String: email,
|
||||||
|
|
||||||
|
Valid: email != "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
fmt.Printf("row: %+v\n", row)
|
||||||
|
if err != nil {
|
||||||
|
return false, false, err
|
||||||
|
}
|
||||||
|
return row.EmailExists, row.PhoneExists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetUserByEmail(ctx context.Context, email string) (domain.User, error) {
|
||||||
|
user, err := s.queries.GetUserByEmail(ctx, pgtype.Text{
|
||||||
|
String: email,
|
||||||
|
Valid: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return domain.User{}, domain.ErrUserNotFound
|
||||||
|
}
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
return domain.User{
|
||||||
|
ID: user.ID,
|
||||||
|
FirstName: user.FirstName,
|
||||||
|
LastName: user.LastName,
|
||||||
|
Email: user.Email.String,
|
||||||
|
PhoneNumber: user.PhoneNumber.String,
|
||||||
|
Role: domain.Role(user.Role),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
func (s *Store) GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error) {
|
||||||
|
user, err := s.queries.GetUserByPhone(ctx, pgtype.Text{
|
||||||
|
String: phoneNum,
|
||||||
|
Valid: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return domain.User{}, domain.ErrUserNotFound
|
||||||
|
}
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
return domain.User{
|
||||||
|
ID: user.ID,
|
||||||
|
FirstName: user.FirstName,
|
||||||
|
LastName: user.LastName,
|
||||||
|
Email: user.Email.String,
|
||||||
|
PhoneNumber: user.PhoneNumber.String,
|
||||||
|
Role: domain.Role(user.Role),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error {
|
||||||
|
err := s.queries.MarkOtpAsUsed(ctx, usedOtpId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.queries.UpdatePassword(ctx, dbgen.UpdatePasswordParams{
|
||||||
|
Password: password,
|
||||||
|
Email: pgtype.Text{
|
||||||
|
String: identifier,
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
PhoneNumber: pgtype.Text{
|
||||||
|
String: identifier,
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
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 {
|
type UserStore interface {
|
||||||
CreateUser(ctx context.Context, CfirstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error)
|
CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error)
|
||||||
GetUserByID(ctx context.Context, id int64) (domain.User, error)
|
GetUserByID(ctx context.Context, id int64) (domain.User, error)
|
||||||
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||||
UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error
|
UpdateUser(ctx context.Context, user domain.UpdateUserReq) error
|
||||||
DeleteUser(ctx context.Context, id int64) error
|
DeleteUser(ctx context.Context, id int64) error
|
||||||
|
CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error)
|
||||||
|
GetUserByEmail(ctx context.Context, email string) (domain.User, error)
|
||||||
|
GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error)
|
||||||
|
//
|
||||||
|
UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone
|
||||||
|
}
|
||||||
|
type SmsGateway interface {
|
||||||
|
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
|
||||||
|
}
|
||||||
|
type EmailGateway interface {
|
||||||
|
SendEmailOTP(ctx context.Context, email string, otp string) error
|
||||||
|
}
|
||||||
|
type OtpStore interface {
|
||||||
|
CreateOtp(ctx context.Context, otp domain.Otp) error
|
||||||
|
GetOtp(ctx context.Context, sentTo string, sentfor domain.OtpFor, medium domain.OtpMedium) (domain.Otp, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
79
internal/services/user/register.go
Normal file
79
internal/services/user/register.go
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
|
||||||
|
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
|
||||||
|
}
|
||||||
|
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error {
|
||||||
|
var err error
|
||||||
|
// check if user exists
|
||||||
|
switch medium {
|
||||||
|
case domain.OtpMediumEmail:
|
||||||
|
_, err = s.userStore.GetUserByEmail(ctx, sentTo)
|
||||||
|
case domain.OtpMediumSms:
|
||||||
|
_, err = s.userStore.GetUserByPhone(ctx, sentTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && err != domain.ErrUserNotFound {
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// send otp based on the medium
|
||||||
|
return s.SendOtp(ctx, sentTo, domain.OtpRegister, medium)
|
||||||
|
}
|
||||||
|
func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) { // normal
|
||||||
|
// get otp
|
||||||
|
fmt.Printf("registerReq: %+v\n", registerReq)
|
||||||
|
var sentTo string
|
||||||
|
if registerReq.OtpMedium == domain.OtpMediumEmail {
|
||||||
|
sentTo = registerReq.Email
|
||||||
|
} else {
|
||||||
|
sentTo = registerReq.PhoneNumber
|
||||||
|
}
|
||||||
|
//
|
||||||
|
otp, err := s.otpStore.GetOtp(
|
||||||
|
ctx, sentTo,
|
||||||
|
domain.OtpRegister, registerReq.OtpMedium)
|
||||||
|
if err != nil {
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
// verify otp
|
||||||
|
if otp.Used {
|
||||||
|
return domain.User{}, domain.ErrOtpAlreadyUsed
|
||||||
|
}
|
||||||
|
if time.Now().After(otp.ExpiresAt) {
|
||||||
|
return domain.User{}, domain.ErrOtpExpired
|
||||||
|
}
|
||||||
|
if otp.Otp != registerReq.Otp {
|
||||||
|
return domain.User{}, domain.ErrInvalidOtp
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := hashPassword(registerReq.Password)
|
||||||
|
if err != nil {
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
userR := domain.User{
|
||||||
|
FirstName: registerReq.FirstName,
|
||||||
|
LastName: registerReq.LastName,
|
||||||
|
Email: registerReq.Email,
|
||||||
|
PhoneNumber: registerReq.PhoneNumber,
|
||||||
|
Password: hashedPassword,
|
||||||
|
Role: "user",
|
||||||
|
EmailVerified: registerReq.OtpMedium == domain.OtpMediumEmail,
|
||||||
|
PhoneVerified: registerReq.OtpMedium == domain.OtpMediumSms,
|
||||||
|
}
|
||||||
|
// create the user and mark otp as used
|
||||||
|
user, err := s.userStore.CreateUser(ctx, userR, otp.ID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
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
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
const (
|
||||||
|
OtpExpiry = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
userStore UserStore
|
userStore UserStore
|
||||||
|
otpStore OtpStore
|
||||||
|
smsGateway SmsGateway
|
||||||
|
emailGateway EmailGateway
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(userStore UserStore) *Service {
|
func NewService(
|
||||||
|
userStore UserStore,
|
||||||
|
otpStore OtpStore, smsGateway SmsGateway,
|
||||||
|
emailGateway EmailGateway,
|
||||||
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
userStore: userStore,
|
userStore: userStore,
|
||||||
|
otpStore: otpStore,
|
||||||
|
smsGateway: smsGateway,
|
||||||
|
emailGateway: emailGateway,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreateUser(ctx context.Context, firstName, lastName, email, phoneNumber, password, role string, verified bool) (domain.User, error) {
|
|
||||||
return s.userStore.CreateUser(ctx, firstName, lastName, email, phoneNumber, password, role, verified)
|
|
||||||
}
|
|
||||||
func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
|
|
||||||
return s.userStore.GetUserByID(ctx, id)
|
|
||||||
}
|
|
||||||
func (s *Service) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
|
||||||
return s.userStore.GetAllUsers(ctx)
|
|
||||||
}
|
|
||||||
func (s *Service) UpdateUser(ctx context.Context, id int64, firstName, lastName, email, phoneNumber, password, role string, verified bool) error {
|
|
||||||
return s.userStore.UpdateUser(ctx, id, firstName, lastName, email, phoneNumber, password, role, verified)
|
|
||||||
}
|
|
||||||
func (s *Service) DeleteUser(ctx context.Context, id int64) error {
|
|
||||||
return s.userStore.DeleteUser(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
@ -2,17 +2,33 @@ package httpserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
|
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||||
|
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
fiber *fiber.App
|
fiber *fiber.App
|
||||||
|
logger *slog.Logger
|
||||||
port int
|
port int
|
||||||
|
authSvc *authentication.Service
|
||||||
|
userSvc *user.Service
|
||||||
|
validator *customvalidator.CustomValidator
|
||||||
|
JwtConfig jwtutil.JwtConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(port int) *App {
|
func NewApp(
|
||||||
|
port int, validator *customvalidator.CustomValidator,
|
||||||
|
authSvc *authentication.Service,
|
||||||
|
logger *slog.Logger,
|
||||||
|
JwtConfig jwtutil.JwtConfig,
|
||||||
|
userSvc *user.Service,
|
||||||
|
) *App {
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
CaseSensitive: true,
|
CaseSensitive: true,
|
||||||
DisableHeaderNormalizing: true,
|
DisableHeaderNormalizing: true,
|
||||||
|
|
@ -22,6 +38,11 @@ func NewApp(port int) *App {
|
||||||
s := &App{
|
s := &App{
|
||||||
fiber: app,
|
fiber: app,
|
||||||
port: port,
|
port: port,
|
||||||
|
authSvc: authSvc,
|
||||||
|
validator: validator,
|
||||||
|
logger: logger,
|
||||||
|
JwtConfig: JwtConfig,
|
||||||
|
userSvc: userSvc,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.initAppRoutes()
|
s.initAppRoutes()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package httpserver
|
|
||||||
|
|
||||||
func (a *App) initAppRoutes() {
|
|
||||||
// a.fiber.Group("/users", users.CreateAccount(a.userAPI))
|
|
||||||
}
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
38
internal/web_server/routes.go
Normal file
38
internal/web_server/routes.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package httpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/SamuelTariku/FortuneBet-Backend/docs"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/handlers"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
fiberSwagger "github.com/swaggo/fiber-swagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *App) initAppRoutes() {
|
||||||
|
a.fiber.Post("/auth/login", handlers.LoginCustomer(a.logger, a.authSvc, a.validator, a.JwtConfig))
|
||||||
|
a.fiber.Post("/auth/refresh", a.authMiddleware, handlers.RefreshToken(a.logger, a.authSvc, a.validator, a.JwtConfig))
|
||||||
|
a.fiber.Post("/auth/logout", a.authMiddleware, handlers.LogOutCustomer(a.logger, a.authSvc, a.validator))
|
||||||
|
a.fiber.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error {
|
||||||
|
userId := c.Locals("user_id")
|
||||||
|
role := c.Locals("role")
|
||||||
|
refreshToken := c.Locals("refresh_token")
|
||||||
|
a.logger.Info("User ID: " + userId.(string))
|
||||||
|
a.logger.Info("Role: " + role.(string))
|
||||||
|
a.logger.Info("Refresh Token: " + refreshToken.(string))
|
||||||
|
return c.SendString("Test endpoint")
|
||||||
|
})
|
||||||
|
a.fiber.Post("/user/resetPassword", handlers.ResetPassword(a.logger, a.userSvc, a.validator))
|
||||||
|
a.fiber.Post("/user/sendResetCode", handlers.SendResetCode(a.logger, a.userSvc, a.validator))
|
||||||
|
a.fiber.Post("/user/register", handlers.RegisterUser(a.logger, a.userSvc, a.validator))
|
||||||
|
a.fiber.Post("/user/sendRegisterCode", handlers.SendRegisterCode(a.logger, a.userSvc, a.validator))
|
||||||
|
a.fiber.Post("/user/checkPhoneEmailExist", handlers.CheckPhoneEmailExist(a.logger, a.userSvc, a.validator))
|
||||||
|
a.fiber.Get("/user/profile", a.authMiddleware, handlers.UserProfile(a.logger, a.userSvc))
|
||||||
|
// Swagger
|
||||||
|
a.fiber.Get("/swagger/*", fiberSwagger.WrapHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
///user/profile get
|
||||||
|
// @Router /user/resetPassword [post]
|
||||||
|
// @Router /user/sendResetCode [post]
|
||||||
|
// @Router /user/register [post]
|
||||||
|
// @Router /user/sendRegisterCode [post]
|
||||||
|
// @Router /user/checkPhoneEmailExist [post]
|
||||||
|
|
@ -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 ""
|
||||||
|
}
|
||||||
|
|
|
||||||
10
makefile
10
makefile
|
|
@ -12,11 +12,11 @@ build:
|
||||||
go build -ldflags="-s" -o ./bin/web ./
|
go build -ldflags="-s" -o ./bin/web ./
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
run:
|
run:
|
||||||
echo "Running Go application"; \
|
@echo "Running Go application"; \
|
||||||
go run ./cmd/main.go
|
go run ./cmd/main.go
|
||||||
.PHONY: air
|
.PHONY: air
|
||||||
air:
|
air:
|
||||||
echo "Running air"; \
|
@echo "Running air"; \
|
||||||
air -c .air.toml
|
air -c .air.toml
|
||||||
.PHONY: migrations/up
|
.PHONY: migrations/up
|
||||||
migrations/new:
|
migrations/new:
|
||||||
|
|
@ -30,3 +30,9 @@ migrations/up:
|
||||||
.PHONY: swagger
|
.PHONY: swagger
|
||||||
swagger:
|
swagger:
|
||||||
swag init -g cmd/main.go
|
swag init -g cmd/main.go
|
||||||
|
.PHONY: db-up
|
||||||
|
db-up:
|
||||||
|
docker compose -f compose.db.yaml up
|
||||||
|
.PHONY: db-down
|
||||||
|
db-down:
|
||||||
|
docker compose -f compose.db.yaml down
|
||||||
Loading…
Reference in New Issue
Block a user