Compare commits
27 Commits
main
...
feat/DEZON
| Author | SHA1 | Date | |
|---|---|---|---|
| f0914bcad9 | |||
| 7a365f57ec | |||
| e3d37a8e4d | |||
| c27e3945d5 | |||
| b2d5659aec | |||
| 5829b471d5 | |||
| 36cda3fd5a | |||
| 75e1b5111e | |||
| 2da912739b | |||
| 667d2ce2f4 | |||
| 68d33d5ad9 | |||
| e3d2ceb808 | |||
| 7d7f54261b | |||
| 406b181576 | |||
| d97646adfd | |||
| 5ae1263c7e | |||
| 5265732f39 | |||
| 6bf6a8ffc2 | |||
| dee9ce2f64 | |||
| f4e5e074d3 | |||
| 5c5df48644 | |||
| 6d16a6ea8c | |||
| 723d883bc2 | |||
| 7e471fd8d9 | |||
| c3c7e908f0 | |||
| 9bade56794 | |||
| 0e32d0d820 |
51
.air.toml
Normal file
51
.air.toml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main cmd/api/main.go "
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[proxy]
|
||||||
|
app_port = 0
|
||||||
|
enabled = false
|
||||||
|
proxy_port = 0
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
config.yaml
|
||||||
|
bin
|
||||||
|
tmp
|
||||||
23
cmd/api/main.go
Normal file
23
cmd/api/main.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/config"
|
||||||
|
"backend/internal/api/http"
|
||||||
|
"backend/internal/app"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := config.ReadStandard("config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appContainer, err := app.NewAppContainer(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize application: %v", err)
|
||||||
|
}
|
||||||
|
defer appContainer.Close()
|
||||||
|
|
||||||
|
http.Run(cfg.Server, appContainer)
|
||||||
|
}
|
||||||
49
config/config.go
Normal file
49
config/config.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Server Server `mapstructure:"server"`
|
||||||
|
JWT JWT `mapstructure:"jwt"`
|
||||||
|
DB DB `mapstructure:"db"`
|
||||||
|
Redis Redis `mapstructure:"redis"`
|
||||||
|
KYCProvider KYC `mapstructure:"kyc"`
|
||||||
|
OTP OTP `mapstructure:"otp"`
|
||||||
|
Kavenegar SMSProvider `mapstructure:"kavenegar"`
|
||||||
|
}
|
||||||
|
type Server struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
User string `mapstructure:"user"`
|
||||||
|
Pass string `mapstructure:"pass"`
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
DBName string `mapstructure:"db_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Redis struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
Pass string `mapstructure:"pass"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWT struct {
|
||||||
|
TokenExpMinutes uint `mapstructure:"token_exp_minutes"`
|
||||||
|
RefreshTokenExpMinutes uint `mapstructure:"refresh_token_exp_minutes"`
|
||||||
|
TokenSecret string `mapstructure:"token_secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KYC struct {
|
||||||
|
APIKey string `mapstructure:"api_key"`
|
||||||
|
URL string `mapstructure:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OTP struct {
|
||||||
|
CodeExpMinutes uint `mapstructure:"code_exp_minutes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SMSProvider struct {
|
||||||
|
APIKey string `mapstructure:"api_key"`
|
||||||
|
Template string `mapstructure:"template"`
|
||||||
|
}
|
||||||
51
config/read.go
Normal file
51
config/read.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReadGeneric[T any](cfgPath string) (T, error) {
|
||||||
|
var cfg T
|
||||||
|
fullAbsPath, err := absPath(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// configPath := filepath.Dir(fullAbsPath)
|
||||||
|
// viper.AddConfigPath(configPath)
|
||||||
|
// configType := strings.TrimPrefix(filepath.Ext(fullAbsPath), ".")
|
||||||
|
// viper.SetConfigType(configType)
|
||||||
|
// configFile := strings.TrimSuffix(filepath.Base(fullAbsPath), filepath.Ext(fullAbsPath))
|
||||||
|
// viper.SetConfigName(configFile)
|
||||||
|
|
||||||
|
viper.SetConfigFile(fullAbsPath)
|
||||||
|
|
||||||
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, viper.Unmarshal(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadStandard(cfgPath string) (Config, error) {
|
||||||
|
return ReadGeneric[Config](cfgPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func absPath(cfgPath string) (string, error) {
|
||||||
|
if !filepath.IsAbs(cfgPath) {
|
||||||
|
return filepath.Abs(cfgPath)
|
||||||
|
}
|
||||||
|
return cfgPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustReadStandard(configPath string) Config {
|
||||||
|
cfg, err := ReadStandard(configPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
367
docs/docs.go
Normal file
367
docs/docs.go
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
// 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}}",
|
||||||
|
"contact": {},
|
||||||
|
"version": "{{.Version}}"
|
||||||
|
},
|
||||||
|
"host": "{{.Host}}",
|
||||||
|
"basePath": "{{.BasePath}}",
|
||||||
|
"paths": {
|
||||||
|
"/auth/authenticate": {
|
||||||
|
"post": {
|
||||||
|
"description": "Authenticate user with wallet signature",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Authenticate user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Authentication Request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.AuthenticateRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.AuthenticateResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/challenge": {
|
||||||
|
"post": {
|
||||||
|
"description": "Generate a challenge message for wallet authentication",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Generate authentication challenge",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Challenge Request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ChallengeRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ChallengeResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/kyc": {
|
||||||
|
"post": {
|
||||||
|
"description": "Verify user KYC with national ID and birth date",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Verify user KYC",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "KYC Verify Request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.KYCVerifyRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/otp": {
|
||||||
|
"post": {
|
||||||
|
"description": "Verify the provided OTP code for the phone number",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Verify OTP code",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "OTP Verify Request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.OTPVerifyRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.OTPVerifyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"dto.APIResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.AuthenticateRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"pubKey",
|
||||||
|
"signature"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"pubKey": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"signature": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.AuthenticateResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"authorizationToken": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"refreshToken": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.ChallengeRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"pubKey"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"pubKey": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.ChallengeResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timeStamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.KYCVerifyRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"birthDate",
|
||||||
|
"nationalId"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"birthDate": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"nationalId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.OTPProviderReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"receptor": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.OTPProviderResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.OTPVerifyRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"code",
|
||||||
|
"phone"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.OTPVerifyResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||||
|
var SwaggerInfo = &swag.Spec{
|
||||||
|
Version: "",
|
||||||
|
Host: "",
|
||||||
|
BasePath: "",
|
||||||
|
Schemes: []string{},
|
||||||
|
Title: "",
|
||||||
|
Description: "",
|
||||||
|
InfoInstanceName: "swagger",
|
||||||
|
SwaggerTemplate: docTemplate,
|
||||||
|
LeftDelim: "{{",
|
||||||
|
RightDelim: "}}",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||||
|
}
|
||||||
338
docs/swagger.json
Normal file
338
docs/swagger.json
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
{
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"contact": {}
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/auth/authenticate": {
|
||||||
|
"post": {
|
||||||
|
"description": "Authenticate user with wallet signature",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Authenticate user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Authentication Request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.AuthenticateRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.AuthenticateResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/challenge": {
|
||||||
|
"post": {
|
||||||
|
"description": "Generate a challenge message for wallet authentication",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Generate authentication challenge",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Challenge Request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ChallengeRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ChallengeResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/kyc": {
|
||||||
|
"post": {
|
||||||
|
"description": "Verify user KYC with national ID and birth date",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Verify user KYC",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "KYC Verify Request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.KYCVerifyRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/otp": {
|
||||||
|
"post": {
|
||||||
|
"description": "Verify the provided OTP code for the phone number",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Verify OTP code",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "OTP Verify Request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.OTPVerifyRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.OTPVerifyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"dto.APIResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.AuthenticateRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"pubKey",
|
||||||
|
"signature"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"pubKey": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"signature": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.AuthenticateResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"authorizationToken": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"refreshToken": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.ChallengeRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"pubKey"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"pubKey": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.ChallengeResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timeStamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.KYCVerifyRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"birthDate",
|
||||||
|
"nationalId"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"birthDate": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"nationalId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.OTPProviderReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"receptor": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.OTPProviderResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.OTPVerifyRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"code",
|
||||||
|
"phone"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.OTPVerifyResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
219
docs/swagger.yaml
Normal file
219
docs/swagger.yaml
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
definitions:
|
||||||
|
dto.APIResponse:
|
||||||
|
properties:
|
||||||
|
data: {}
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
dto.AuthenticateRequest:
|
||||||
|
properties:
|
||||||
|
pubKey:
|
||||||
|
type: string
|
||||||
|
signature:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- pubKey
|
||||||
|
- signature
|
||||||
|
type: object
|
||||||
|
dto.AuthenticateResponse:
|
||||||
|
properties:
|
||||||
|
authorizationToken:
|
||||||
|
type: string
|
||||||
|
expiresAt:
|
||||||
|
type: integer
|
||||||
|
refreshToken:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
dto.ChallengeRequest:
|
||||||
|
properties:
|
||||||
|
pubKey:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- pubKey
|
||||||
|
type: object
|
||||||
|
dto.ChallengeResponse:
|
||||||
|
properties:
|
||||||
|
expiresAt:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
timeStamp:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
dto.KYCVerifyRequest:
|
||||||
|
properties:
|
||||||
|
birthDate:
|
||||||
|
type: string
|
||||||
|
nationalId:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- birthDate
|
||||||
|
- nationalId
|
||||||
|
type: object
|
||||||
|
dto.OTPProviderReq:
|
||||||
|
properties:
|
||||||
|
receptor:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
dto.OTPProviderResponse:
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
dto.OTPVerifyRequest:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
phone:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
- phone
|
||||||
|
type: object
|
||||||
|
dto.OTPVerifyResponse:
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
info:
|
||||||
|
contact: {}
|
||||||
|
paths:
|
||||||
|
/auth/authenticate:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Authenticate user with wallet signature
|
||||||
|
parameters:
|
||||||
|
- description: Authentication Request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.AuthenticateRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.AuthenticateResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Authenticate user
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
/auth/challenge:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Generate a challenge message for wallet authentication
|
||||||
|
parameters:
|
||||||
|
- description: Challenge Request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ChallengeRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ChallengeResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Generate authentication challenge
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
/auth/kyc:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Verify user KYC with national ID and birth date
|
||||||
|
parameters:
|
||||||
|
- description: KYC Verify Request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.KYCVerifyRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.APIResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Verify user KYC
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
/auth/otp:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Verify the provided OTP code for the phone number
|
||||||
|
parameters:
|
||||||
|
- description: OTP Verify Request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.OTPVerifyRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.OTPVerifyResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Verify OTP code
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
swagger: "2.0"
|
||||||
73
go.mod
73
go.mod
@ -1,20 +1,81 @@
|
|||||||
module boiler-plate
|
module backend
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/ethereum/go-ethereum v1.16.3
|
||||||
|
github.com/gofiber/adaptor/v2 v2.2.1
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.9
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d
|
||||||
|
github.com/redis/go-redis/v9 v9.13.0
|
||||||
|
github.com/spf13/viper v1.21.0
|
||||||
|
github.com/swaggo/http-swagger v1.3.4
|
||||||
|
github.com/swaggo/swag v1.16.6
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.30.2
|
gorm.io/gorm v1.30.5
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/bits-and-blooms/bitset v1.20.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/consensys/gnark-crypto v0.18.0 // indirect
|
||||||
|
github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect
|
||||||
|
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
|
||||||
|
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect
|
||||||
|
github.com/ethereum/go-verkle v0.2.2 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.6 // indirect
|
||||||
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
|
github.com/holiman/uint256 v1.3.2 // 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/pgx/v5 v5.6.0 // indirect
|
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
golang.org/x/crypto v0.31.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
github.com/mailru/easyjson v0.7.6 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
|
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/supranational/blst v0.3.14 // indirect
|
||||||
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.6.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
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/crypto v0.40.0 // indirect
|
||||||
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
|
golang.org/x/net v0.42.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
golang.org/x/tools v0.35.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
310
go.sum
310
go.sum
@ -1,6 +1,138 @@
|
|||||||
|
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
||||||
|
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
||||||
|
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||||
|
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
|
||||||
|
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
|
||||||
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
|
||||||
|
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
|
||||||
|
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
|
||||||
|
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
|
||||||
|
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
|
||||||
|
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
|
||||||
|
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
|
||||||
|
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
|
||||||
|
github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
|
||||||
|
github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
|
||||||
|
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
|
||||||
|
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||||
|
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
|
||||||
|
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||||
|
github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0=
|
||||||
|
github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/crate-crypto/go-eth-kzg v1.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg/0E3OOdI=
|
||||||
|
github.com/crate-crypto/go-eth-kzg v1.3.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
|
||||||
|
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
|
||||||
|
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
|
||||||
|
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/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
|
||||||
|
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||||
|
github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0=
|
||||||
|
github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
|
||||||
|
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
|
||||||
|
github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w=
|
||||||
|
github.com/ethereum/c-kzg-4844/v2 v2.1.0/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E=
|
||||||
|
github.com/ethereum/go-ethereum v1.16.3 h1:nDoBSrmsrPbrDIVLTkDQCy1U9KdHN+F2PzvMbDoS42Q=
|
||||||
|
github.com/ethereum/go-ethereum v1.16.3/go.mod h1:Lrsc6bt9Gm9RyvhfFK53vboCia8kpF9nv+2Ukntnl+8=
|
||||||
|
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
|
||||||
|
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
|
||||||
|
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
|
||||||
|
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI=
|
||||||
|
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
|
||||||
|
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
|
||||||
|
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||||
|
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||||
|
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
|
||||||
|
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||||
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||||
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/gofiber/adaptor/v2 v2.2.1 h1:givE7iViQWlsTR4Jh7tB4iXzrlKBgiraB/yTdHs9Lv4=
|
||||||
|
github.com/gofiber/adaptor/v2 v2.2.1/go.mod h1:AhR16dEqs25W2FY/l8gSj1b51Azg5dtPDmm+pruNOrc=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||||
|
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||||
|
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
|
||||||
|
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0=
|
||||||
|
github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
|
||||||
|
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
|
||||||
|
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
|
||||||
|
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4=
|
||||||
|
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
|
||||||
|
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
|
||||||
|
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
|
||||||
|
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
|
||||||
|
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||||
|
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||||
|
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||||
|
github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k=
|
||||||
|
github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8=
|
||||||
|
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs=
|
||||||
|
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||||
|
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
|
||||||
|
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
|
||||||
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=
|
||||||
@ -9,28 +141,186 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
|||||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
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/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||||
|
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
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/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d h1:5yPyBSS28Nojbr7pAkiXADGj6VpTXx73o6SsprKbSoo=
|
||||||
|
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d/go.mod h1:CRhvvr4KNAyrg+ewrutOf+/QoHs7lztSoLjp+GqhYlA=
|
||||||
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
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/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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
|
||||||
|
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||||
|
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 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||||
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
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/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/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
|
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||||
|
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||||
|
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||||
|
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
|
||||||
|
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
|
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
|
||||||
|
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM=
|
||||||
|
github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
|
||||||
|
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||||
|
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||||
|
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||||
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
|
github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
|
||||||
|
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
|
||||||
|
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
|
||||||
|
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||||
|
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
||||||
|
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
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/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
|
||||||
|
github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
|
||||||
|
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||||
|
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||||
|
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||||
|
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||||
|
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||||
|
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||||
|
github.com/redis/go-redis/v9 v9.13.0 h1:PpmlVykE0ODh8P43U0HqC+2NXHXwG+GUtQyz+MPKGRg=
|
||||||
|
github.com/redis/go-redis/v9 v9.13.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
|
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||||
|
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
|
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
|
||||||
|
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
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/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.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||||
|
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
|
||||||
|
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
|
||||||
|
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||||
|
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||||
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
|
||||||
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
|
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||||
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
|
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||||
|
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||||
|
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/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
|
||||||
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
|
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||||
|
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||||
|
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||||
|
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||||
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
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=
|
||||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/gorm v1.30.2 h1:f7bevlVoVe4Byu3pmbWPVHnPsLoWaMjEb7/clyr9Ivs=
|
gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s=
|
||||||
gorm.io/gorm v1.30.2/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
|
|||||||
58
internal/api/dto/auth.go
Normal file
58
internal/api/dto/auth.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type ChallengeRequest struct {
|
||||||
|
PubKey string `json:"pubKey" validate:"required,eth_pubkey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChallengeResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
TimeStamp string `json:"timeStamp"`
|
||||||
|
ExpiresAt string `json:"expiresAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthenticateRequest struct {
|
||||||
|
PubKey string `json:"pubKey" validate:"required,eth_pubkey"`
|
||||||
|
Signature string `json:"signature" validate:"required,eth_signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthenticateResponse struct {
|
||||||
|
AuthorizationToken string `json:"authorizationToken"`
|
||||||
|
RefreshToken string `json:"refreshToken"`
|
||||||
|
ExpiresAt int64 `json:"expiresAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshTokenRequest struct {
|
||||||
|
RefreshToken string `json:"refreshToken" validate:"required,jwt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OTPProviderReq struct {
|
||||||
|
Receptor string `json:"receptor"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OTPProviderResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OTPVerifyRequest struct {
|
||||||
|
Phone string `json:"phone" validate:"required"`
|
||||||
|
Code string `json:"code" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OTPVerifyResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KYCVerifyRequest struct {
|
||||||
|
NationalID string `json:"nationalId" validate:"required"`
|
||||||
|
BirthDate string `json:"birthDate" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KYCVerifyResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data any `json:"data,omitempty"`
|
||||||
|
}
|
||||||
193
internal/api/http/handlers/auth_handler.go
Normal file
193
internal/api/http/handlers/auth_handler.go
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/internal/api/dto"
|
||||||
|
"backend/internal/api/http/middlewares"
|
||||||
|
"backend/internal/usecase"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthHandler struct {
|
||||||
|
authService usecase.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthHandler(authService *usecase.AuthService) *AuthHandler {
|
||||||
|
return &AuthHandler{
|
||||||
|
authService: *authService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateChallenge generates a challenge for authentication
|
||||||
|
// @Summary Generate authentication challenge
|
||||||
|
// @Description Generate a challenge message for wallet authentication
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.ChallengeRequest true "Challenge Request"
|
||||||
|
// @Success 200 {object} dto.ChallengeResponse
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /auth/challenge [post]
|
||||||
|
func (h *AuthHandler) GenerateChallenge(c *fiber.Ctx) error {
|
||||||
|
var req dto.ChallengeRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "invalid request body",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
challenge, err := h.authService.GenerateChallenge(c.Context(), req.PubKey)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(
|
||||||
|
dto.ChallengeResponse{
|
||||||
|
Message: challenge.Message,
|
||||||
|
TimeStamp: challenge.TimeStamp.String(),
|
||||||
|
ExpiresAt: challenge.ExpiresAt.String(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate authenticates a user with signed challenge
|
||||||
|
// @Summary Authenticate user
|
||||||
|
// @Description Authenticate user with wallet signature
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.AuthenticateRequest true "Authentication Request"
|
||||||
|
// @Success 200 {object} dto.AuthenticateResponse
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 401 {object} map[string]string
|
||||||
|
// @Router /auth/authenticate [post]
|
||||||
|
func (h *AuthHandler) Authenticate(c *fiber.Ctx) error {
|
||||||
|
var req dto.AuthenticateRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "invalid request body",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
userToken, err := h.authService.Authenticate(
|
||||||
|
c.Context(),
|
||||||
|
req.PubKey,
|
||||||
|
req.Signature,
|
||||||
|
//TODO: add chainID to cfg
|
||||||
|
1,
|
||||||
|
c.IP(),
|
||||||
|
c.Get("User-Agent"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(dto.AuthenticateResponse{
|
||||||
|
AuthorizationToken: userToken.AuthorizationToken,
|
||||||
|
RefreshToken: userToken.RefreshToken,
|
||||||
|
ExpiresAt: userToken.ExpiresAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendOTP sends OTP code to the provided phone number
|
||||||
|
// @Summary Send OTP code
|
||||||
|
// @Description Send OTP code to the provided phone number
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.OTPProviderReq true "OTP Request"
|
||||||
|
// @Success 200 {object} dto.OTPProviderResponse
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /auth/otp [post]
|
||||||
|
func (h *AuthHandler) SendOTP(c *fiber.Ctx) error {
|
||||||
|
var req dto.OTPProviderReq
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "invalid request body",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := h.authService.SendOTPCode(c.Context(), req.Receptor)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(dto.OTPProviderResponse{
|
||||||
|
Message: "OTP code sent successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyOTP verifies the OTP code
|
||||||
|
// @Summary Verify OTP code
|
||||||
|
// @Description Verify the provided OTP code for the phone number
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.OTPVerifyRequest true "OTP Verify Request"
|
||||||
|
// @Success 200 {object} dto.OTPVerifyResponse
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 401 {object} map[string]string
|
||||||
|
// @Router /auth/otp [post]
|
||||||
|
func (h *AuthHandler) VerifyOTP(c *fiber.Ctx) error {
|
||||||
|
var req dto.OTPVerifyRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "invalid request body",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.authService.VerifyOTP(c.Context(), req.Phone, req.Code)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(dto.OTPVerifyResponse{
|
||||||
|
Message: "OTP verified successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyKYC verifies user KYC information
|
||||||
|
// @Summary Verify user KYC
|
||||||
|
// @Description Verify user KYC with national ID and birth date
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body dto.KYCVerifyRequest true "KYC Verify Request"
|
||||||
|
// @Success 200 {object} dto.APIResponse
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /auth/kyc [post]
|
||||||
|
func (h *AuthHandler) VerifyKYC(c *fiber.Ctx) error {
|
||||||
|
var req dto.KYCVerifyRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "invalid request body",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
claims := middlewares.GetUserClaims(c)
|
||||||
|
err := h.authService.VerifyKYC(c.Context(), claims.UserID, req.NationalID, req.BirthDate)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(dto.APIResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "KYC verified successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) HelloWorld(c *fiber.Ctx) error {
|
||||||
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||||
|
"message": "Hello, World!",
|
||||||
|
})
|
||||||
|
}
|
||||||
37
internal/api/http/middlewares/auth.go
Normal file
37
internal/api/http/middlewares/auth.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/pkg/jwt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const userClaimsKey = "User-Claims"
|
||||||
|
|
||||||
|
func JWTAuthMiddleware(secret []byte) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
authHeader := c.Get("Authorization")
|
||||||
|
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"error": "missing or invalid Authorization header",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
claims, err := jwt.ParseToken(tokenString, secret)
|
||||||
|
if err != nil || claims == nil {
|
||||||
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||||
|
"error": "invalid or expired token",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Locals(userClaimsKey, claims)
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserClaims(c *fiber.Ctx) *jwt.UserClaims {
|
||||||
|
claims, _ := c.Locals(userClaimsKey).(*jwt.UserClaims)
|
||||||
|
return claims
|
||||||
|
}
|
||||||
57
internal/api/http/setup.go
Normal file
57
internal/api/http/setup.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/config"
|
||||||
|
"backend/docs"
|
||||||
|
httpHandlers "backend/internal/api/http/handlers"
|
||||||
|
"backend/internal/api/http/middlewares"
|
||||||
|
"backend/internal/app"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gofiber/adaptor/v2"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
httpSwagger "github.com/swaggo/http-swagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run(cfg config.Server, app *app.AppContainer) {
|
||||||
|
fiberApp := fiber.New()
|
||||||
|
|
||||||
|
fiberApp.Use(cors.New())
|
||||||
|
|
||||||
|
// Serve static files (HTML, CSS, JS)
|
||||||
|
fiberApp.Static("/", "./static")
|
||||||
|
|
||||||
|
api := fiberApp.Group("/api")
|
||||||
|
// register routes here
|
||||||
|
registerPublicRoutes(api, app)
|
||||||
|
|
||||||
|
docs.SwaggerInfo.Host = fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||||
|
docs.SwaggerInfo.BasePath = "/api"
|
||||||
|
docs.SwaggerInfo.Schemes = []string{"http", "https"}
|
||||||
|
|
||||||
|
api.Get("/swagger/*", adaptor.HTTPHandler(httpSwagger.Handler()))
|
||||||
|
|
||||||
|
api.Get("/hello", middlewares.JWTAuthMiddleware([]byte("Secret")), func(c *fiber.Ctx) error {
|
||||||
|
return c.SendString("Hello, World!")
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("Server starting on %s:%d", cfg.Host, cfg.Port)
|
||||||
|
log.Fatal(fiberApp.Listen(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerPublicRoutes(router fiber.Router, app *app.AppContainer) {
|
||||||
|
authgroup := router.Group("/auth")
|
||||||
|
|
||||||
|
authService := app.AuthService()
|
||||||
|
authHandler := httpHandlers.NewAuthHandler(&authService)
|
||||||
|
|
||||||
|
// Register auth routes
|
||||||
|
authgroup.Post("/challenge", authHandler.GenerateChallenge)
|
||||||
|
authgroup.Post("/authenticate", authHandler.Authenticate)
|
||||||
|
authgroup.Post("/otp", authHandler.SendOTP)
|
||||||
|
authgroup.Post("/verify", authHandler.VerifyOTP)
|
||||||
|
// add JWT middleware for KYC
|
||||||
|
authgroup.Post("/kyc", middlewares.JWTAuthMiddleware([]byte("Secret")), authHandler.VerifyKYC)
|
||||||
|
}
|
||||||
89
internal/app/app.go
Normal file
89
internal/app/app.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/config"
|
||||||
|
"backend/internal/repository/cache"
|
||||||
|
"backend/internal/repository/storage"
|
||||||
|
"backend/internal/repository/storage/types"
|
||||||
|
"backend/internal/usecase"
|
||||||
|
cachePackage "backend/pkg/cache"
|
||||||
|
"backend/pkg/postgres"
|
||||||
|
"backend/pkg/sms"
|
||||||
|
"backend/pkg/zohal"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppContainer struct {
|
||||||
|
cfg config.Config
|
||||||
|
dbConn *gorm.DB
|
||||||
|
redis *cachePackage.Redis
|
||||||
|
authService usecase.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAppContainer(cfg config.Config) (*AppContainer, error) {
|
||||||
|
dbOptions := postgres.DBConnOptions{
|
||||||
|
User: cfg.DB.User,
|
||||||
|
Pass: cfg.DB.Pass,
|
||||||
|
Host: cfg.DB.Host,
|
||||||
|
Port: uint(cfg.DB.Port),
|
||||||
|
DBName: cfg.DB.DBName,
|
||||||
|
}
|
||||||
|
dbConn, err := postgres.NewPsqlGormConnection(dbOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||||
|
}
|
||||||
|
dbConn.AutoMigrate(&types.User{})
|
||||||
|
|
||||||
|
redis, err := cachePackage.NewRedis(cfg.Redis)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userRepo := storage.NewUserRepository(dbConn)
|
||||||
|
sessionRepo := cache.NewSessionRepository(redis)
|
||||||
|
challengeRepo := cache.NewChallengeRepository(redis)
|
||||||
|
zohal := zohal.NewZohal(cfg.KYCProvider)
|
||||||
|
otpRepo := cache.NewOTPRepository(redis)
|
||||||
|
smsProvider := sms.NewKavenegar(cfg.Kavenegar)
|
||||||
|
|
||||||
|
authService := usecase.NewAuthService(
|
||||||
|
userRepo,
|
||||||
|
sessionRepo,
|
||||||
|
challengeRepo,
|
||||||
|
30,
|
||||||
|
otpRepo,
|
||||||
|
zohal,
|
||||||
|
smsProvider,
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
|
|
||||||
|
return &AppContainer{
|
||||||
|
cfg: cfg,
|
||||||
|
dbConn: dbConn,
|
||||||
|
redis: redis,
|
||||||
|
authService: authService,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *AppContainer) AuthService() usecase.AuthService {
|
||||||
|
return app.authService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *AppContainer) Close() {
|
||||||
|
if app.redis != nil {
|
||||||
|
if err := app.redis.Close(); err != nil {
|
||||||
|
log.Printf("Error closing Redis connection: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.dbConn != nil {
|
||||||
|
if sqlDB, err := app.dbConn.DB(); err == nil {
|
||||||
|
if err := sqlDB.Close(); err != nil {
|
||||||
|
log.Printf("Error closing database connection: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
internal/domain/otp.go
Normal file
34
internal/domain/otp.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/pkg/util"
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OTPRepo interface {
|
||||||
|
Create(ctx context.Context, phone string, otp *OTP) error
|
||||||
|
Get(ctx context.Context, phone string) (*OTP, error)
|
||||||
|
Delete(ctx context.Context, phone string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type OTP struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Code string
|
||||||
|
Phone string
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOTP(phone string) *OTP {
|
||||||
|
return &OTP{
|
||||||
|
ID: uuid.New(),
|
||||||
|
Code: util.GenOTPCode(),
|
||||||
|
Phone: phone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OTP) IsExpired() bool {
|
||||||
|
return time.Now().After(o.ExpiresAt)
|
||||||
|
}
|
||||||
38
internal/domain/session.go
Normal file
38
internal/domain/session.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionRepo interface {
|
||||||
|
Create(ctx context.Context, session *UserSession) error
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID) (*UserSession, error)
|
||||||
|
Delete(ctx context.Context, id uuid.UUID) error
|
||||||
|
GetUserSessions(ctx context.Context, userID uuid.UUID) ([]UserSession, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add IP and UserAgent
|
||||||
|
type UserSession struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
UserID uuid.UUID
|
||||||
|
User User
|
||||||
|
WalletID string
|
||||||
|
ExpiresAt time.Time
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSession(userID uuid.UUID, walletID string, expiresAt time.Time) *UserSession {
|
||||||
|
return &UserSession{
|
||||||
|
ID: uuid.New(),
|
||||||
|
UserID: userID,
|
||||||
|
WalletID: walletID,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
UpdatedAt: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
}
|
||||||
162
internal/domain/user.go
Normal file
162
internal/domain/user.go
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"net/mail"
|
||||||
|
|
||||||
|
validate "backend/pkg/validate/common"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUserNotFound = errors.New("user not found")
|
||||||
|
ErrEmailInvalid = errors.New("email is invalid")
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRepo interface {
|
||||||
|
Create(ctx context.Context, user *User) error
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID) (*User, error)
|
||||||
|
GetByPhone(ctx context.Context, phone string) (*User, error)
|
||||||
|
GetByPubKey(ctx context.Context, pubKey string) (*User, error)
|
||||||
|
GetOrCreateUserByPubKey(ctx context.Context, pubKey string) (*User, error)
|
||||||
|
Update(ctx context.Context, user *User) error
|
||||||
|
Delete(ctx context.Context, id uuid.UUID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChallengeRepo interface {
|
||||||
|
Create(ctx context.Context, pubKey string, challenge *Challenge) error
|
||||||
|
GetByPubKey(ctx context.Context, pubKey string) (*Challenge, error)
|
||||||
|
Delete(ctx context.Context, pubKey string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type KYCLevel int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
KYCLevel0 KYCLevel = iota
|
||||||
|
KYCLevel1
|
||||||
|
KYCLevel2
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
PubKey string
|
||||||
|
Name string
|
||||||
|
KYCLevel KYCLevel
|
||||||
|
LastName string
|
||||||
|
PhoneNumber string
|
||||||
|
Email *string
|
||||||
|
NationalID string
|
||||||
|
BirthDate *time.Time
|
||||||
|
LastLogin *time.Time
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUser(pubKey, name, lastName, phoneNumber, email, nationalID string) (*User, error) {
|
||||||
|
|
||||||
|
_, err := validate.IsValidPhone(phoneNumber)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = validate.IsValidNationalID(nationalID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = validate.IsValidPublicKey(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &User{
|
||||||
|
ID: uuid.New(),
|
||||||
|
PubKey: pubKey,
|
||||||
|
Name: name,
|
||||||
|
LastName: lastName,
|
||||||
|
PhoneNumber: phoneNumber,
|
||||||
|
NationalID: nationalID,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
UpdatedAt: time.Now().UTC(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) UpdateLastLogin() {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
u.LastLogin = &now
|
||||||
|
u.UpdatedAt = now
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) UpdateInfo(email string) {
|
||||||
|
addr, err := mail.ParseAddress(email)
|
||||||
|
if err == nil {
|
||||||
|
u.Email = &addr.Address
|
||||||
|
}
|
||||||
|
u.UpdatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) UpdateKYCLevel(level KYCLevel) {
|
||||||
|
u.KYCLevel = level
|
||||||
|
u.UpdatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Challenge struct {
|
||||||
|
Message string
|
||||||
|
TimeStamp time.Time
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Challenge) IsExpired() bool {
|
||||||
|
return time.Now().After(c.ExpiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Challenge) Verify(address string, signedMsg string) (bool, error) {
|
||||||
|
if c.IsExpired() {
|
||||||
|
return false, errors.New("challenge has expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, err := hexutil.Decode(signedMsg)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("decode signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
challengeBytes := []byte(c.Message)
|
||||||
|
|
||||||
|
return c.VerifySignedBytes(address, challengeBytes, signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Challenge) VerifySignedBytes(expectedAddress string, msg []byte, sig []byte) (bool, error) {
|
||||||
|
msgHash := accounts.TextHash(msg)
|
||||||
|
|
||||||
|
if len(sig) != 65 {
|
||||||
|
return false, fmt.Errorf("invalid signature length: expected 65 bytes, got %d", len(sig))
|
||||||
|
}
|
||||||
|
|
||||||
|
if sig[crypto.RecoveryIDOffset] == 27 || sig[crypto.RecoveryIDOffset] == 28 {
|
||||||
|
sig[crypto.RecoveryIDOffset] -= 27
|
||||||
|
}
|
||||||
|
|
||||||
|
recovered, err := crypto.SigToPub(msgHash, sig)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to recover public key: %w", err)
|
||||||
|
}
|
||||||
|
recoveredAddr := crypto.PubkeyToAddress(*recovered)
|
||||||
|
|
||||||
|
if !common.IsHexAddress(expectedAddress) {
|
||||||
|
return false, fmt.Errorf("invalid Ethereum address format: %s", expectedAddress)
|
||||||
|
}
|
||||||
|
expectedAddr := common.HexToAddress(expectedAddress)
|
||||||
|
|
||||||
|
return strings.EqualFold(expectedAddr.Hex(), recoveredAddr.Hex()), nil
|
||||||
|
}
|
||||||
0
internal/repository/cache/.gitkeep
vendored
0
internal/repository/cache/.gitkeep
vendored
80
internal/repository/cache/challenge_repo.go
vendored
Normal file
80
internal/repository/cache/challenge_repo.go
vendored
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/internal/domain"
|
||||||
|
"backend/pkg/cache"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChallengeRepository struct {
|
||||||
|
redis *cache.Redis
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChallengeRepository(redis *cache.Redis) domain.ChallengeRepo {
|
||||||
|
return &ChallengeRepository{
|
||||||
|
redis: redis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChallengeRepository) Create(ctx context.Context, pubKey string, challenge *domain.Challenge) error {
|
||||||
|
key := r.getChallengeKey(pubKey)
|
||||||
|
|
||||||
|
challengeData, err := json.Marshal(challenge)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal challenge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl := time.Until(challenge.ExpiresAt)
|
||||||
|
if ttl <= 0 {
|
||||||
|
return fmt.Errorf("challenge already expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.redis.Client().Set(ctx, key, challengeData, ttl).Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to store challenge in Redis: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChallengeRepository) GetByPubKey(ctx context.Context, pubKey string) (*domain.Challenge, error) {
|
||||||
|
key := r.getChallengeKey(pubKey)
|
||||||
|
|
||||||
|
result, err := r.redis.Client().Get(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return nil, fmt.Errorf("challenge not found")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to get challenge from Redis: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var challenge domain.Challenge
|
||||||
|
if err := json.Unmarshal([]byte(result), &challenge); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal challenge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if challenge.IsExpired() {
|
||||||
|
r.Delete(ctx, pubKey)
|
||||||
|
return nil, fmt.Errorf("challenge expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &challenge, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChallengeRepository) Delete(ctx context.Context, pubKey string) error {
|
||||||
|
key := r.getChallengeKey(pubKey)
|
||||||
|
|
||||||
|
if err := r.redis.Client().Del(ctx, key).Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete challenge from Redis: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChallengeRepository) getChallengeKey(pubKey string) string {
|
||||||
|
return fmt.Sprintf("challenge:%s", pubKey)
|
||||||
|
}
|
||||||
79
internal/repository/cache/otp_repo.go
vendored
Normal file
79
internal/repository/cache/otp_repo.go
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/internal/domain"
|
||||||
|
"backend/pkg/cache"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OTPRepository struct {
|
||||||
|
redis *cache.Redis
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOTPRepository(redis *cache.Redis) domain.OTPRepo {
|
||||||
|
return &OTPRepository{
|
||||||
|
redis: redis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OTPRepository) Create(ctx context.Context, phone string, otp *domain.OTP) error {
|
||||||
|
key := r.getOTPKey(phone)
|
||||||
|
|
||||||
|
otpData, err := json.Marshal(otp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal otp: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl := time.Until(otp.ExpiresAt)
|
||||||
|
if ttl <= 0 {
|
||||||
|
return fmt.Errorf("OTP expired already")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.redis.Client().Set(ctx, key, otpData, ttl).Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to store otp in redis: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OTPRepository) Delete(ctx context.Context, phone string) error {
|
||||||
|
key := r.getOTPKey(phone)
|
||||||
|
|
||||||
|
if err := r.redis.Client().Del(ctx, key).Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to del otp from redis")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OTPRepository) Get(ctx context.Context, phone string) (*domain.OTP, error) {
|
||||||
|
key := r.getOTPKey(phone)
|
||||||
|
|
||||||
|
result, err := r.redis.Client().Get(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return nil, fmt.Errorf("otp code not found")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var otp domain.OTP
|
||||||
|
if err := json.Unmarshal([]byte(result), &otp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if otp.IsExpired() {
|
||||||
|
r.Delete(ctx, phone)
|
||||||
|
return nil, fmt.Errorf("otp is expired")
|
||||||
|
}
|
||||||
|
return &otp, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OTPRepository) getOTPKey(phone string) string {
|
||||||
|
return fmt.Sprintf("challenge:%s", phone)
|
||||||
|
}
|
||||||
135
internal/repository/cache/session_repo.go
vendored
Normal file
135
internal/repository/cache/session_repo.go
vendored
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/internal/domain"
|
||||||
|
"backend/pkg/cache"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionRepository struct {
|
||||||
|
redis *cache.Redis
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionRepository(redis *cache.Redis) domain.SessionRepo {
|
||||||
|
return &SessionRepository{
|
||||||
|
redis: redis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SessionRepository) Create(ctx context.Context, session *domain.UserSession) error {
|
||||||
|
key := r.getSessionKey(session.ID)
|
||||||
|
|
||||||
|
sessionData, err := json.Marshal(session)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl := time.Until(session.ExpiresAt)
|
||||||
|
if ttl <= 0 {
|
||||||
|
return fmt.Errorf("session already expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.redis.Client().Set(ctx, key, sessionData, ttl).Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to store session in Redis: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userSessionsKey := r.getUserSessionsKey(session.UserID)
|
||||||
|
if err := r.redis.Client().SAdd(ctx, userSessionsKey, session.ID.String()).Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to add session to user sessions set: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.redis.Client().Expire(ctx, userSessionsKey, ttl).Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to set TTL for user sessions set: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SessionRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.UserSession, error) {
|
||||||
|
key := r.getSessionKey(id)
|
||||||
|
|
||||||
|
result, err := r.redis.Client().Get(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return nil, fmt.Errorf("session not found")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to get session from Redis: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var session domain.UserSession
|
||||||
|
if err := json.Unmarshal([]byte(result), &session); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(session.ExpiresAt) {
|
||||||
|
r.Delete(ctx, id)
|
||||||
|
return nil, fmt.Errorf("session expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SessionRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
key := r.getSessionKey(id)
|
||||||
|
|
||||||
|
session, err := r.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.redis.Client().Del(ctx, key).Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete session from Redis: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userSessionsKey := r.getUserSessionsKey(session.UserID)
|
||||||
|
if err := r.redis.Client().SRem(ctx, userSessionsKey, id.String()).Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove session from user sessions set: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SessionRepository) GetUserSessions(ctx context.Context, userID uuid.UUID) ([]domain.UserSession, error) {
|
||||||
|
userSessionsKey := r.getUserSessionsKey(userID)
|
||||||
|
|
||||||
|
sessionIDs, err := r.redis.Client().SMembers(ctx, userSessionsKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return []domain.UserSession{}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to get user sessions from Redis: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessions []domain.UserSession
|
||||||
|
for _, sessionIDStr := range sessionIDs {
|
||||||
|
sessionID, err := uuid.Parse(sessionIDStr)
|
||||||
|
if err != nil {
|
||||||
|
r.redis.Client().SRem(ctx, userSessionsKey, sessionIDStr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := r.GetByID(ctx, sessionID)
|
||||||
|
if err != nil {
|
||||||
|
r.redis.Client().SRem(ctx, userSessionsKey, sessionIDStr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions = append(sessions, *session)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SessionRepository) getSessionKey(sessionID uuid.UUID) string {
|
||||||
|
return fmt.Sprintf("session:%s", sessionID.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SessionRepository) getUserSessionsKey(userID uuid.UUID) string {
|
||||||
|
return fmt.Sprintf("user_sessions:%s", userID.String())
|
||||||
|
}
|
||||||
0
internal/repository/external/.gitkeep
vendored
0
internal/repository/external/.gitkeep
vendored
22
internal/repository/storage/types/base.go
Normal file
22
internal/repository/storage/types/base.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Base struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Base) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
|
if b.ID == uuid.Nil {
|
||||||
|
b.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
58
internal/repository/storage/types/user.go
Normal file
58
internal/repository/storage/types/user.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/internal/domain"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Base
|
||||||
|
PubKey string
|
||||||
|
Name string
|
||||||
|
LastName string
|
||||||
|
KYCLevel int
|
||||||
|
//TODO: add NFT Embedded struct
|
||||||
|
Phone string
|
||||||
|
NationalID string
|
||||||
|
LastLogin *time.Time
|
||||||
|
Email *string
|
||||||
|
BirthDate *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func CastUserToStorage(u domain.User) *User {
|
||||||
|
return &User{
|
||||||
|
Base: Base{
|
||||||
|
ID: u.ID,
|
||||||
|
CreatedAt: u.CreatedAt,
|
||||||
|
UpdatedAt: u.UpdatedAt,
|
||||||
|
DeletedAt: u.DeletedAt,
|
||||||
|
},
|
||||||
|
PubKey: u.PubKey,
|
||||||
|
Name: u.Name,
|
||||||
|
LastName: u.LastName,
|
||||||
|
KYCLevel: int(u.KYCLevel),
|
||||||
|
Phone: u.PhoneNumber,
|
||||||
|
NationalID: u.NationalID,
|
||||||
|
Email: u.Email,
|
||||||
|
BirthDate: u.BirthDate,
|
||||||
|
LastLogin: u.LastLogin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CastUserToDomain(u User) *domain.User {
|
||||||
|
return &domain.User{
|
||||||
|
ID: u.ID,
|
||||||
|
PubKey: u.PubKey,
|
||||||
|
Name: u.Name,
|
||||||
|
LastName: u.LastName,
|
||||||
|
KYCLevel: domain.KYCLevel(u.KYCLevel),
|
||||||
|
PhoneNumber: u.Phone,
|
||||||
|
NationalID: u.NationalID,
|
||||||
|
Email: u.Email,
|
||||||
|
BirthDate: u.BirthDate,
|
||||||
|
LastLogin: u.LastLogin,
|
||||||
|
CreatedAt: u.CreatedAt,
|
||||||
|
UpdatedAt: u.UpdatedAt,
|
||||||
|
DeletedAt: u.DeletedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
81
internal/repository/storage/user_repo.go
Normal file
81
internal/repository/storage/user_repo.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/internal/domain"
|
||||||
|
"backend/internal/repository/storage/types"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserRepository(db *gorm.DB) domain.UserRepo {
|
||||||
|
return &UserRepository{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) Create(ctx context.Context, user *domain.User) error {
|
||||||
|
model := types.CastUserToStorage(*user)
|
||||||
|
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*user = *types.CastUserToDomain(*model)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
|
||||||
|
var user types.User
|
||||||
|
if err := r.db.WithContext(ctx).First(&user, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return types.CastUserToDomain(user), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) GetByPhone(ctx context.Context, phone string) (*domain.User, error) {
|
||||||
|
var user types.User
|
||||||
|
if err := r.db.WithContext(ctx).First(&user, "phone = ?", phone).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return types.CastUserToDomain(user), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) Update(ctx context.Context, user *domain.User) error {
|
||||||
|
userModel := types.CastUserToStorage(*user)
|
||||||
|
return r.db.WithContext(ctx).Save(&userModel).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&types.User{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) GetByPubKey(ctx context.Context, pubKey string) (*domain.User, error) {
|
||||||
|
var user types.User
|
||||||
|
if err := r.db.WithContext(ctx).First(&user, "pub_key = ?", pubKey).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return types.CastUserToDomain(user), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) GetOrCreateUserByPubKey(ctx context.Context, pubKey string) (*domain.User, error) {
|
||||||
|
var user types.User
|
||||||
|
err := r.db.WithContext(ctx).First(&user, "pub_key = ?", pubKey).Error
|
||||||
|
if err == nil {
|
||||||
|
return types.CastUserToDomain(user), nil
|
||||||
|
}
|
||||||
|
if err != gorm.ErrRecordNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user = types.User{
|
||||||
|
PubKey: pubKey,
|
||||||
|
}
|
||||||
|
if err := r.db.WithContext(ctx).Create(&user).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return types.CastUserToDomain(user), nil
|
||||||
|
}
|
||||||
237
internal/usecase/auth_service.go
Normal file
237
internal/usecase/auth_service.go
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/config"
|
||||||
|
"backend/internal/domain"
|
||||||
|
"backend/pkg/jwt"
|
||||||
|
"backend/pkg/sms"
|
||||||
|
"backend/pkg/validate/common"
|
||||||
|
"backend/pkg/zohal"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
jwt2 "github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/kavenegar/kavenegar-go"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthService interface {
|
||||||
|
GenerateChallenge(ctx context.Context, pubKey string) (*domain.Challenge, error)
|
||||||
|
Authenticate(ctx context.Context, pubKey string, signature string, chainID uint, ipAddress, userAgent string) (*UserToken, error)
|
||||||
|
SendOTPCode(ctx context.Context, phone string) (string, error)
|
||||||
|
VerifyOTP(ctx context.Context, phone, code string) error
|
||||||
|
VerifyKYC(ctx context.Context, userID, nationalID, birthDate string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type authService struct {
|
||||||
|
userRepo domain.UserRepo
|
||||||
|
sessionRepo domain.SessionRepo
|
||||||
|
challengeRepo domain.ChallengeRepo
|
||||||
|
otpRepo domain.OTPRepo
|
||||||
|
zohal *zohal.Zohal
|
||||||
|
smsProvider *sms.Kavenegar
|
||||||
|
challengeExp uint
|
||||||
|
cfg config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserToken struct {
|
||||||
|
AuthorizationToken string
|
||||||
|
RefreshToken string
|
||||||
|
ExpiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthService(
|
||||||
|
userRepo domain.UserRepo,
|
||||||
|
sessionRepo domain.SessionRepo,
|
||||||
|
challengeRepo domain.ChallengeRepo,
|
||||||
|
challengeExp uint,
|
||||||
|
otpRepo domain.OTPRepo,
|
||||||
|
zohal *zohal.Zohal,
|
||||||
|
smsProvider *sms.Kavenegar,
|
||||||
|
cfg config.Config,
|
||||||
|
) AuthService {
|
||||||
|
return &authService{
|
||||||
|
userRepo: userRepo,
|
||||||
|
sessionRepo: sessionRepo,
|
||||||
|
challengeRepo: challengeRepo,
|
||||||
|
otpRepo: otpRepo,
|
||||||
|
challengeExp: challengeExp,
|
||||||
|
zohal: zohal,
|
||||||
|
smsProvider: smsProvider,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *authService) GenerateChallenge(ctx context.Context, pubKey string) (*domain.Challenge, error) {
|
||||||
|
_, err := common.IsValidPublicKey(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
challenge := &domain.Challenge{
|
||||||
|
Message: uuid.New().String(),
|
||||||
|
TimeStamp: time.Now().UTC(),
|
||||||
|
ExpiresAt: time.Now().Add(time.Duration(s.challengeExp) * time.Minute),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save challenge to Redis
|
||||||
|
err = s.challengeRepo.Create(ctx, pubKey, challenge)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save challenge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return challenge, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *authService) Authenticate(ctx context.Context, pubKey string, signature string, chainID uint, ipAddress, userAgent string) (*UserToken, error) {
|
||||||
|
_, err := common.IsValidPublicKey(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve challenge from Redis
|
||||||
|
challenge, err := s.challengeRepo.GetByPubKey(ctx, pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to retrieve challenge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid, err := challenge.Verify(pubKey, signature)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("signature verification failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValid {
|
||||||
|
return nil, errors.New("invalid signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the challenge after successful verification to prevent replay attacks
|
||||||
|
err = s.challengeRepo.Delete(ctx, pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to delete challenge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.userRepo.GetOrCreateUserByPubKey(ctx, pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get or create user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.UpdateLastLogin()
|
||||||
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expiresAt := time.Now().Add(time.Duration(s.cfg.JWT.TokenExpMinutes) * time.Minute)
|
||||||
|
session := domain.NewSession(user.ID, user.PubKey, expiresAt)
|
||||||
|
|
||||||
|
err = s.sessionRepo.Create(ctx, session)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := &jwt.UserClaims{
|
||||||
|
UserID: user.ID.String(),
|
||||||
|
KYCLevel: int(user.KYCLevel),
|
||||||
|
Sections: []string{},
|
||||||
|
}
|
||||||
|
claims.ExpiresAt = jwt2.NewNumericDate(expiresAt)
|
||||||
|
claims.IssuedAt = jwt2.NewNumericDate(time.Now())
|
||||||
|
|
||||||
|
authToken, err := jwt.CreateToken([]byte(s.cfg.JWT.TokenSecret), claims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshExpiresAt := time.Now().Add(time.Duration(s.cfg.JWT.RefreshTokenExpMinutes) * time.Minute)
|
||||||
|
refreshClaims := &jwt.UserClaims{
|
||||||
|
UserID: user.ID.String(),
|
||||||
|
KYCLevel: int(user.KYCLevel),
|
||||||
|
Sections: []string{"refresh"},
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshClaims.ExpiresAt = jwt2.NewNumericDate(refreshExpiresAt)
|
||||||
|
refreshClaims.IssuedAt = jwt2.NewNumericDate(time.Now())
|
||||||
|
|
||||||
|
refreshToken, err := jwt.CreateToken([]byte(s.cfg.JWT.TokenSecret), refreshClaims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create refresh token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserToken{
|
||||||
|
AuthorizationToken: authToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
ExpiresAt: expiresAt.Unix(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *authService) VerifyKYC(ctx context.Context, userID, nationalID, birthDate string) error {
|
||||||
|
uID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid user ID format: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.userRepo.GetByID(ctx, uID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shahkarResp, err := s.zohal.Shahkar(ctx, user.PhoneNumber, user.NationalID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("shahkar verification failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
identityResp, err := s.zohal.GetPerson(ctx, nationalID, birthDate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("identity verification failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if shahkarResp.StatusCode == 200 && identityResp.StatusCode == 200 {
|
||||||
|
user.UpdateKYCLevel(domain.KYCLevel1)
|
||||||
|
err = s.userRepo.Update(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update user KYC level: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("KYC verification failed - shahkar status: %d, identity status: %d",
|
||||||
|
shahkarResp.StatusCode, identityResp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *authService) SendOTPCode(ctx context.Context, phone string) (string, error) {
|
||||||
|
otp := domain.NewOTP(phone)
|
||||||
|
if err := s.otpRepo.Create(ctx, phone, otp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
otpMsg := &sms.OTPMsg{
|
||||||
|
Receptor: otp.Phone,
|
||||||
|
Token: otp.Code,
|
||||||
|
Template: s.cfg.Kavenegar.Template,
|
||||||
|
// TODO: make sure when use VerfiyLookup Params
|
||||||
|
Params: kavenegar.VerifyLookupParam{},
|
||||||
|
}
|
||||||
|
err := s.smsProvider.OTP(otpMsg)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return otp.Code, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *authService) VerifyOTP(ctx context.Context, phone, code string) error {
|
||||||
|
otp, err := s.otpRepo.Get(ctx, phone)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if otp == nil {
|
||||||
|
return errors.New("otp code not found or expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
if code != otp.Code {
|
||||||
|
return errors.New("otp code is not valid")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
36
pkg/cache/redis.go
vendored
Normal file
36
pkg/cache/redis.go
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/config"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Redis struct {
|
||||||
|
client *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRedis(cfg config.Redis) (*Redis, error) {
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||||
|
Password: cfg.Pass,
|
||||||
|
DB: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Redis{client: rdb}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Client() *redis.Client {
|
||||||
|
return r.client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Redis) Close() error {
|
||||||
|
return r.client.Close()
|
||||||
|
}
|
||||||
7
pkg/errors/errors.go
Normal file
7
pkg/errors/errors.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrPhoneInvalid = NewAppError(400, "Invalid phone number", ErrorTypeValidation, nil)
|
||||||
|
ErrNationalIDInvalid = NewAppError(400, "Invalid national ID", ErrorTypeValidation, nil)
|
||||||
|
ErrWalletInvalid = NewAppError(400, "Invalid wallet address", ErrorTypeValidation, nil)
|
||||||
|
)
|
||||||
51
pkg/errors/types.go
Normal file
51
pkg/errors/types.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrorTypeValidation ErrorType = "VALIDATION_ERROR"
|
||||||
|
|
||||||
|
ErrorTypeAuth ErrorType = "AUTH_ERROR"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppError struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Type ErrorType `json:"type"`
|
||||||
|
Cause error `json:"_"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAppError(code int, msg string, typ ErrorType, cause error) *AppError {
|
||||||
|
return &AppError{
|
||||||
|
Code: code,
|
||||||
|
Message: msg,
|
||||||
|
Type: typ,
|
||||||
|
Cause: cause,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AppError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AppError) Unwrap() error {
|
||||||
|
return e.Cause
|
||||||
|
}
|
||||||
|
func (e *AppError) Is(target error) bool {
|
||||||
|
if t, ok := target.(*AppError); ok {
|
||||||
|
return e.Type == t.Type
|
||||||
|
}
|
||||||
|
return errors.Is(e.Cause, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AppError) As(target interface{}) bool {
|
||||||
|
if t, ok := target.(**AppError); ok {
|
||||||
|
*t = e
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return errors.As(e.Cause, target)
|
||||||
|
}
|
||||||
253
pkg/ethereum/geth.go
Normal file
253
pkg/ethereum/geth.go
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
package ethereum
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EthereumClient interface {
|
||||||
|
GetEthClient() *ethclient.Client
|
||||||
|
GetRPCClient() *rpc.Client
|
||||||
|
Close()
|
||||||
|
IsConnected(ctx context.Context) error
|
||||||
|
|
||||||
|
NewBoundContract(address common.Address, abi string) (*bind.BoundContract, error)
|
||||||
|
// Creates options for sending transactions to the blockchain.
|
||||||
|
CreateTransactOpts(ctx context.Context, privateKey string, value *big.Int) (*bind.TransactOpts, error)
|
||||||
|
// Creates options for reading from the blockchain.
|
||||||
|
CreateCallOpts(ctx context.Context, blockNumber *big.Int) *bind.CallOpts
|
||||||
|
// Waits for a transaction to be mined and returns the receipt.
|
||||||
|
WaitForTransaction(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Geth struct {
|
||||||
|
ethClient *ethclient.Client
|
||||||
|
rpcClient *rpc.Client
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GethConfig struct {
|
||||||
|
URL string
|
||||||
|
RequestTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultGethConfig() *GethConfig {
|
||||||
|
return &GethConfig{
|
||||||
|
RequestTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGeth(clientURL string) (*Geth, error) {
|
||||||
|
if clientURL == "" {
|
||||||
|
return nil, fmt.Errorf("client URL cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcClient, err := rpc.Dial(clientURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to RPC client at %s: %w", clientURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ethClient := ethclient.NewClient(rpcClient)
|
||||||
|
|
||||||
|
geth := &Geth{
|
||||||
|
ethClient: ethClient,
|
||||||
|
rpcClient: rpcClient,
|
||||||
|
url: clientURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := geth.IsConnected(ctx); err != nil {
|
||||||
|
geth.Close()
|
||||||
|
return nil, fmt.Errorf("connection test failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return geth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGethWithConfig(config *GethConfig) (*Geth, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, fmt.Errorf("config cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.URL == "" {
|
||||||
|
return nil, fmt.Errorf("client URL cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcClient, err := rpc.Dial(config.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to RPC client at %s: %w", config.URL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ethClient := ethclient.NewClient(rpcClient)
|
||||||
|
|
||||||
|
geth := &Geth{
|
||||||
|
ethClient: ethClient,
|
||||||
|
rpcClient: rpcClient,
|
||||||
|
url: config.URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), config.RequestTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := geth.IsConnected(ctx); err != nil {
|
||||||
|
geth.Close()
|
||||||
|
return nil, fmt.Errorf("connection test failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return geth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Geth) GetEthClient() *ethclient.Client {
|
||||||
|
return g.ethClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Geth) GetRPCClient() *rpc.Client {
|
||||||
|
return g.rpcClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Geth) Close() {
|
||||||
|
if g.rpcClient != nil {
|
||||||
|
g.rpcClient.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Geth) IsConnected(ctx context.Context) error {
|
||||||
|
if g.ethClient == nil {
|
||||||
|
return fmt.Errorf("ethereum client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := g.ethClient.NetworkID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get network ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Geth) GetNetworkID(ctx context.Context) (uint64, error) {
|
||||||
|
if g.ethClient == nil {
|
||||||
|
return 0, fmt.Errorf("ethereum client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
networkID, err := g.ethClient.NetworkID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get network ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return networkID.Uint64(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Geth) GetChainID(ctx context.Context) (uint64, error) {
|
||||||
|
if g.ethClient == nil {
|
||||||
|
return 0, fmt.Errorf("ethereum client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
chainID, err := g.ethClient.ChainID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get chain ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chainID.Uint64(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Geth) GetBlockNumber(ctx context.Context) (uint64, error) {
|
||||||
|
if g.ethClient == nil {
|
||||||
|
return 0, fmt.Errorf("ethereum client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
blockNumber, err := g.ethClient.BlockNumber(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get block number: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockNumber, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Geth) GetURL() string {
|
||||||
|
return g.url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Geth) NewBoundContract(address common.Address, abiJSON string) (*bind.BoundContract, error) {
|
||||||
|
if g.ethClient == nil {
|
||||||
|
return nil, fmt.Errorf("ethereum client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedABI, err := parseABI(abiJSON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse ABI: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bind.NewBoundContract(address, parsedABI, g.ethClient, g.ethClient, g.ethClient), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Geth) CreateTransactOpts(ctx context.Context, privateKeyHex string, value *big.Int) (*bind.TransactOpts, error) {
|
||||||
|
if g.ethClient == nil {
|
||||||
|
return nil, fmt.Errorf("ethereum client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey, err := parsePrivateKey(privateKeyHex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
chainID, err := g.ethClient.ChainID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get chain ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create transactor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Context = ctx
|
||||||
|
if value != nil {
|
||||||
|
opts.Value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Geth) CreateCallOpts(ctx context.Context, blockNumber *big.Int) *bind.CallOpts {
|
||||||
|
return &bind.CallOpts{
|
||||||
|
Context: ctx,
|
||||||
|
BlockNumber: blockNumber,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Geth) WaitForTransaction(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
|
||||||
|
if g.ethClient == nil {
|
||||||
|
return nil, fmt.Errorf("ethereum client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
receipt, err := bind.WaitMined(ctx, g.ethClient, &types.Transaction{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to wait for transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return receipt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseABI(abiJSON string) (abi.ABI, error) {
|
||||||
|
return abi.JSON(strings.NewReader(abiJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePrivateKey(privateKeyHex string) (*ecdsa.PrivateKey, error) {
|
||||||
|
privateKeyHex = strings.TrimPrefix(privateKeyHex, "0x")
|
||||||
|
|
||||||
|
return crypto.HexToECDSA(privateKeyHex)
|
||||||
|
}
|
||||||
10
pkg/jwt/claims.go
Normal file
10
pkg/jwt/claims.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import jwt2 "github.com/golang-jwt/jwt/v5"
|
||||||
|
|
||||||
|
type UserClaims struct {
|
||||||
|
jwt2.RegisteredClaims
|
||||||
|
UserID string
|
||||||
|
KYCLevel int
|
||||||
|
Sections []string
|
||||||
|
}
|
||||||
37
pkg/jwt/jwt.go
Normal file
37
pkg/jwt/jwt.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
jwt2 "github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const UserClaimKey = "User-Claims"
|
||||||
|
|
||||||
|
func CreateToken(secret []byte, claims *UserClaims) (string, error) {
|
||||||
|
return jwt2.NewWithClaims(jwt2.SigningMethodHS512, claims).SignedString(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseToken(tokenString string, secret []byte) (*UserClaims, error) {
|
||||||
|
token, err := jwt2.ParseWithClaims(tokenString, &UserClaims{}, func(t *jwt2.Token) (interface{}, error) {
|
||||||
|
return secret, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
var claim *UserClaims
|
||||||
|
if token.Claims != nil {
|
||||||
|
cc, ok := token.Claims.(*UserClaims)
|
||||||
|
if ok {
|
||||||
|
claim = cc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return claim, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.Valid {
|
||||||
|
return claim, errors.New("token is not valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claim, nil
|
||||||
|
}
|
||||||
57
pkg/logger/logger.go
Normal file
57
pkg/logger/logger.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogLevel string
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const TraceIDKey contextKey = "trace_id"
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
*slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger(level LogLevel) *Logger {
|
||||||
|
return &Logger{
|
||||||
|
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelInfo, // default log level
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateTraceID() string {
|
||||||
|
return uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTraceID(ctx context.Context) context.Context {
|
||||||
|
if ctx.Value(TraceIDKey) == nil {
|
||||||
|
return context.WithValue(ctx, TraceIDKey, GenerateTraceID())
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTraceID(ctx context.Context) string {
|
||||||
|
if traceID, ok := ctx.Value(TraceIDKey).(string); ok {
|
||||||
|
return traceID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Info(ctx context.Context, msg string, args ...any) {
|
||||||
|
traceID := GetTraceID(ctx)
|
||||||
|
if traceID != "" {
|
||||||
|
args = append(args, slog.String(string(TraceIDKey), traceID))
|
||||||
|
}
|
||||||
|
l.Logger.Info(msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) ErrorWithoutContext(msg string, args ...any) {
|
||||||
|
l.Logger.Error(msg, args...)
|
||||||
|
}
|
||||||
32
pkg/sms/kavengar.go
Normal file
32
pkg/sms/kavengar.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package sms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/config"
|
||||||
|
|
||||||
|
"github.com/kavenegar/kavenegar-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Kavenegar struct {
|
||||||
|
*kavenegar.Kavenegar
|
||||||
|
}
|
||||||
|
|
||||||
|
type OTPMsg struct {
|
||||||
|
Receptor string
|
||||||
|
Token string
|
||||||
|
Template string
|
||||||
|
Params kavenegar.VerifyLookupParam
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKavenegar(cfg config.SMSProvider) *Kavenegar {
|
||||||
|
instance := kavenegar.New(cfg.APIKey)
|
||||||
|
return &Kavenegar{
|
||||||
|
Kavenegar: instance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kavenegar) OTP(otpMsg *OTPMsg) error {
|
||||||
|
if _, err := k.Verify.Lookup(otpMsg.Receptor, otpMsg.Template, otpMsg.Token, &otpMsg.Params); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
57
pkg/util/http.go
Normal file
57
pkg/util/http.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HttpClient struct {
|
||||||
|
client *http.Client
|
||||||
|
once sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpClient() *HttpClient {
|
||||||
|
return &HttpClient{
|
||||||
|
http.DefaultClient,
|
||||||
|
sync.Once{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpClient) getClient() *http.Client {
|
||||||
|
h.once.Do(func() {
|
||||||
|
h.client = &http.Client{}
|
||||||
|
})
|
||||||
|
return h.client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpClient) HttpRequest(ctx context.Context, method, url string, body interface{}, headers map[string]string) (*http.Response, error) {
|
||||||
|
payload, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for headerTitle, headerValue := range headers {
|
||||||
|
req.Header.Set(headerTitle, headerValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.getClient().Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return &http.Response{
|
||||||
|
Status: "200 ok",
|
||||||
|
StatusCode: 200,
|
||||||
|
Body: resp.Body,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
14
pkg/util/otp.go
Normal file
14
pkg/util/otp.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenOTPCode() string {
|
||||||
|
newRand := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
minNum := 100000
|
||||||
|
maxNum := 999999
|
||||||
|
return strconv.Itoa(newRand.Intn(maxNum-minNum+1) + minNum)
|
||||||
|
}
|
||||||
64
pkg/validate/common/common.go
Normal file
64
pkg/validate/common/common.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/pkg/errors"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsValidPhone(value string) (bool, error) {
|
||||||
|
re := regexp.MustCompile(`^(?:0|\+98|0098)(\d{10})$`)
|
||||||
|
result := re.FindStringSubmatch(value)
|
||||||
|
if len(result) > 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, errors.ErrPhoneInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidNationalID(value string) (bool, error) {
|
||||||
|
valueCount := len(value)
|
||||||
|
if valueCount < 8 {
|
||||||
|
return false, errors.ErrNationalIDInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if valueCount >= 8 && valueCount < 10 {
|
||||||
|
value = strings.Repeat("0", 10-valueCount) + value
|
||||||
|
}
|
||||||
|
|
||||||
|
var valueSlices []uint8
|
||||||
|
for _, s := range value {
|
||||||
|
i, err := strconv.ParseInt(string(s), 10, 8)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.ErrNationalIDInvalid
|
||||||
|
}
|
||||||
|
valueSlices = append(valueSlices, uint8(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
s := calculateNationalIDNumbers(&valueSlices)
|
||||||
|
|
||||||
|
s %= 11
|
||||||
|
l := valueSlices[len(value)-1]
|
||||||
|
if (s < 2 && s != int(l)) || (s >= 2 && int(l) != 11-s) {
|
||||||
|
return false, errors.ErrNationalIDInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateNationalIDNumbers(valueSlices *[]uint8) (sum int) {
|
||||||
|
var i uint8
|
||||||
|
for i = 10; i >= 2; i-- {
|
||||||
|
sum += int(i * (*valueSlices)[10-i])
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidPublicKey(value string) (bool, error) {
|
||||||
|
if !common.IsHexAddress(value) {
|
||||||
|
return false, errors.ErrWalletInvalid
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
50
pkg/zohal/types.go
Normal file
50
pkg/zohal/types.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package zohal
|
||||||
|
|
||||||
|
type respMatched struct {
|
||||||
|
Matched bool `json:"matched"`
|
||||||
|
}
|
||||||
|
type personIdentity struct {
|
||||||
|
Matched bool `json:"matched"`
|
||||||
|
LastName string `json:"last_name"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
FatherName string `json:"father_name"`
|
||||||
|
NationalCode string `json:"national_code"`
|
||||||
|
IsDead bool `json:"is_dead"`
|
||||||
|
Alive bool `json:"alive"`
|
||||||
|
}
|
||||||
|
type respBody[T any] struct {
|
||||||
|
Data T `json:"data"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
type zohalResp[T any] struct {
|
||||||
|
StatusCode int `json:"status_code"`
|
||||||
|
Body respBody[T] `json:"response_body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// https://service.zohal.io/api/v0/services/inquiry/shahkar
|
||||||
|
ZohalShahkarReq struct {
|
||||||
|
Phone string `json:"mobile"`
|
||||||
|
NationalID string `json:"national_code"`
|
||||||
|
}
|
||||||
|
ZohalShahkarResp struct {
|
||||||
|
zohalResp[respMatched]
|
||||||
|
}
|
||||||
|
// https://service.zohal.io/api/v0/services/inquiry/national_identity_inquiry
|
||||||
|
ZohalIdentityReq struct {
|
||||||
|
NationalID string `json:"national_code"`
|
||||||
|
BirthDate string `json:"birth_date"`
|
||||||
|
}
|
||||||
|
ZohalIdentityResp struct {
|
||||||
|
zohalResp[personIdentity]
|
||||||
|
}
|
||||||
|
// https://service.zohal.io/api/v0/services/inquiry/check_iban_with_national_code
|
||||||
|
ZohalIbanReq struct {
|
||||||
|
NationalID string `json:"national_code"`
|
||||||
|
IBAN string `json:"IBAN"`
|
||||||
|
BirthDate string `json:"birth_date"`
|
||||||
|
}
|
||||||
|
ZohalIbanResp struct {
|
||||||
|
zohalResp[respMatched]
|
||||||
|
}
|
||||||
|
)
|
||||||
105
pkg/zohal/zohal.go
Normal file
105
pkg/zohal/zohal.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package zohal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/config"
|
||||||
|
"backend/pkg/util"
|
||||||
|
"backend/pkg/validate/common"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Zohal struct {
|
||||||
|
cfg config.KYC
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewZohal(cfg config.KYC) *Zohal {
|
||||||
|
return &Zohal{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check Phone and NationalID
|
||||||
|
func (z *Zohal) Shahkar(ctx context.Context, phone, nationalID string) (ZohalShahkarResp, error) {
|
||||||
|
var req ZohalShahkarReq
|
||||||
|
var resp ZohalShahkarResp
|
||||||
|
ok, err := common.IsValidPhone(phone)
|
||||||
|
if !ok {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err = common.IsValidNationalID(nationalID)
|
||||||
|
if !ok {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
header := make(map[string]string)
|
||||||
|
header["Content-Type"] = "application/json"
|
||||||
|
header["Accept"] = "application/json"
|
||||||
|
header["Authorization"] = fmt.Sprintf("Bearer %s", z.cfg.APIKey)
|
||||||
|
|
||||||
|
req.Phone = phone
|
||||||
|
req.NationalID = nationalID
|
||||||
|
|
||||||
|
u, err := url.Parse(z.cfg.URL)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
u = u.JoinPath("inquiry", "shahkar")
|
||||||
|
|
||||||
|
client := util.NewHttpClient()
|
||||||
|
clientResp, err := client.HttpRequest(ctx, http.MethodPost, u.String(), req, header)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(clientResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(bodyBytes, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z *Zohal) GetPerson(ctx context.Context, nationalID, birthDate string) (ZohalIdentityResp, error) {
|
||||||
|
var req ZohalIdentityReq
|
||||||
|
var resp ZohalIdentityResp
|
||||||
|
|
||||||
|
header := make(map[string]string)
|
||||||
|
header["Content-Type"] = "application/json"
|
||||||
|
header["Accept"] = "application/json"
|
||||||
|
header["Authorization"] = fmt.Sprintf("Bearer %s", z.cfg.APIKey)
|
||||||
|
|
||||||
|
req.BirthDate = birthDate
|
||||||
|
req.NationalID = nationalID
|
||||||
|
|
||||||
|
u, err := url.Parse(z.cfg.URL)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
u = u.JoinPath("inquiry", "shahkar")
|
||||||
|
|
||||||
|
client := util.NewHttpClient()
|
||||||
|
clientResp, err := client.HttpRequest(ctx, http.MethodPost, u.String(), req, header)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(clientResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(bodyBytes, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
server:
|
||||||
|
host: "localhost"
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
db:
|
||||||
|
user: "postgres"
|
||||||
|
pass: "postgres"
|
||||||
|
host: "localhost"
|
||||||
|
port: 5432
|
||||||
|
db_name: "dezone"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: "localhost"
|
||||||
|
port: 6379
|
||||||
|
pass: ""
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
token_exp_minutes: 60
|
||||||
|
refresh_token_exp_minutes: 10080
|
||||||
|
token_secret: "Secret"
|
||||||
|
|
||||||
|
RPC:
|
||||||
|
url: "exmaple.com"
|
||||||
|
|
||||||
|
otp:
|
||||||
|
code_exp_minutes: 2
|
||||||
245
static/index.html
Normal file
245
static/index.html
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Web3 Login process with MetaMask</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/ethers@5.6.4/dist/ethers.umd.min.js"
|
||||||
|
type="application/javascript"
|
||||||
|
></script>
|
||||||
|
<script>
|
||||||
|
function web3_check_metamask() {
|
||||||
|
if (!window.ethereum) {
|
||||||
|
console.error(
|
||||||
|
"It seems that the MetaMask extension is not detected. Please install MetaMask first.",
|
||||||
|
);
|
||||||
|
alert(
|
||||||
|
"It seems that the MetaMask extension is not detected. Please install MetaMask first.",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
alert("✅ MetaMask extension has been detected!");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function web3_metamask_hash(pubKey) {
|
||||||
|
const z = await (
|
||||||
|
await fetch("http://127.0.0.1:8080/api/auth/challenge", {
|
||||||
|
body: `{ "pubKey": "${pubKey}"\n}`,
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
).json();
|
||||||
|
return z.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login_api(pubKey, sig) {
|
||||||
|
res = await (await fetch("http://127.0.0.1:8080/api/auth/authenticate", {
|
||||||
|
body: JSON.stringify({ pubKey: pubKey, signature: sig }),
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
})).json();
|
||||||
|
updateStatus(JSON.stringify(res), "text-green-600");
|
||||||
|
response = await (await fetch("http://127.0.0.1:8080/api/hello", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${res.authorizationToken}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
})).json();
|
||||||
|
console.log(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function web3_metamask_login() {
|
||||||
|
// Check first if the user has the MetaMask installed
|
||||||
|
if (web3_check_metamask()) {
|
||||||
|
console.log("Initate Login Process");
|
||||||
|
updateStatus("Initiating login process...", "text-blue-600");
|
||||||
|
|
||||||
|
// Get the Ethereum provider
|
||||||
|
const provider = new ethers.providers.Web3Provider(window.ethereum);
|
||||||
|
// Get Ethereum accounts
|
||||||
|
await provider.send("eth_requestAccounts", []);
|
||||||
|
console.log("Connected!!");
|
||||||
|
updateStatus("Connected to MetaMask!", "text-green-600");
|
||||||
|
|
||||||
|
// Get the User Ethereum address
|
||||||
|
const address = await provider.getSigner().getAddress();
|
||||||
|
console.log(address);
|
||||||
|
document.getElementById("address").textContent = address;
|
||||||
|
|
||||||
|
// Get custom message from input or use hashed string
|
||||||
|
const customMessage = document
|
||||||
|
.getElementById("messageInput")
|
||||||
|
.value.trim();
|
||||||
|
const messageToSign = customMessage || await web3_metamask_hash(address);
|
||||||
|
console.log("Message to sign: " + messageToSign);
|
||||||
|
|
||||||
|
// Request the user to sign it
|
||||||
|
updateStatus(
|
||||||
|
"Please sign the message in MetaMask...",
|
||||||
|
"text-orange-600",
|
||||||
|
);
|
||||||
|
const signature = await provider
|
||||||
|
.getSigner()
|
||||||
|
.signMessage(messageToSign);
|
||||||
|
// Got the signature
|
||||||
|
console.log("The signature: " + signature);
|
||||||
|
document.getElementById("signature").textContent = signature;
|
||||||
|
updateStatus("Message signed successfully!", "text-green-600");
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// you can then send the signature to the webserver for further processing and verification
|
||||||
|
console.log({address, signature})
|
||||||
|
await login_api(address, signature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(message, className) {
|
||||||
|
const statusEl = document.getElementById("status");
|
||||||
|
statusEl.textContent = message;
|
||||||
|
statusEl.className = `text-sm font-medium ${className}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 min-h-screen">
|
||||||
|
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
||||||
|
<div class="bg-white rounded-xl shadow-lg p-8">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-800 mb-2">
|
||||||
|
Web3 MetaMask Login
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Connect your wallet and sign messages securely
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- MetaMask Detection Section -->
|
||||||
|
<div class="bg-gray-50 rounded-lg p-6">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-700 mb-3">
|
||||||
|
1. MetaMask Detection
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onclick="web3_check_metamask();"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition duration-200 ease-in-out transform hover:scale-105"
|
||||||
|
>
|
||||||
|
Detect MetaMask
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message Input Section -->
|
||||||
|
<div class="bg-gray-50 rounded-lg p-6">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-700 mb-3">
|
||||||
|
2. Custom Message (Optional)
|
||||||
|
</h2>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label
|
||||||
|
for="messageInput"
|
||||||
|
class="block text-sm font-medium text-gray-600 mb-2"
|
||||||
|
>
|
||||||
|
Enter a custom message to sign (leave empty for random hash):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="messageInput"
|
||||||
|
placeholder="Enter your custom message here..."
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Section -->
|
||||||
|
<div class="bg-gray-50 rounded-lg p-6">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-700 mb-3">
|
||||||
|
3. Wallet Login
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onclick="web3_metamask_login();"
|
||||||
|
class="bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white font-medium py-3 px-8 rounded-lg transition duration-200 ease-in-out transform hover:scale-105 shadow-md"
|
||||||
|
>
|
||||||
|
🦊 Login with MetaMask
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Section -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700 mb-3">Status</h3>
|
||||||
|
<div id="status" class="text-sm text-gray-500">
|
||||||
|
Ready to connect...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results Section -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700 mb-4">
|
||||||
|
Connection Details
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1"
|
||||||
|
>Wallet Address:</label
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="address"
|
||||||
|
class="bg-gray-50 p-3 rounded border text-sm font-mono break-all text-gray-700"
|
||||||
|
>
|
||||||
|
Not connected
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-600 mb-1"
|
||||||
|
>Message Signature:</label
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="signature"
|
||||||
|
class="bg-gray-50 p-3 rounded border text-sm font-mono break-all text-gray-700 max-h-32 overflow-y-auto"
|
||||||
|
>
|
||||||
|
No signature yet
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Section -->
|
||||||
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-blue-400"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h4 class="text-sm font-medium text-blue-800">How it works</h4>
|
||||||
|
<p class="text-sm text-blue-700 mt-1">
|
||||||
|
First detect MetaMask, then optionally enter a custom message,
|
||||||
|
and finally connect your wallet to sign the message securely.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user