From c8b215cc43f88057878e993ba76a9384439252d1 Mon Sep 17 00:00:00 2001 From: lafetz Date: Sat, 29 Mar 2025 06:25:19 +0300 Subject: [PATCH] impl auth http layer --- cmd/main.go | 41 ++- db/migrations/000001_fortune.up.sql | 30 ++- db/query/auth.sql | 16 ++ docs/docs.go | 285 ++++++++++++++++++++ docs/swagger.json | 259 ++++++++++++++++++ docs/swagger.yaml | 172 ++++++++++++ gen/db/auth.sql.go | 94 +++++++ gen/db/models.go | 13 +- go.mod | 40 ++- go.sum | 165 ++++++++++-- internal/config/config.go | 61 ++++- internal/logger/logger.go | 7 +- internal/repository/auth.go | 71 +++++ internal/services/authentication/impl.go | 49 ++-- internal/services/authentication/port.go | 2 +- internal/services/authentication/service.go | 10 +- internal/web_server/app.go | 30 ++- internal/web_server/dto/user.go | 6 + internal/web_server/jwt/jwt.go | 6 +- internal/web_server/jwt/jwt_test.go | 2 +- internal/web_server/middleware.go | 43 +++ internal/web_server/response/res.go | 47 ++++ internal/web_server/routes.go | 20 +- internal/web_server/user_handler.go | 170 ++++++++++++ internal/web_server/validator/validatord.go | 66 ++++- load_env.sh | 16 ++ makefile | 4 +- 27 files changed, 1621 insertions(+), 104 deletions(-) create mode 100644 db/query/auth.sql create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 gen/db/auth.sql.go create mode 100644 internal/repository/auth.go create mode 100644 internal/web_server/dto/user.go create mode 100644 internal/web_server/middleware.go create mode 100644 internal/web_server/response/res.go create mode 100644 internal/web_server/user_handler.go create mode 100644 load_env.sh diff --git a/cmd/main.go b/cmd/main.go index 7899d54..451f9d4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,16 +6,29 @@ import ( "os" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" - "github.com/joho/godotenv" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" + httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server" + 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() { - err := godotenv.Load() - if err != nil { - slog.Error(err.Error()) - os.Exit(1) - } + cfg, err := config.NewConfig() if err != nil { slog.Error(err.Error()) @@ -23,9 +36,21 @@ func main() { } db, _, err := repository.OpenDB(cfg.DbUrl) if err != nil { - fmt.Print(err) + fmt.Print("db", err) os.Exit(1) } + logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel) store := repository.NewStore(db) - fmt.Println(store) + v := customvalidator.NewCustomValidator(validator.New()) + authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) + app := httpserver.NewApp(cfg.Port, v, authSvc, logger, httpserver.JwtConfig{ + JwtAccessKey: cfg.JwtKey, + JwtAccessExpiry: cfg.AccessExpiry, + }) + logger.Info("Starting server", "port", cfg.Port) + if err := app.Run(); err != nil { + logger.Error("Failed to start server", "error", err) + os.Exit(1) + } + } diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index b1df3e1..69b5959 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -7,6 +7,32 @@ CREATE TABLE users ( password BYTEA NOT NULL, role VARCHAR(50) NOT NULL, verified BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP , - updated_at TIMESTAMP + created_at TIMESTAMPTZ , + updated_at TIMESTAMPTZ , + CONSTRAINT unique_email_phone UNIQUE (email, phone_number) +); +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) +); +----------------------------------------------seed data------------------------------------------------------------- +-------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- + +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +INSERT INTO users (first_name, last_name, email, phone_number, password, role, created_at, updated_at) +VALUES ( + 'John', + 'Doe', + 'john.doe@example.com', + '1234567890', + crypt('password123', gen_salt('bf'))::bytea, + 'user', + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP ); \ No newline at end of file diff --git a/db/query/auth.sql b/db/query/auth.sql new file mode 100644 index 0000000..71e45ec --- /dev/null +++ b/db/query/auth.sql @@ -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; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..28a683c --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,285 @@ +// 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/httpserver.loginCustomerReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpserver.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/httpserver.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/httpserver.refreshToken" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpserver.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" + } + } + } + } + } + }, + "definitions": { + "httpserver.loginCustomerReq": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "john.doe@example.com" + }, + "password": { + "type": "string", + "example": "password123" + }, + "phone_number": { + "type": "string", + "example": "1234567890" + } + } + }, + "httpserver.loginCustomerRes": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, + "httpserver.logoutReq": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "httpserver.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) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..07db1f3 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,259 @@ +{ + "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/httpserver.loginCustomerReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpserver.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/httpserver.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/httpserver.refreshToken" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpserver.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" + } + } + } + } + } + }, + "definitions": { + "httpserver.loginCustomerReq": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "john.doe@example.com" + }, + "password": { + "type": "string", + "example": "password123" + }, + "phone_number": { + "type": "string", + "example": "1234567890" + } + } + }, + "httpserver.loginCustomerRes": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, + "httpserver.logoutReq": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "httpserver.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" + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..81f777c --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,172 @@ +definitions: + httpserver.loginCustomerReq: + properties: + email: + example: john.doe@example.com + type: string + password: + example: password123 + type: string + phone_number: + example: "1234567890" + type: string + type: object + httpserver.loginCustomerRes: + properties: + access_token: + type: string + refresh_token: + type: string + type: object + httpserver.logoutReq: + properties: + refresh_token: + type: string + type: object + httpserver.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/httpserver.loginCustomerReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpserver.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/httpserver.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/httpserver.refreshToken' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpserver.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 +securityDefinitions: + Bearer: + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go new file mode 100644 index 0000000..7ad4b74 --- /dev/null +++ b/gen/db/auth.sql.go @@ -0,0 +1,94 @@ +// 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, password, role, verified, created_at, updated_at FROM users +WHERE email = $1 OR phone_number = $2 +` + +type GetUserByEmailPhoneParams struct { + Email string + PhoneNumber string +} + +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.Password, + &i.Role, + &i.Verified, + &i.CreatedAt, + &i.UpdatedAt, + ) + 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 +} diff --git a/gen/db/models.go b/gen/db/models.go index 82db472..b32f097 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -8,6 +8,15 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +type RefreshToken struct { + ID int64 + UserID int64 + Token string + ExpiresAt pgtype.Timestamptz + CreatedAt pgtype.Timestamptz + Revoked bool +} + type User struct { ID int64 FirstName string @@ -17,6 +26,6 @@ type User struct { Password []byte Role string Verified pgtype.Bool - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz } diff --git a/go.mod b/go.mod index 73cdf76..2fb3275 100644 --- a/go.mod +++ b/go.mod @@ -4,39 +4,51 @@ go 1.24.1 require ( github.com/bytedance/sonic v1.13.2 + github.com/go-playground/validator/v10 v10.26.0 github.com/gofiber/fiber/v2 v2.52.6 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/jackc/pgx/v5 v5.7.4 github.com/joho/godotenv v1.5.1 - github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.32.0 + github.com/swaggo/fiber-swagger v1.3.0 + github.com/swaggo/swag v1.16.4 + golang.org/x/crypto v0.36.0 ) require ( - github.com/andybalholm/brotli v1.1.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect - github.com/davecgh/go-spew v1.1.1 // 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/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.51.0 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect + github.com/valyala/fasthttp v1.59.0 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/tools v0.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d53d292..c86e5af 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,12 @@ -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -8,16 +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/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -28,62 +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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggo/fiber-swagger v1.3.0 h1:RMjIVDleQodNVdKuu7GRs25Eq8RVXK7MwY9f5jbobNg= +github.com/swaggo/fiber-swagger v1.3.0/go.mod h1:18MuDqBkYEiUmeM/cAAB8CI28Bi62d/mys39j1QqF9w= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= -github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/fasthttp v1.35.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= +github.com/valyala/fasthttp v1.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= +github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= +github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/internal/config/config.go b/internal/config/config.go index 0d53448..229bd47 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,18 +2,33 @@ package config import ( "errors" + "log/slog" "os" "strconv" + + customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger" + "github.com/joho/godotenv" ) var ( - ErrInvalidDbUrl = errors.New("db url is invalid") - ErrInvalidPort = errors.New("port number is invalid") + ErrInvalidDbUrl = errors.New("db url is invalid") + ErrInvalidPort = errors.New("port number is invalid") + ErrRefreshExpiry = errors.New("refresh token expiry is invalid") + ErrAccessExpiry = errors.New("access token expiry is invalid") + ErrInvalidJwtKey = errors.New("jwt key is invalid") + ErrLogLevel = errors.New("log level not set") + ErrInvalidLevel = errors.New("invalid log level") + ErrInvalidEnv = errors.New("env not set or invalid") ) type Config struct { - Port int - DbUrl string + Port int + DbUrl string + RefreshExpiry int + AccessExpiry int + JwtKey string + LogLevel slog.Level + Env string } func NewConfig() (*Config, error) { @@ -24,7 +39,16 @@ func NewConfig() (*Config, error) { return config, nil } func (c *Config) loadEnv() error { - + err := godotenv.Load() + if err != nil { + return errors.New("failed to load env file") + } + // env + env := os.Getenv("ENV") + if env == "" { + return ErrInvalidEnv + } + c.Env = env portStr := os.Getenv("PORT") port, err := strconv.Atoi(portStr) if err != nil { @@ -37,6 +61,33 @@ func (c *Config) loadEnv() error { return ErrInvalidDbUrl } c.DbUrl = dbUrl + refreshExpiryStr := os.Getenv("REFRESH_EXPIRY") + refreshExpiry, err := strconv.Atoi(refreshExpiryStr) + if err != nil { + return ErrRefreshExpiry + } + c.RefreshExpiry = refreshExpiry + jwtKey := os.Getenv("JWT_KEY") + if jwtKey == "" { + return ErrInvalidJwtKey + } + c.JwtKey = jwtKey + accessExpiryStr := os.Getenv("ACCESS_EXPIRY") + accessExpiry, err := strconv.Atoi(accessExpiryStr) + if err != nil { + return ErrAccessExpiry + } + c.AccessExpiry = accessExpiry + // log level + logLevel := os.Getenv("LOG_LEVEL") + if logLevel == "" { + return ErrLogLevel + } + lvl, ok := customlogger.LogLevels[logLevel] + if !ok { + return ErrInvalidLevel + } + c.LogLevel = lvl return nil } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 2c7035f..043836c 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -11,8 +11,12 @@ var LogLevels = map[string]slog.Level{ "warn": slog.LevelWarn, "error": slog.LevelError, } +var Environment = map[string]string{ + "dev": "development", + "prod": "production", +} -func NewLogger(env string, lvl slog.Level, version string) *slog.Logger { +func NewLogger(env string, lvl slog.Level) *slog.Logger { var logHandler slog.Handler switch env { case "development": @@ -28,7 +32,6 @@ func NewLogger(env string, lvl slog.Level, version string) *slog.Logger { logger := slog.New(logHandler).With(slog.Group( "service_info", slog.String("env", env), - slog.String("version", version), ), ) diff --git a/internal/repository/auth.go b/internal/repository/auth.go new file mode 100644 index 0000000..663e5b3 --- /dev/null +++ b/internal/repository/auth.go @@ -0,0 +1,71 @@ +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: email, + PhoneNumber: phone, + }) + 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, + PhoneNumber: user.PhoneNumber, + Password: user.Password, + 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) +} diff --git a/internal/services/authentication/impl.go b/internal/services/authentication/impl.go index 1bac295..7447312 100644 --- a/internal/services/authentication/impl.go +++ b/internal/services/authentication/impl.go @@ -18,23 +18,25 @@ var ( ErrRefreshTokenNotFound = errors.New("refresh token not found") // i.e login again ) -func (s *Service) Login(ctx context.Context, emailPhone EmailPhone, password string) (string, error) { - user, err := s.userStore.GetUserByEmailPhone(ctx, emailPhone) +type LoginSuccess struct { + UserId int64 + Role string + 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 "", err + return LoginSuccess{}, err } err = matchPassword(password, user.Password) if err != nil { - return "", err + return LoginSuccess{}, err } - // //create session - // accessToken, err := CreateJwt(strconv.Itoa(int(user.ID)), s.jwtConfig.JwtAccessKey, s.jwtConfig.JwtAccessExpiry) - // if err != nil { - // return Tokens{}, err - // } + refreshToken, err := generateRefreshToken() if err != nil { - return "", err + return LoginSuccess{}, err } err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{ Token: refreshToken, @@ -43,19 +45,17 @@ func (s *Service) Login(ctx context.Context, emailPhone EmailPhone, password str ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second), }) if err != nil { - return "", err + return LoginSuccess{}, err } - return refreshToken, nil + return LoginSuccess{ + UserId: user.ID, + Role: user.Role, + RfToken: refreshToken, + }, nil } func (s *Service) RefreshToken(ctx context.Context, refToken string) (string, error) { - // us, err := ParseJwt(tokens.RefreshToken, s.jwtConfig.JwtAccessKey) - // if err == nil { - // return Tokens{}, err - // } - // if !errors.Is(err, ErrExpiredToken) { - // return Tokens{}, err - // } + token, err := s.tokenStore.GetRefreshToken(ctx, refToken) if err != nil { return "", err @@ -66,21 +66,12 @@ func (s *Service) RefreshToken(ctx context.Context, refToken string) (string, er if token.ExpiresAt.Before(time.Now()) { return "", ErrExpiredToken } - // - // naccessToken, err := CreateJwt(token., s.jwtConfig.JwtAccessKey, s.jwtConfig.JwtAccessExpiry) - // if err != nil { - // return Tokens{}, err - // } + newRefToken, err := generateRefreshToken() if err != nil { return "", err } - // ntokens := Tokens{ - // AccessToken: naccessToken, - // RefreshToken: nrefreshToken, - // } - err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{ Token: newRefToken, UserID: token.UserID, diff --git a/internal/services/authentication/port.go b/internal/services/authentication/port.go index a9f3136..d177dbe 100644 --- a/internal/services/authentication/port.go +++ b/internal/services/authentication/port.go @@ -7,7 +7,7 @@ import ( ) type UserStore interface { - GetUserByEmailPhone(ctx context.Context, emailPhone EmailPhone) (domain.User, error) + GetUserByEmailPhone(ctx context.Context, email, phone string) (domain.User, error) } type TokenStore interface { CreateRefreshToken(ctx context.Context, rt domain.RefreshToken) error diff --git a/internal/services/authentication/service.go b/internal/services/authentication/service.go index e40b75b..577e9da 100644 --- a/internal/services/authentication/service.go +++ b/internal/services/authentication/service.go @@ -1,10 +1,10 @@ package authentication -type EmailPhone struct { - Email ValidString - PhoneNumber ValidString - Password ValidString -} +// type EmailPhone struct { +// Email ValidString +// PhoneNumber ValidString +// Password ValidString +// } type ValidString struct { Value string Valid bool diff --git a/internal/web_server/app.go b/internal/web_server/app.go index b7da149..3672a2c 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -2,17 +2,33 @@ package httpserver import ( "fmt" + "log/slog" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" + customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "github.com/bytedance/sonic" "github.com/gofiber/fiber/v2" ) +type JwtConfig struct { + JwtAccessKey string + JwtAccessExpiry int +} type App struct { - fiber *fiber.App - port int + fiber *fiber.App + logger *slog.Logger + port int + authSvc *authentication.Service + validator *customvalidator.CustomValidator + JwtConfig JwtConfig } -func NewApp(port int) *App { +func NewApp( + port int, validator *customvalidator.CustomValidator, + authSvc *authentication.Service, + logger *slog.Logger, + JwtConfig JwtConfig, +) *App { app := fiber.New(fiber.Config{ CaseSensitive: true, DisableHeaderNormalizing: true, @@ -20,8 +36,12 @@ func NewApp(port int) *App { JSONDecoder: sonic.Unmarshal, }) s := &App{ - fiber: app, - port: port, + fiber: app, + port: port, + authSvc: authSvc, + validator: validator, + logger: logger, + JwtConfig: JwtConfig, } s.initAppRoutes() diff --git a/internal/web_server/dto/user.go b/internal/web_server/dto/user.go new file mode 100644 index 0000000..7708f59 --- /dev/null +++ b/internal/web_server/dto/user.go @@ -0,0 +1,6 @@ +package dto + +var loginCustomerRes struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} diff --git a/internal/web_server/jwt/jwt.go b/internal/web_server/jwt/jwt.go index 4dd89e1..7dc3659 100644 --- a/internal/web_server/jwt/jwt.go +++ b/internal/web_server/jwt/jwt.go @@ -1,4 +1,4 @@ -package user +package jwtutil import ( "errors" @@ -20,15 +20,17 @@ var ( type UserClaim struct { jwt.RegisteredClaims UserId string + Role string } -func CreateJwt(userId string, key string, expiry int) (string, error) { +func CreateJwt(userId string, Role string, 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 diff --git a/internal/web_server/jwt/jwt_test.go b/internal/web_server/jwt/jwt_test.go index 45d1dcc..bffe0c5 100644 --- a/internal/web_server/jwt/jwt_test.go +++ b/internal/web_server/jwt/jwt_test.go @@ -1,4 +1,4 @@ -package user +package jwtutil // func TestCreateJwt(t *testing.T) { // // Define a user to test diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go new file mode 100644 index 0000000..e80689f --- /dev/null +++ b/internal/web_server/middleware.go @@ -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() +} diff --git a/internal/web_server/response/res.go b/internal/web_server/response/res.go new file mode 100644 index 0000000..593758d --- /dev/null +++ b/internal/web_server/response/res.go @@ -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) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 8388c84..e54fc9c 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -1,5 +1,23 @@ package httpserver +import ( + _ "github.com/SamuelTariku/FortuneBet-Backend/docs" + "github.com/gofiber/fiber/v2" + fiberSwagger "github.com/swaggo/fiber-swagger" +) + func (a *App) initAppRoutes() { - // a.fiber.Group("/users", users.CreateAccount(a.userAPI)) + a.fiber.Post("/auth/login", a.LoginCustomer) + a.fiber.Post("/auth/refresh", a.authMiddleware, a.RefreshToken) + a.fiber.Post("/auth/logout", a.authMiddleware, a.LogOutCustomer) + 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.Get("/swagger/*", fiberSwagger.WrapHandler) } diff --git a/internal/web_server/user_handler.go b/internal/web_server/user_handler.go new file mode 100644 index 0000000..199eadb --- /dev/null +++ b/internal/web_server/user_handler.go @@ -0,0 +1,170 @@ +package httpserver + +import ( + "errors" + "strconv" + + "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" + "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 (a *App) LoginCustomer(c *fiber.Ctx) error { + var req loginCustomerReq + if err := c.BodyParser(&req); err != nil { + a.logger.Error("Login failed", "error", err) + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) + } + valErrs, ok := a.validator.Validate(c, req) + if !ok { + + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + successRes, err := a.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password) + if err != nil { + a.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 + } + a.logger.Error("Login failed", "error", err) + response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil) + return nil + + } + accessToken, err := jwtutil.CreateJwt(strconv.Itoa(int(successRes.UserId)), successRes.Role, a.JwtConfig.JwtAccessKey, a.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 (a *App) RefreshToken(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 := a.validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + rf, err := a.authSvc.RefreshToken(c.Context(), req.RefreshToken) + if err != nil { + a.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 + } + a.logger.Error("Refresh token failed", "error", err) + response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil) + return nil + } + accessToken, err := jwtutil.CreateJwt("", "", a.JwtConfig.JwtAccessKey, a.JwtConfig.JwtAccessExpiry) + if err != nil { + a.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 (a *App) LogOutCustomer(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 := a.validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + err := a.authSvc.Logout(c.Context(), req.RefreshToken) + if err != nil { + a.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 + } + a.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) +} diff --git a/internal/web_server/validator/validatord.go b/internal/web_server/validator/validatord.go index 47f2da9..16e13e9 100644 --- a/internal/web_server/validator/validatord.go +++ b/internal/web_server/validator/validatord.go @@ -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 "" +} diff --git a/load_env.sh b/load_env.sh new file mode 100644 index 0000000..761d37d --- /dev/null +++ b/load_env.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +dotenv() { + if [ -f .env ]; then + while IFS='=' read -r key value; do + if [[ ! -z "$key" && "$key" != \#* ]]; then + export "$key=$value" + fi + done < .env + else + echo ".env file not found." + fi +} + + +dotenv \ No newline at end of file diff --git a/makefile b/makefile index 845b5e7..6fad4c7 100644 --- a/makefile +++ b/makefile @@ -12,11 +12,11 @@ build: go build -ldflags="-s" -o ./bin/web ./ .PHONY: run run: - echo "Running Go application"; \ + @echo "Running Go application"; \ go run ./cmd/main.go .PHONY: air air: - echo "Running air"; \ + @echo "Running air"; \ air -c .air.toml .PHONY: migrations/up migrations/new: