From 5c5df48644c4fce2ad74f367ec67acd5ea4a094c Mon Sep 17 00:00:00 2001 From: AmirMahdi Qiasvand Date: Sun, 7 Sep 2025 14:35:40 +0330 Subject: [PATCH] feat: add EIP712 verification, auth service implemention and jwt gen --- go.mod | 8 ++ go.sum | 58 ++++++++++ internal/domain/session.go | 9 +- internal/domain/user.go | 71 +++++++++--- internal/repository/storage/types/session.go | 8 +- internal/repository/storage/user_repo.go | 8 ++ internal/usecase/auth_service.go | 105 +++++++++++++++--- pkg/errors/errors.go | 1 + .../validate.go => common/common.go} | 23 +++- pkg/validate/phone/validate.go | 15 --- 10 files changed, 244 insertions(+), 62 deletions(-) rename pkg/validate/{national/validate.go => common/common.go} (62%) delete mode 100644 pkg/validate/phone/validate.go diff --git a/go.mod b/go.mod index fa937da..3f74173 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,13 @@ require ( ) require ( + github.com/bits-and-blooms/bitset v1.20.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/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // 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.8.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/holiman/uint256 v1.3.2 // indirect @@ -28,6 +35,7 @@ require ( github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/supranational/blst v0.3.14 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/crypto v0.36.0 // indirect diff --git a/go.sum b/go.sum index 33667b5..29fef1b 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,48 @@ +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/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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/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/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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +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-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +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/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/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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -29,18 +61,36 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD 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/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/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/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +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.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= @@ -58,6 +108,12 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= +github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +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= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= @@ -73,6 +129,8 @@ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/domain/session.go b/internal/domain/session.go index 66792ea..10264a4 100644 --- a/internal/domain/session.go +++ b/internal/domain/session.go @@ -14,26 +14,23 @@ type SessionRepo interface { 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 uuid.UUID - IPaddress string - Agent string + WalletID string ExpiresAt time.Time CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time } -func NewSession(userID, walletID uuid.UUID, ipAddress, agent string, expiresAt time.Time) *UserSession { +func NewSession(userID uuid.UUID, walletID string, expiresAt time.Time) *UserSession { return &UserSession{ ID: uuid.New(), UserID: userID, WalletID: walletID, - IPaddress: ipAddress, - Agent: agent, ExpiresAt: expiresAt, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), diff --git a/internal/domain/user.go b/internal/domain/user.go index 74a431e..22cc526 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -1,30 +1,33 @@ package domain import ( - "backend/pkg/validate/national" - "backend/pkg/validate/phone" "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 ( - ErrPhoneInvalid = errors.New("phone number is invalid") - ErrUserNotFound = errors.New("user not found") - ErrNationalIDInvalid = errors.New("national ID is invalid") - ErrWalletInvalid = errors.New("wallet address is invalid") - ErrEmailInvalid = errors.New("email is invalid") + 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) Update(ctx context.Context, user *User) error Delete(ctx context.Context, id uuid.UUID) error } @@ -53,25 +56,65 @@ type Challenge struct { ExpiresAt time.Time } -// TODO: check EIP712 in here for challenge validation 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.String()) + + 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 +} + func NewUser(pubKey, name, lastName, phoneNumber, email, nationalID string) (*User, error) { - _, err := phone.IsValid(phoneNumber) + _, err := validate.IsValidPhone(phoneNumber) if err != nil { - return nil, ErrPhoneInvalid + return nil, err } - _, err = national.IsValid(nationalID) + _, err = validate.IsValidNationalID(nationalID) if err != nil { - return nil, ErrNationalIDInvalid + return nil, err } - if !common.IsHexAddress(pubKey) { - return nil, ErrWalletInvalid + _, err = validate.IsValidPublicKey(pubKey) + if err != nil { + return nil, err } return &User{ diff --git a/internal/repository/storage/types/session.go b/internal/repository/storage/types/session.go index 7eeb7a5..23b6a2a 100644 --- a/internal/repository/storage/types/session.go +++ b/internal/repository/storage/types/session.go @@ -11,9 +11,7 @@ type UserSession struct { Base UserID uuid.UUID User *User - WalletID uuid.UUID - IP string - Agent string + WalletID string ExpireAt time.Time } @@ -28,8 +26,6 @@ func CastSessionToStorage(s domain.UserSession) *UserSession { UserID: s.UserID, User: CastUserToStorage(s.User), WalletID: s.WalletID, - IP: s.IPaddress, - Agent: s.Agent, ExpireAt: s.ExpiresAt, } } @@ -40,8 +36,6 @@ func CastSessionToDomain(s UserSession) *domain.UserSession { UserID: s.UserID, User: *CastUserToDomain(*s.User), WalletID: s.WalletID, - IPaddress: s.IP, - Agent: s.Agent, ExpiresAt: s.ExpireAt, CreatedAt: s.CreatedAt, UpdatedAt: s.UpdatedAt, diff --git a/internal/repository/storage/user_repo.go b/internal/repository/storage/user_repo.go index 304fce4..a7d8ff1 100644 --- a/internal/repository/storage/user_repo.go +++ b/internal/repository/storage/user_repo.go @@ -48,3 +48,11 @@ func (r *UserRepository) Update(ctx context.Context, user *domain.User) 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 +} diff --git a/internal/usecase/auth_service.go b/internal/usecase/auth_service.go index 53b362a..fb45ba9 100644 --- a/internal/usecase/auth_service.go +++ b/internal/usecase/auth_service.go @@ -1,11 +1,17 @@ package usecase import ( + "backend/config" "backend/internal/domain" + "backend/pkg/jwt" + "backend/pkg/validate/common" "context" + "errors" + "fmt" "time" - "github.com/ethereum/go-ethereum/common" + jwt2 "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" ) @@ -15,11 +21,10 @@ type AuthService interface { } type authService struct { - userRepo *domain.UserRepo - sessionRepo *domain.SessionRepo - challengeExp uint - tokenExp uint - refreshTokenExp uint + userRepo domain.UserRepo + sessionRepo domain.SessionRepo + challengeExp uint + cfg config.JWT } type UserToken struct { @@ -29,24 +34,25 @@ type UserToken struct { } func NewAuthService( - userRepo *domain.UserRepo, - sessionRepo *domain.SessionRepo, + userRepo domain.UserRepo, + sessionRepo domain.SessionRepo, challengeExp uint, - tokenExp uint, - refreshTokenExp uint) AuthService { + cfg config.JWT, +) AuthService { return &authService{ - userRepo: userRepo, - sessionRepo: sessionRepo, - challengeExp: challengeExp, - tokenExp: tokenExp, - refreshTokenExp: refreshTokenExp, + userRepo: userRepo, + sessionRepo: sessionRepo, + challengeExp: challengeExp, + cfg: cfg, } } func (s *authService) GenerateChallenge(ctx context.Context, pubKey string) (*domain.Challenge, error) { - if !common.IsHexAddress(pubKey) { - return nil, domain.ErrWalletInvalid + _, err := common.IsValidPublicKey(pubKey) + if err != nil { + return nil, err } + challenge := &domain.Challenge{ Message: uuid.New(), TimeStamp: time.Now().UTC(), @@ -56,5 +62,68 @@ func (s *authService) GenerateChallenge(ctx context.Context, pubKey string) (*do } func (s *authService) Authenticate(ctx context.Context, pubKey string, signature string, challenge *domain.Challenge, chainID uint) (*UserToken, error) { - return nil, nil + _, err := common.IsValidPublicKey(pubKey) + if err != nil { + return nil, fmt.Errorf("invalid public key: %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") + } + user, err := s.userRepo.GetByPubKey(ctx, pubKey) + if err != nil { + return nil, err + } + if user == nil { + return nil, domain.ErrUserNotFound + } + user.UpdateLastLogin() + if err := s.userRepo.Update(ctx, user); err != nil { + return nil, err + } + + expiresAt := time.Now().Add(time.Duration(s.cfg.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: uint(user.ID.ID()), + Sections: []string{}, + } + claims.ExpiresAt = jwt2.NewNumericDate(expiresAt) + claims.IssuedAt = jwt2.NewNumericDate(time.Now()) + + authToken, err := jwt.CreateToken([]byte(s.cfg.TokenSecret), claims) + if err != nil { + return nil, err + } + + refreshExpiresAt := time.Now().Add(time.Duration(s.cfg.RefreshTokenExpMinutes) * time.Minute) + refreshClaims := &jwt.UserClaims{ + UserID: uint(user.ID.ID()), + Sections: []string{"refresh"}, + } + + refreshClaims.ExpiresAt = jwt2.NewNumericDate(refreshExpiresAt) + refreshClaims.IssuedAt = jwt2.NewNumericDate(time.Now()) + + refreshToken, err := jwt.CreateToken([]byte(s.cfg.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 } diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 5b4aa67..e8ae380 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -3,4 +3,5 @@ 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) ) diff --git a/pkg/validate/national/validate.go b/pkg/validate/common/common.go similarity index 62% rename from pkg/validate/national/validate.go rename to pkg/validate/common/common.go index af13a4c..37dc43c 100644 --- a/pkg/validate/national/validate.go +++ b/pkg/validate/common/common.go @@ -1,12 +1,24 @@ -package national +package common import ( "backend/pkg/errors" + "regexp" "strconv" "strings" + + "github.com/ethereum/go-ethereum/common" ) -func IsValid(value string) (bool, error) { +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 @@ -43,3 +55,10 @@ func calculateNationalIDNumbers(valueSlices *[]uint8) (sum int) { } return sum } + +func IsValidPublicKey(value string) (bool, error) { + if !common.IsHexAddress(value) { + return false, errors.ErrWalletInvalid + } + return true, nil +} diff --git a/pkg/validate/phone/validate.go b/pkg/validate/phone/validate.go deleted file mode 100644 index 791485c..0000000 --- a/pkg/validate/phone/validate.go +++ /dev/null @@ -1,15 +0,0 @@ -package phone - -import ( - "backend/pkg/errors" - "regexp" -) - -func IsValid(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 -}