From 343552492ab8af45627470c39871fbcb16ff2ba5 Mon Sep 17 00:00:00 2001 From: Auroter Date: Thu, 27 Oct 2022 19:01:30 -0700 Subject: [PATCH 01/12] onboarding endpoints are up, time to test now --- .env.example | 1 + api/handler/user.go | 116 +++++++++++++++++ pkg/internal/common/evm.go | 23 ++++ pkg/model/request.go | 20 +++ pkg/model/user.go | 5 + pkg/repository/contact.go | 42 ++++++ pkg/repository/instrument.go | 17 ++- pkg/repository/user.go | 4 +- pkg/service/user.go | 239 +++++++++++++++++++++++++++++++++++ 9 files changed, 463 insertions(+), 4 deletions(-) create mode 100644 api/handler/user.go create mode 100644 pkg/model/user.go create mode 100644 pkg/repository/contact.go create mode 100644 pkg/service/user.go diff --git a/.env.example b/.env.example index 9f1692c9..d26a29e0 100644 --- a/.env.example +++ b/.env.example @@ -26,4 +26,5 @@ UNIT21_URL=https://sandbox2-api.unit21.com/v1/ TWILIO_ACCOUNT_SID=AC034879a536d54325687e48544403cb4d TWILIO_AUTH_TOKEN= TWILIO_SMS_SID=MG367a4f51ea6f67a28db4d126eefc734f +TWILIO_AUTH_SID=VAc0221d2e2de51bbc28afe5349469a3e4 DEV_PHONE_NUMBERS=+14088675309,+14155555555 \ No newline at end of file diff --git a/api/handler/user.go b/api/handler/user.go new file mode 100644 index 00000000..e07738cd --- /dev/null +++ b/api/handler/user.go @@ -0,0 +1,116 @@ +package handler + +import ( + "net/http" + + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/service" + "github.com/labstack/echo/v4" + "github.com/rs/zerolog" +) + +type User interface { + GetStatus(c echo.Context) error // If wallet addr is associated with user, return current state of their onboarding + Create(c echo.Context) error // Create new user using wallet addr, optionally mark as validated if signature is provided + Sign(c echo.Context) error // Takes in a signed timestamp from user, validating their wallet + Authenticate(c echo.Context) error // Takes e-mail and wallet addr of user, validates email with twilio + Name(c echo.Context) error // Takes name and wallet addr of user, associates name with wallet addr + RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) +} + +type user struct { + Service service.User + Group *echo.Group +} + +func NewUser(route *echo.Echo, service service.User) User { + return &user{service, nil} +} + +func (u user) GetStatus(c echo.Context) error { + lg := c.Get("logger").(*zerolog.Logger) + var body model.UserRequest + err := c.Bind(&body) + if err != nil { + return c.String(http.StatusBadRequest, "Bad Request") + } + res, err := u.Service.GetStatus(body) + if err != nil { + lg.Err(err).Msg("user getstatus") + return c.String(http.StatusOK, "User Service Failed") + } + return c.JSON(http.StatusOK, res) +} + +func (u user) Create(c echo.Context) error { + lg := c.Get("logger").(*zerolog.Logger) + var body model.UserRequest + err := c.Bind(&body) + if err != nil { + return c.String(http.StatusBadRequest, "Bad Request") + } + err = u.Service.Create(body) + if err != nil { + lg.Err(err).Msg("user create") + return c.String(http.StatusOK, "User Service Failed") + } + return c.JSON(http.StatusOK, nil) +} + +func (u user) Sign(c echo.Context) error { + lg := c.Get("logger").(*zerolog.Logger) + var body model.UserRequest + err := c.Bind(&body) + if err != nil { + return c.String(http.StatusBadRequest, "Bad Request") + } + err = u.Service.Sign(body) + if err != nil { + lg.Err(err).Msg("user sign") + return c.String(http.StatusOK, "User Service Failed") + } + return c.JSON(http.StatusOK, nil) +} + +func (u user) Authenticate(c echo.Context) error { + lg := c.Get("logger").(*zerolog.Logger) + var body model.UserRequest + err := c.Bind(&body) + if err != nil { + return c.String(http.StatusBadRequest, "Bad Request") + } + err = u.Service.Authenticate(body) + if err != nil { + lg.Err(err).Msg("user authenticate") + return c.String(http.StatusOK, "User Service Failed") + } + return c.JSON(http.StatusOK, nil) +} + +func (u user) Name(c echo.Context) error { + lg := c.Get("logger").(*zerolog.Logger) + var body model.UserRequest + err := c.Bind(&body) + if err != nil { + return c.String(http.StatusBadRequest, "Bad Request") + } + err = u.Service.Name(body) + if err != nil { + lg.Err(err).Msg("user name") + return c.String(http.StatusOK, "User Service Failed") + } + return c.JSON(http.StatusOK, nil) +} + +func (u user) RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) { + if g == nil { + panic("No group attached to the User Handler") + } + u.Group = g + g.Use(ms...) + g.GET("/", u.GetStatus) + g.POST("/", u.Create) + g.PUT("/", u.Sign) + g.POST("/email", u.Authenticate) + g.POST("/name", u.Name) +} diff --git a/pkg/internal/common/evm.go b/pkg/internal/common/evm.go index e90460d3..a15566d7 100644 --- a/pkg/internal/common/evm.go +++ b/pkg/internal/common/evm.go @@ -1,11 +1,15 @@ package common import ( + "context" "errors" "math/big" + "regexp" "strconv" "strings" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/params" "github.com/lmittmann/w3" ) @@ -76,3 +80,22 @@ func WeiToEther(wei *big.Int) float64 { eth64, _ := ethBig.Float64() // OK to reduce precision? return eth64 } + +// TODO: Eventually make sure we support smart contract wallets +func IsWallet(addr string) bool { + RPC := "https://mainnet.infura.io/v3" // temporarily just use ETH mainnet + geth, _ := ethclient.Dial(RPC) + + re := regexp.MustCompile("^0x[0-9a-fA-F]{40}$") + valid := re.MatchString(addr) + if !valid { + return false + } + address := common.HexToAddress(addr) + bytecode, err := geth.CodeAt(context.Background(), address, nil) + if err != nil { + return false + } + isContract := len(bytecode) > 0 + return !isContract +} diff --git a/pkg/model/request.go b/pkg/model/request.go index a0501ee9..6a29da16 100644 --- a/pkg/model/request.go +++ b/pkg/model/request.go @@ -76,5 +76,25 @@ type UserPKLogin struct { Nonce string `json:"nonce"` } +type UserRequest struct { + WalletAddress string `json:"walletAddress"` + EmailAddress string `json:"emailAddress"` + FirstName string `json:"firstName"` + MiddleName string `json:"middleName"` + LastName string `json:"lastName"` + Signature string `json:"signature"` +} + +// Can be used for user, instrument, etc +type UpdateStatus struct { + Status string `json:"status"` +} + +type UpdateUserName struct { + FirstName string `json:"firstName"` + MiddleName string `json:"middleName"` + LastName string `json:"lastName"` +} + type EntityType string type AuthType string diff --git a/pkg/model/user.go b/pkg/model/user.go new file mode 100644 index 00000000..0281a95c --- /dev/null +++ b/pkg/model/user.go @@ -0,0 +1,5 @@ +package model + +type UserOnboardingStatus struct { + Status string `json:"status"` +} diff --git a/pkg/repository/contact.go b/pkg/repository/contact.go new file mode 100644 index 00000000..6ca1695c --- /dev/null +++ b/pkg/repository/contact.go @@ -0,0 +1,42 @@ +package repository + +import ( + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/jmoiron/sqlx" +) + +type Contact interface { + Transactable + Readable + Create(model.Contact) (model.Contact, error) + GetID(ID string) (model.User, error) + Update(ID string, updates any) error +} + +type contact[T any] struct { + base[T] +} + +func NewContact(db *sqlx.DB) User { + return &user[model.User]{base[model.User]{store: db, table: "string_user"}} +} + +func (c contact[T]) Create(insert model.Contact) (model.Contact, error) { + m := model.Contact{} + rows, err := c.store.NamedQuery(` + INSERT INTO contact (type, user_id, status) + VALUES(:type, :user_id, :status) RETURNING *`, insert) + if err != nil { + return m, common.StringError(err) + } + for rows.Next() { + err = rows.StructScan(&m) + if err != nil { + return m, common.StringError(err) + } + } + + defer rows.Close() + return m, nil +} diff --git a/pkg/repository/instrument.go b/pkg/repository/instrument.go index c7a2b32d..80a0cc80 100644 --- a/pkg/repository/instrument.go +++ b/pkg/repository/instrument.go @@ -1,6 +1,9 @@ package repository import ( + "database/sql" + "fmt" + "github.com/String-xyz/string-api/pkg/internal/common" "github.com/String-xyz/string-api/pkg/model" "github.com/jmoiron/sqlx" @@ -10,6 +13,7 @@ type Instrument interface { Transactable Create(model.Instrument) (model.Instrument, error) GetID(id string) (model.Instrument, error) + GetWallet(addr string) (model.Instrument, error) Update(ID string, updates any) error } @@ -24,8 +28,8 @@ func NewInstrument(db *sqlx.DB) Instrument { func (i instrument[T]) Create(insert model.Instrument) (model.Instrument, error) { m := model.Instrument{} rows, err := i.store.NamedQuery(` - INSERT INTO instrument (name) - VALUES(:name) RETURNING *`, insert) + INSERT INTO instrument (type, status, network, public_key, user_id) + VALUES(:type, :status, :network, :public_key, :user_id) RETURNING *`, insert) if err != nil { return m, common.StringError(err) } @@ -39,3 +43,12 @@ func (i instrument[T]) Create(insert model.Instrument) (model.Instrument, error) defer rows.Close() return m, nil } + +func (i instrument[T]) GetWallet(addr string) (model.Instrument, error) { + m := model.Instrument{} + err := i.store.Get(&m, fmt.Sprintf("SELECT * FROM %s WHERE public_key = $1", i.table), addr) + if err != nil && err == sql.ErrNoRows { + return m, common.StringError(ErrNotFound) + } + return m, nil +} diff --git a/pkg/repository/user.go b/pkg/repository/user.go index cb776e19..a0c714b7 100644 --- a/pkg/repository/user.go +++ b/pkg/repository/user.go @@ -26,8 +26,8 @@ func NewUser(db *sqlx.DB) User { func (u user[T]) Create(insert model.User) (model.User, error) { m := model.User{} rows, err := u.store.NamedQuery(` - INSERT INTO string_user (first_name, last_name, type, status) - VALUES(:first_name,:last_name, :type, :status) RETURNING *`, insert) + INSERT INTO string_user (type, status) + VALUES(:type, :status) RETURNING *`, insert) if err != nil { return m, common.StringError(err) } diff --git a/pkg/service/user.go b/pkg/service/user.go new file mode 100644 index 00000000..5839fb76 --- /dev/null +++ b/pkg/service/user.go @@ -0,0 +1,239 @@ +package service + +import ( + "math" + "net/mail" + "os" + "strconv" + "strings" + "time" + + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" + "github.com/pkg/errors" + "github.com/twilio/twilio-go" + verify "github.com/twilio/twilio-go/rest/verify/v2" +) + +type UserRequest = model.UserRequest + +type User interface { + GetStatus(request UserRequest) (model.UserOnboardingStatus, error) // If wallet addr is associated with user, return current state of their onboarding + Create(request UserRequest) error // Create new user using wallet addr, optionally mark as validated if signature is provided + Sign(request UserRequest) error // Takes in a signed timestamp from user, validating their wallet + Authenticate(request UserRequest) error // Takes e-mail and wallet addr of user, validates email with twilio + Name(request UserRequest) error // Takes name and wallet addr of user, associates name with wallet addr +} + +type user struct { + userRepo repository.User + contactRepo repository.Contact + instrumentRepo repository.Instrument +} + +func NewUser(u repository.User, c repository.Contact, i repository.Instrument) User { + return &user{userRepo: u, contactRepo: c, instrumentRepo: i} +} + +func (u user) GetStatus(request UserRequest) (model.UserOnboardingStatus, error) { + res := model.UserOnboardingStatus{Status: "Not Found"} + addr := request.WalletAddress + if addr == "" { + return res, common.StringError(errors.New("no wallet address provided")) + } + instrument, err := u.instrumentRepo.GetWallet(addr) + if err != nil { + return res, common.StringError(err) + } + associatedUser, err := u.userRepo.GetID(instrument.UserID) + if err != nil { + return res, common.StringError(err) + } + res.Status = associatedUser.Status + return res, nil +} + +func (u user) Create(request UserRequest) error { + addr := request.WalletAddress + if addr == "" { + return common.StringError(errors.New("no wallet address provided")) + } + + // Make sure wallet does not already exist + instrument, err := u.instrumentRepo.GetWallet(addr) + if err != nil && !strings.Contains(err.Error(), "not found") { // because we are wrapping error and care about its value + return common.StringError(err) + } else if err == nil && instrument.UserID != "" { + return common.StringError(errors.New("wallet already associated with user")) + } + + // Make sure address is a wallet and not a smart contract + if !common.IsWallet(addr) { + return common.StringError(errors.New("address provided is not a valid wallet")) + } + + // Optionally verify signature + status := "Created" + if request.Signature != "" { + valid, err := common.ValidateExternalEVMSignature(request.Signature, addr, addr) // they signed their own address. + // it's like writing your name on your hand and then xeroxing it + if err != nil { + return common.StringError(err) + } + if !valid { + return common.StringError(errors.New("signature invalid")) + } + status = "Validated" + } + + // Initialize a new user + user := model.User{Type: "String User", Status: "Created"} // Validated status pertains to specific instrument + user, err = u.userRepo.Create(user) + if err != nil { + return common.StringError(err) + } + + // Create a new wallet instrument and associate it with the new user + instrument = model.Instrument{Type: "Crypto Wallet", Status: status, Network: "EVM", PublicKey: addr, UserID: user.ID} + instrument, err = u.instrumentRepo.Create(instrument) + if err != nil { + return common.StringError(err) + } + return nil +} + +func (u user) Sign(request UserRequest) error { + addr := request.WalletAddress + // Make sure wallet exists + instrument, err := u.instrumentRepo.GetWallet(addr) + if err != nil { + return common.StringError(err) + } + // Make sure wallet is associated with a user + if instrument.UserID == "" { + return common.StringError(errors.New("wallet not associated with user")) + } + _, err = u.userRepo.GetID(instrument.UserID) // Don't need to update user, just verify they exist + if err != nil { + return common.StringError(err) + } + if instrument.Status == "Validated" { + return common.StringError(errors.New("wallet already validated")) + } + // Verify signature + valid, err := common.ValidateExternalEVMSignature(request.Signature, addr, addr) + if err != nil { + return common.StringError(err) + } + if !valid { + return common.StringError(errors.New("signature invalid")) + } + status := model.UpdateStatus{Status: "Validated"} + err = u.instrumentRepo.Update(instrument.ID, status) + if err != nil { + return common.StringError(err) + } + return nil +} + +func randomNumericString(digits int) string { + randomNumber := time.Now().Nanosecond() + randomString := "" + for i := digits - 1; i > -1; i++ { + // iterating through digits prevents truncating leading 0s from string + randomString += strconv.Itoa(randomNumber & int(math.Pow10(i))) + } + return randomString +} + +func (u user) Authenticate(request UserRequest) error { + addr := request.WalletAddress + email := request.EmailAddress + if addr == "" || email == "" { + return common.StringError(errors.New("missing wallet/email")) + } + _, err := mail.ParseAddress(email) + if err != nil { + return common.StringError(err) + } + + // Make sure wallet exists + instrument, err := u.instrumentRepo.GetWallet(addr) + if err != nil { + return common.StringError(err) + } + // Make sure wallet is associated with a user + if instrument.UserID == "" { + return common.StringError(errors.New("wallet not associated with user")) + } + user, err := u.userRepo.GetID(instrument.UserID) + if err != nil { + return common.StringError(err) + } + + // twilio / sendgrid stuff + var AUTH_SID = os.Getenv("TWILIO_AUTH_SID") + client := twilio.NewRestClient() + params := &verify.CreateVerificationParams{} + params.SetTo(email) + params.SetChannel("email") + code := randomNumericString(6) + params.SetCustomCode(code) // Code is needed to check if email was verified + _, err = client.VerifyV2.CreateVerification(AUTH_SID, params) + if err != nil { + return common.StringError(err) + } + + go u.waitForEmailAuthentication(addr, email, user.ID, code) + return nil +} + +func (u user) waitForEmailAuthentication(addr string, email string, userId string, code string) { + var AUTH_SID = os.Getenv("TWILIO_AUTH_SID") + client := twilio.NewRestClient() + params := &verify.CreateVerificationCheckParams{} + params.SetTo(email) + params.SetCode(code) + + // Wait for up to 15 minutes, final timeout TBD + start := time.Now().Unix() + end := start + (60 * 15) + currentTime := start + lastChecked := start + for currentTime < end { + currentTime = time.Now().Unix() + if currentTime > lastChecked { + resp, _ := client.VerifyV2.CreateVerificationCheck(AUTH_SID, params) + lastChecked = currentTime + if resp.Status != nil && *resp.Status == "approved" && resp.Valid != nil && *resp.Valid { + // Only create email entry in table once it's validated + contact := model.Contact{UserID: userId, Type: "email", Status: "validated", Data: email} + contact, _ = u.contactRepo.Create(contact) + return + } + } + } + +} + +func (u user) Name(request UserRequest) error { + addr := request.WalletAddress + instrument, err := u.instrumentRepo.GetWallet(addr) + if err != nil { + return common.StringError(err) + } + if instrument.UserID == "" { + return common.StringError(errors.New("wallet not associated with user")) + } + user, err := u.userRepo.GetID(instrument.UserID) + if err != nil { + return common.StringError(err) + } + updates := model.UpdateUserName{FirstName: request.FirstName, MiddleName: request.MiddleName, LastName: request.LastName} + err = u.userRepo.Update(user.ID, updates) + if err != nil { + return common.StringError(err) + } + return nil +} From 2f2308484769032ff696f1da95ca83e4b5db54a4 Mon Sep 17 00:00:00 2001 From: Auroter Date: Sat, 29 Oct 2022 21:33:50 -0700 Subject: [PATCH 02/12] all onboarding endpoints are functional --- .env.example | 4 +- api/api.go | 12 ++ api/handler/user.go | 13 ++ go.mod | 2 + go.sum | 4 + pkg/internal/common/base64.go | 29 +++++ pkg/internal/common/crypt.go | 66 ++++++++++ pkg/internal/common/crypt_test.go | 76 ++++++++++++ pkg/internal/common/evm.go | 2 +- pkg/internal/common/sign.go | 17 ++- pkg/internal/common/sign_test.go | 22 ++++ pkg/internal/unit21/entities_mapper.go | 11 +- pkg/model/entity.go | 72 +++++------ pkg/model/request.go | 8 +- pkg/repository/contact.go | 6 +- pkg/service/user.go | 161 ++++++++++++++----------- 16 files changed, 379 insertions(+), 126 deletions(-) create mode 100644 pkg/internal/common/base64.go create mode 100644 pkg/internal/common/crypt.go create mode 100644 pkg/internal/common/crypt_test.go create mode 100644 pkg/internal/common/sign_test.go diff --git a/.env.example b/.env.example index d26a29e0..6b432d12 100644 --- a/.env.example +++ b/.env.example @@ -27,4 +27,6 @@ TWILIO_ACCOUNT_SID=AC034879a536d54325687e48544403cb4d TWILIO_AUTH_TOKEN= TWILIO_SMS_SID=MG367a4f51ea6f67a28db4d126eefc734f TWILIO_AUTH_SID=VAc0221d2e2de51bbc28afe5349469a3e4 -DEV_PHONE_NUMBERS=+14088675309,+14155555555 \ No newline at end of file +DEV_PHONE_NUMBERS=+14088675309,+14155555555 +STRING_ENCRYPTION_KEY=secret_encryption_key_0123456789 +SENDGRID_API_KEY= \ No newline at end of file diff --git a/api/api.go b/api/api.go index 296a2561..80244630 100644 --- a/api/api.go +++ b/api/api.go @@ -31,6 +31,7 @@ func Start(config APIConfig) { authService := authRoute(config, e) platformRoute(config, e) transactRoute(config, authService, e) + userRoute(config, authService, e) e.Logger.Fatal(e.Start(":" + config.Port)) } @@ -74,3 +75,14 @@ func transactRoute(config APIConfig, auth service.Auth, e *echo.Echo) { handler := handler.NewTransaction(e, service) handler.RegisterRoutes(e.Group("/transact"), middleware.APIKeyAuth(auth), middleware.BearerAuth()) } + +func userRoute(config APIConfig, auth service.Auth, e *echo.Echo) { + repos := service.UserRepos{ + User: repository.NewUser(config.DB), + Contact: repository.NewContact(config.DB), + Instrument: repository.NewInstrument(config.DB), + } + service := service.NewUser(repos) + handler := handler.NewUser(e, service) + handler.RegisterRoutes(e.Group("/user"), middleware.APIKeyAuth(auth), middleware.BearerAuth()) +} diff --git a/api/handler/user.go b/api/handler/user.go index e07738cd..eeedb801 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -79,6 +79,19 @@ func (u user) Authenticate(c echo.Context) error { if err != nil { return c.String(http.StatusBadRequest, "Bad Request") } + + // Token was provided + token := c.QueryParam("token") + if token != "" { + err = u.Service.ReceiveEmailAuthentication(token) + if err != nil { + lg.Err(err).Msg("user authenticate") + return c.String(http.StatusOK, "User Service Failed") + } + return c.JSON(http.StatusOK, nil) + } + + // User needs a token err = u.Service.Authenticate(body) if err != nil { lg.Err(err).Msg("user authenticate") diff --git a/go.mod b/go.mod index ae406a62..382c093d 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,8 @@ require ( github.com/philhofer/fwd v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/tsdb v0.7.1 // indirect + github.com/sendgrid/rest v2.6.9+incompatible // indirect + github.com/sendgrid/sendgrid-go v3.12.0+incompatible // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/tinylib/msgp v1.1.2 // indirect diff --git a/go.sum b/go.sum index 43e1b9d4..f2808c16 100644 --- a/go.sum +++ b/go.sum @@ -193,6 +193,10 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= +github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= +github.com/sendgrid/sendgrid-go v3.12.0+incompatible h1:/N2vx18Fg1KmQOh6zESc5FJB8pYwt5QFBDflYPh1KVg= +github.com/sendgrid/sendgrid-go v3.12.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= 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/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= diff --git a/pkg/internal/common/base64.go b/pkg/internal/common/base64.go new file mode 100644 index 00000000..b0a2a088 --- /dev/null +++ b/pkg/internal/common/base64.go @@ -0,0 +1,29 @@ +package common + +import ( + "encoding/base64" + "encoding/json" + "fmt" +) + +func EncodeToBase64(object interface{}) (string, error) { + buffer, err := json.Marshal(object) + if err != nil { + return "", StringError(err) + } + return base64.StdEncoding.EncodeToString(buffer), nil +} + +func DecodeFromBase64[T any](from string) (T, error) { + var result *T = new(T) + buffer, err := base64.StdEncoding.DecodeString(from) + if err != nil { + fmt.Printf("\nFROM=%+v", from) + return *result, StringError(err) + } + err = json.Unmarshal(buffer, &result) + if err != nil { + return *result, StringError(err) + } + return *result, nil +} diff --git a/pkg/internal/common/crypt.go b/pkg/internal/common/crypt.go new file mode 100644 index 00000000..b507de92 --- /dev/null +++ b/pkg/internal/common/crypt.go @@ -0,0 +1,66 @@ +package common + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/json" + "io" +) + +func Encrypt(object interface{}, secret string) (string, error) { + buffer, err := json.Marshal(object) + if err != nil { + return "", StringError(err) + } + return EncryptString(string(buffer), secret) +} + +func Decrypt[T any](from string, secret string) (T, error) { + var result *T = new(T) + decrypted, err := DecryptString(from, secret) + if err != nil { + return *result, StringError(err) + } + err = json.Unmarshal([]byte(decrypted), &result) + if err != nil { + return *result, StringError(err) + } + return *result, nil +} + +func EncryptString(data string, secret string) (string, error) { + block, err := aes.NewCipher([]byte(secret)) + if err != nil { + return "", StringError(err) + } + plainText := []byte(data) + cipherText := make([]byte, aes.BlockSize+len(plainText)) + iv := cipherText[:aes.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return "", StringError(err) + } + cfb := cipher.NewCFBEncrypter(block, iv) + cfb.XORKeyStream(cipherText[aes.BlockSize:], plainText) + return base64.StdEncoding.EncodeToString(cipherText), nil +} + +func DecryptString(data string, secret string) (string, error) { + block, err := aes.NewCipher([]byte(secret)) + if err != nil { + return "", StringError(err) + } + cipherText, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return "", StringError(err) + } + iv := cipherText[:aes.BlockSize] + + cipherText = cipherText[aes.BlockSize:] + + cfb := cipher.NewCFBDecrypter(block, iv) + plainText := make([]byte, len(cipherText)) + cfb.XORKeyStream(plainText, cipherText) + return string(plainText), nil +} diff --git a/pkg/internal/common/crypt_test.go b/pkg/internal/common/crypt_test.go new file mode 100644 index 00000000..be87e432 --- /dev/null +++ b/pkg/internal/common/crypt_test.go @@ -0,0 +1,76 @@ +package common + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type randomObject1 struct { + Timestamp int64 `json:"timestamp"` + Email string `json:"email"` + Address string `json:"address"` +} + +func TestEncodeDecodeString(t *testing.T) { + obj1 := "this is a test" + + obj1Encoded, err := EncodeToBase64(obj1) + assert.NoError(t, err) + + obj1Decoded, err := DecodeFromBase64[string](obj1Encoded) + assert.NoError(t, err) + assert.Equal(t, obj1, obj1Decoded) +} + +func TestEncodeDecodeObject(t *testing.T) { + obj2 := randomObject1{Timestamp: time.Now().Unix(), Email: "test@test.com", Address: "0xdecafbabe"} + + obj2Encoded, err := EncodeToBase64(obj2) + assert.NoError(t, err) + + obj2Decoded, err := DecodeFromBase64[randomObject1](obj2Encoded) + assert.NoError(t, err) + assert.Equal(t, obj2, obj2Decoded) +} + +func TestEncryptDecryptString(t *testing.T) { + str := "this is a string" + + strEncrypted, err := EncryptString(str, "secret_encryption_key_0123456789") + assert.NoError(t, err) + + strDecrypted, err := DecryptString(strEncrypted, "secret_encryption_key_0123456789") + assert.NoError(t, err) + + assert.Equal(t, str, strDecrypted) +} + +func TestEncryptDecryptObject(t *testing.T) { + obj := randomObject1{Timestamp: time.Now().Unix(), Email: "test@test.com", Address: "0xdecafbabe"} + + objEncoded, err := EncodeToBase64(obj) + assert.NoError(t, err) + + objEncrypted, err := EncryptString(objEncoded, "secret_encryption_key_0123456789") + assert.NoError(t, err) + + objDecrypted, err := DecryptString(objEncrypted, "secret_encryption_key_0123456789") + assert.NoError(t, err) + + objDecoded, err := DecodeFromBase64[randomObject1](objDecrypted) + assert.NoError(t, err) + assert.Equal(t, obj, objDecoded) +} + +func TestEncryptDecryptUnencoded(t *testing.T) { + obj := randomObject1{Timestamp: time.Now().Unix(), Email: "test@test.com", Address: "0xdecafbabe"} + + objEncrypted, err := Encrypt(obj, "secret_encryption_key_0123456789") + assert.NoError(t, err) + + objDecrypted, err := Decrypt[randomObject1](objEncrypted, "secret_encryption_key_0123456789") + assert.NoError(t, err) + assert.Equal(t, obj, objDecrypted) +} diff --git a/pkg/internal/common/evm.go b/pkg/internal/common/evm.go index a15566d7..e717eb76 100644 --- a/pkg/internal/common/evm.go +++ b/pkg/internal/common/evm.go @@ -83,7 +83,7 @@ func WeiToEther(wei *big.Int) float64 { // TODO: Eventually make sure we support smart contract wallets func IsWallet(addr string) bool { - RPC := "https://mainnet.infura.io/v3" // temporarily just use ETH mainnet + RPC := "https://rpc.ankr.com/eth" // temporarily just use ETH mainnet geth, _ := ethclient.Dial(RPC) re := regexp.MustCompile("^0x[0-9a-fA-F]{40}$") diff --git a/pkg/internal/common/sign.go b/pkg/internal/common/sign.go index 911499ee..070b3c04 100644 --- a/pkg/internal/common/sign.go +++ b/pkg/internal/common/sign.go @@ -4,6 +4,7 @@ import ( "crypto/ecdsa" "encoding/json" "errors" + "fmt" "os" "github.com/ethereum/go-ethereum/common" @@ -25,6 +26,7 @@ func EVMSign(data interface{}) (string, error) { if err != nil { return "", StringError(err) } + fmt.Printf("\nSIGNED=%+v", hexutil.Encode(signature)) return hexutil.Encode(signature), nil } @@ -50,8 +52,7 @@ func ValidateEVMSignature(signature string, data interface{}) (bool, error) { if err != nil { return false, StringError(err) } - sigBytes = sigBytes[:len(sigBytes)-1] // last byte is a recovery ID - verified := crypto.VerifySignature(pkBytes, hash.Bytes(), sigBytes) + verified := crypto.VerifySignature(pkBytes, hash.Bytes(), sigBytes[:len(sigBytes)-1]) // last byte of signature is recovery ID return verified, nil } @@ -66,14 +67,18 @@ func ValidateExternalEVMSignature(signature string, address string, data interfa if err != nil { return false, StringError(err) } - sigBytes = sigBytes[:len(sigBytes)-1] // last byte is a recovery ID - addrBytes, err := hexutil.Decode(address) + sigPKECDSA, err := crypto.SigToPub(hash.Bytes(), sigBytes) if err != nil { return false, StringError(err) } - addrBytes = addrBytes[:len(addrBytes)-1] // last byte is a recovery ID + sigPKBytes := crypto.FromECDSAPub(sigPKECDSA) - verified := crypto.VerifySignature(addrBytes, hash.Bytes(), sigBytes) + SIGPKString := crypto.PubkeyToAddress(*sigPKECDSA).String() + if address != SIGPKString { + return false, nil + } + + verified := crypto.VerifySignature(sigPKBytes, hash.Bytes(), sigBytes[:len(sigBytes)-1]) // last byte of signature is recovery ID return verified, nil } diff --git a/pkg/internal/common/sign_test.go b/pkg/internal/common/sign_test.go new file mode 100644 index 00000000..7dae1933 --- /dev/null +++ b/pkg/internal/common/sign_test.go @@ -0,0 +1,22 @@ +package common + +import ( + "testing" + + "github.com/joho/godotenv" + "github.com/stretchr/testify/assert" +) + +func SignAndValidateString(t *testing.T) { + err := godotenv.Load("../../.env") + assert.NoError(t, err) + + obj1 := "test string" + + obj1Signed, err := EVMSign(obj1) + assert.NoError(t, err) + + valid, err := ValidateEVMSignature(obj1Signed, obj1) + assert.NoError(t, err) + assert.Equal(t, false, valid) +} diff --git a/pkg/internal/unit21/entities_mapper.go b/pkg/internal/unit21/entities_mapper.go index dcfdf9dc..688e2fbb 100644 --- a/pkg/internal/unit21/entities_mapper.go +++ b/pkg/internal/unit21/entities_mapper.go @@ -4,11 +4,12 @@ import "github.com/String-xyz/string-api/pkg/model" func MapUserToEntity(user model.User) *u21entity { var userTagArr []string - if user.Tags != nil { - for key, value := range user.Tags { - userTagArr = append(userTagArr, key+":"+value) - } - } + // Temporarily disabled + // if user.Tags != nil { + // for key, value := range user.Tags { + // userTagArr = append(userTagArr, key+":"+value) + // } + // } jsonBody := &u21entity{ GeneralData: &general{ diff --git a/pkg/model/entity.go b/pkg/model/entity.go index 55de9a94..42b925c1 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -10,16 +10,16 @@ import ( // See STRING_USER in Migrations 0001 type User struct { - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` - Type string `json:"type" db:"type"` - Status string `json:"status" db:"status"` - Tags map[string]string `json:"tags" db:"tags"` - FirstName string `json:"firstName" db:"first_name"` - MiddleName string `json:"middleName" db:"middle_name"` - LastName string `json:"lastName" db:"last_name"` + ID string `json:"id" db:"id"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` + Type string `json:"type" db:"type"` + Status string `json:"status" db:"status"` + Tags types.JSONText `json:"tags" db:"tags"` + FirstName string `json:"firstName" db:"first_name"` + MiddleName string `json:"middleName" db:"middle_name"` + LastName string `json:"lastName" db:"last_name"` } // See PLATFORM in Migrations 0001 @@ -91,36 +91,36 @@ type Contact struct { // See LOCATION in Migrations 0002 type Location struct { - ID string `json:"id" db:"id"` - UserID string `json:"userId" db:"user_id"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - Type string `json:"type" db:"type"` - Status string `json:"status" db:"status"` - Tags map[string]string `json:"tags" db:"tags"` - BuildingNumber string `json:"buildingNumber" db:"building_number"` - UnitNumber string `json:"unitNumber" db:"unit_number"` - StreetName string `json:"streetName" db:"street_name"` - City string `json:"city" db:"city"` - State string `json:"state" db:"state"` - PostalCode string `json:"postalCode" db:"postal_code"` - Country string `json:"country" db:"country"` + ID string `json:"id" db:"id"` + UserID string `json:"userId" db:"user_id"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + Type string `json:"type" db:"type"` + Status string `json:"status" db:"status"` + Tags types.JSONText `json:"tags" db:"tags"` + BuildingNumber string `json:"buildingNumber" db:"building_number"` + UnitNumber string `json:"unitNumber" db:"unit_number"` + StreetName string `json:"streetName" db:"street_name"` + City string `json:"city" db:"city"` + State string `json:"state" db:"state"` + PostalCode string `json:"postalCode" db:"postal_code"` + Country string `json:"country" db:"country"` } // See INSTRUMENT in Migrations 0002 type Instrument struct { - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` - Type string `json:"type" db:"type"` - Status string `json:"status" db:"status"` - Tags map[string]string `json:"tags" db:"tags"` - Network string `json:"network" db:"network"` - PublicKey string `json:"publicKey" db:"public_key"` - Last4 string `json:"last4" db:"last_4"` - UserID string `json:"userId" db:"user_id"` - LocationID string `json:"locationId" db:"location_id"` + ID string `json:"id" db:"id"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` + Type string `json:"type" db:"type"` + Status string `json:"status" db:"status"` + Tags types.JSONText `json:"tags" db:"tags"` + Network string `json:"network" db:"network"` + PublicKey string `json:"publicKey" db:"public_key"` + Last4 string `json:"last4" db:"last_4"` + UserID string `json:"userId" db:"user_id"` + LocationID sql.NullString `json:"locationId" db:"location_id"` } // See CONTACT_PLATFORM in Migrations 0003 diff --git a/pkg/model/request.go b/pkg/model/request.go index 6a29da16..c2b5b467 100644 --- a/pkg/model/request.go +++ b/pkg/model/request.go @@ -87,13 +87,13 @@ type UserRequest struct { // Can be used for user, instrument, etc type UpdateStatus struct { - Status string `json:"status"` + Status *string `json:"status" db:"status"` } type UpdateUserName struct { - FirstName string `json:"firstName"` - MiddleName string `json:"middleName"` - LastName string `json:"lastName"` + FirstName string `json:"firstName" db:"first_name"` + MiddleName string `json:"middleName" db:"middle_name"` + LastName string `json:"lastName" db:"last_name"` } type EntityType string diff --git a/pkg/repository/contact.go b/pkg/repository/contact.go index 6ca1695c..0fb0347d 100644 --- a/pkg/repository/contact.go +++ b/pkg/repository/contact.go @@ -10,7 +10,7 @@ type Contact interface { Transactable Readable Create(model.Contact) (model.Contact, error) - GetID(ID string) (model.User, error) + GetID(ID string) (model.Contact, error) Update(ID string, updates any) error } @@ -18,8 +18,8 @@ type contact[T any] struct { base[T] } -func NewContact(db *sqlx.DB) User { - return &user[model.User]{base[model.User]{store: db, table: "string_user"}} +func NewContact(db *sqlx.DB) Contact { + return &contact[model.Contact]{base[model.Contact]{store: db, table: "contact"}} } func (c contact[T]) Create(insert model.Contact) (model.Contact, error) { diff --git a/pkg/service/user.go b/pkg/service/user.go index 5839fb76..03dc93d9 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -1,10 +1,9 @@ package service import ( - "math" - "net/mail" + "fmt" + netMail "net/mail" "os" - "strconv" "strings" "time" @@ -12,28 +11,39 @@ import ( "github.com/String-xyz/string-api/pkg/model" "github.com/String-xyz/string-api/pkg/repository" "github.com/pkg/errors" - "github.com/twilio/twilio-go" - verify "github.com/twilio/twilio-go/rest/verify/v2" + "github.com/sendgrid/sendgrid-go" + "github.com/sendgrid/sendgrid-go/helpers/mail" ) type UserRequest = model.UserRequest +type EmailVerification struct { + Timestamp int64 + Email string + UserID string +} + +type UserRepos struct { + User repository.User + Contact repository.Contact + Instrument repository.Instrument +} + type User interface { GetStatus(request UserRequest) (model.UserOnboardingStatus, error) // If wallet addr is associated with user, return current state of their onboarding Create(request UserRequest) error // Create new user using wallet addr, optionally mark as validated if signature is provided Sign(request UserRequest) error // Takes in a signed timestamp from user, validating their wallet Authenticate(request UserRequest) error // Takes e-mail and wallet addr of user, validates email with twilio + ReceiveEmailAuthentication(encrypted string) error // Decrypts query and validates e-mail address of user Name(request UserRequest) error // Takes name and wallet addr of user, associates name with wallet addr } type user struct { - userRepo repository.User - contactRepo repository.Contact - instrumentRepo repository.Instrument + repos UserRepos } -func NewUser(u repository.User, c repository.Contact, i repository.Instrument) User { - return &user{userRepo: u, contactRepo: c, instrumentRepo: i} +func NewUser(repos UserRepos) User { + return &user{repos: repos} } func (u user) GetStatus(request UserRequest) (model.UserOnboardingStatus, error) { @@ -42,11 +52,11 @@ func (u user) GetStatus(request UserRequest) (model.UserOnboardingStatus, error) if addr == "" { return res, common.StringError(errors.New("no wallet address provided")) } - instrument, err := u.instrumentRepo.GetWallet(addr) + instrument, err := u.repos.Instrument.GetWallet(addr) if err != nil { return res, common.StringError(err) } - associatedUser, err := u.userRepo.GetID(instrument.UserID) + associatedUser, err := u.repos.User.GetID(instrument.UserID) if err != nil { return res, common.StringError(err) } @@ -61,11 +71,15 @@ func (u user) Create(request UserRequest) error { } // Make sure wallet does not already exist - instrument, err := u.instrumentRepo.GetWallet(addr) + // TODO: Revisit this logic. Parsing error string for "not found" is bad. + instrument, err := u.repos.Instrument.GetWallet(addr) + fmt.Printf("\nINSTRUMENT=%+v", instrument) if err != nil && !strings.Contains(err.Error(), "not found") { // because we are wrapping error and care about its value return common.StringError(err) } else if err == nil && instrument.UserID != "" { return common.StringError(errors.New("wallet already associated with user")) + } else if err == nil && instrument.PublicKey == addr { + return common.StringError(errors.New("wallet already exists")) } // Make sure address is a wallet and not a smart contract @@ -89,14 +103,14 @@ func (u user) Create(request UserRequest) error { // Initialize a new user user := model.User{Type: "String User", Status: "Created"} // Validated status pertains to specific instrument - user, err = u.userRepo.Create(user) + user, err = u.repos.User.Create(user) if err != nil { return common.StringError(err) } // Create a new wallet instrument and associate it with the new user instrument = model.Instrument{Type: "Crypto Wallet", Status: status, Network: "EVM", PublicKey: addr, UserID: user.ID} - instrument, err = u.instrumentRepo.Create(instrument) + instrument, err = u.repos.Instrument.Create(instrument) if err != nil { return common.StringError(err) } @@ -106,7 +120,7 @@ func (u user) Create(request UserRequest) error { func (u user) Sign(request UserRequest) error { addr := request.WalletAddress // Make sure wallet exists - instrument, err := u.instrumentRepo.GetWallet(addr) + instrument, err := u.repos.Instrument.GetWallet(addr) if err != nil { return common.StringError(err) } @@ -114,13 +128,18 @@ func (u user) Sign(request UserRequest) error { if instrument.UserID == "" { return common.StringError(errors.New("wallet not associated with user")) } - _, err = u.userRepo.GetID(instrument.UserID) // Don't need to update user, just verify they exist + _, err = u.repos.User.GetID(instrument.UserID) // Don't need to update user, just verify they exist if err != nil { return common.StringError(err) } if instrument.Status == "Validated" { return common.StringError(errors.New("wallet already validated")) } + + // TESTING + // signed, _ := common.EVMSign(addr) + // fmt.Printf("\n\nSECRET SIGNATURE=%+v", signed) + // Verify signature valid, err := common.ValidateExternalEVMSignature(request.Signature, addr, addr) if err != nil { @@ -129,37 +148,28 @@ func (u user) Sign(request UserRequest) error { if !valid { return common.StringError(errors.New("signature invalid")) } - status := model.UpdateStatus{Status: "Validated"} - err = u.instrumentRepo.Update(instrument.ID, status) + validated := "Validated" + status := model.UpdateStatus{Status: &validated} + err = u.repos.Instrument.Update(instrument.ID, status) if err != nil { return common.StringError(err) } return nil } -func randomNumericString(digits int) string { - randomNumber := time.Now().Nanosecond() - randomString := "" - for i := digits - 1; i > -1; i++ { - // iterating through digits prevents truncating leading 0s from string - randomString += strconv.Itoa(randomNumber & int(math.Pow10(i))) - } - return randomString -} - func (u user) Authenticate(request UserRequest) error { addr := request.WalletAddress email := request.EmailAddress if addr == "" || email == "" { return common.StringError(errors.New("missing wallet/email")) } - _, err := mail.ParseAddress(email) + _, err := netMail.ParseAddress(email) if err != nil { return common.StringError(err) } // Make sure wallet exists - instrument, err := u.instrumentRepo.GetWallet(addr) + instrument, err := u.repos.Instrument.GetWallet(addr) if err != nil { return common.StringError(err) } @@ -167,71 +177,82 @@ func (u user) Authenticate(request UserRequest) error { if instrument.UserID == "" { return common.StringError(errors.New("wallet not associated with user")) } - user, err := u.userRepo.GetID(instrument.UserID) + // user, err := u.repos.User.GetID(instrument.UserID) + // if err != nil { + // return common.StringError(err) + // } + + // twilio / sendgrid stuff + // key := os.Getenv("STRING_ENCRYPTION_KEY") + // expectedResponse, err := common.Encrypt(EmailVerification{Timestamp: time.Now().Unix(), Email: email, UserID: instrument.UserID}, key) + // if err != nil { + // return common.StringError(err) + // } + // var AUTH_SID = os.Getenv("TWILIO_AUTH_SID") + // client := twilio.NewRestClient() + // params := &verify.CreateVerificationParams{} + // params.SetTo(email) + // params.SetChannel("email") + // params.SetCustomCode(expectedResponse) // Code is needed to check if email was verified + // _, err = client.VerifyV2.CreateVerification(AUTH_SID, params) + // if err != nil { + // return common.StringError(err) + // } + key := os.Getenv("STRING_ENCRYPTION_KEY") + code, err := common.Encrypt(EmailVerification{Timestamp: time.Now().Unix(), Email: email, UserID: instrument.UserID}, key) if err != nil { return common.StringError(err) } - - // twilio / sendgrid stuff - var AUTH_SID = os.Getenv("TWILIO_AUTH_SID") - client := twilio.NewRestClient() - params := &verify.CreateVerificationParams{} - params.SetTo(email) - params.SetChannel("email") - code := randomNumericString(6) - params.SetCustomCode(code) // Code is needed to check if email was verified - _, err = client.VerifyV2.CreateVerification(AUTH_SID, params) + from := mail.NewEmail("String Authentication", "auth@string.xyz") + subject := "String Email Authentication" + to := mail.NewEmail("New String User", email) + textContent := "Click the link below to complete your e-mail authentication!" + htmlContent := "

Verify Email Now
" + message := mail.NewSingleEmail(from, subject, to, textContent, htmlContent) + client := sendgrid.NewSendClient(os.Getenv("SENDGRID_API_KEY")) + response, err := client.Send(message) if err != nil { return common.StringError(err) } - - go u.waitForEmailAuthentication(addr, email, user.ID, code) + fmt.Printf("\nResponse Status Code = %+v", response.StatusCode) + fmt.Printf("\nResponse Body = %+v", response.Body) + fmt.Printf("\nResponse Headers = %+v", response.Headers) + // success return nil } -func (u user) waitForEmailAuthentication(addr string, email string, userId string, code string) { - var AUTH_SID = os.Getenv("TWILIO_AUTH_SID") - client := twilio.NewRestClient() - params := &verify.CreateVerificationCheckParams{} - params.SetTo(email) - params.SetCode(code) +func (u user) ReceiveEmailAuthentication(encrypted string) error { + key := os.Getenv("STRING_ENCRYPTION_KEY") + received, err := common.Decrypt[EmailVerification](encrypted, key) + if err != nil { + return common.StringError(err) + } // Wait for up to 15 minutes, final timeout TBD - start := time.Now().Unix() - end := start + (60 * 15) - currentTime := start - lastChecked := start - for currentTime < end { - currentTime = time.Now().Unix() - if currentTime > lastChecked { - resp, _ := client.VerifyV2.CreateVerificationCheck(AUTH_SID, params) - lastChecked = currentTime - if resp.Status != nil && *resp.Status == "approved" && resp.Valid != nil && *resp.Valid { - // Only create email entry in table once it's validated - contact := model.Contact{UserID: userId, Type: "email", Status: "validated", Data: email} - contact, _ = u.contactRepo.Create(contact) - return - } - } + now := time.Now().Unix() + if now-received.Timestamp > (60 * 15) { + return common.StringError(errors.New("link expired")) } - + contact := model.Contact{UserID: received.UserID, Type: "email", Status: "validated", Data: received.Email} + contact, _ = u.repos.Contact.Create(contact) + return nil } func (u user) Name(request UserRequest) error { addr := request.WalletAddress - instrument, err := u.instrumentRepo.GetWallet(addr) + instrument, err := u.repos.Instrument.GetWallet(addr) if err != nil { return common.StringError(err) } if instrument.UserID == "" { return common.StringError(errors.New("wallet not associated with user")) } - user, err := u.userRepo.GetID(instrument.UserID) + user, err := u.repos.User.GetID(instrument.UserID) if err != nil { return common.StringError(err) } updates := model.UpdateUserName{FirstName: request.FirstName, MiddleName: request.MiddleName, LastName: request.LastName} - err = u.userRepo.Update(user.ID, updates) + err = u.repos.User.Update(user.ID, updates) if err != nil { return common.StringError(err) } From ba3aed17b11581196896cb11f94c3f6f6a0a88cb Mon Sep 17 00:00:00 2001 From: Auroter Date: Mon, 31 Oct 2022 12:38:48 -0700 Subject: [PATCH 03/12] minor code cleanup --- .env.example | 1 - pkg/internal/common/base64.go | 2 -- pkg/service/user.go | 41 ++++++++++------------------------- 3 files changed, 12 insertions(+), 32 deletions(-) diff --git a/.env.example b/.env.example index 6b432d12..e4ba1e23 100644 --- a/.env.example +++ b/.env.example @@ -26,7 +26,6 @@ UNIT21_URL=https://sandbox2-api.unit21.com/v1/ TWILIO_ACCOUNT_SID=AC034879a536d54325687e48544403cb4d TWILIO_AUTH_TOKEN= TWILIO_SMS_SID=MG367a4f51ea6f67a28db4d126eefc734f -TWILIO_AUTH_SID=VAc0221d2e2de51bbc28afe5349469a3e4 DEV_PHONE_NUMBERS=+14088675309,+14155555555 STRING_ENCRYPTION_KEY=secret_encryption_key_0123456789 SENDGRID_API_KEY= \ No newline at end of file diff --git a/pkg/internal/common/base64.go b/pkg/internal/common/base64.go index b0a2a088..8477b6b7 100644 --- a/pkg/internal/common/base64.go +++ b/pkg/internal/common/base64.go @@ -3,7 +3,6 @@ package common import ( "encoding/base64" "encoding/json" - "fmt" ) func EncodeToBase64(object interface{}) (string, error) { @@ -18,7 +17,6 @@ func DecodeFromBase64[T any](from string) (T, error) { var result *T = new(T) buffer, err := base64.StdEncoding.DecodeString(from) if err != nil { - fmt.Printf("\nFROM=%+v", from) return *result, StringError(err) } err = json.Unmarshal(buffer, &result) diff --git a/pkg/service/user.go b/pkg/service/user.go index 03dc93d9..74330156 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -1,7 +1,6 @@ package service import ( - "fmt" netMail "net/mail" "os" "strings" @@ -73,7 +72,6 @@ func (u user) Create(request UserRequest) error { // Make sure wallet does not already exist // TODO: Revisit this logic. Parsing error string for "not found" is bad. instrument, err := u.repos.Instrument.GetWallet(addr) - fmt.Printf("\nINSTRUMENT=%+v", instrument) if err != nil && !strings.Contains(err.Error(), "not found") { // because we are wrapping error and care about its value return common.StringError(err) } else if err == nil && instrument.UserID != "" { @@ -136,7 +134,7 @@ func (u user) Sign(request UserRequest) error { return common.StringError(errors.New("wallet already validated")) } - // TESTING + // @dev Uncomment these lines to sign payload without using front end // signed, _ := common.EVMSign(addr) // fmt.Printf("\n\nSECRET SIGNATURE=%+v", signed) @@ -177,46 +175,31 @@ func (u user) Authenticate(request UserRequest) error { if instrument.UserID == "" { return common.StringError(errors.New("wallet not associated with user")) } - // user, err := u.repos.User.GetID(instrument.UserID) - // if err != nil { - // return common.StringError(err) - // } - - // twilio / sendgrid stuff - // key := os.Getenv("STRING_ENCRYPTION_KEY") - // expectedResponse, err := common.Encrypt(EmailVerification{Timestamp: time.Now().Unix(), Email: email, UserID: instrument.UserID}, key) - // if err != nil { - // return common.StringError(err) - // } - // var AUTH_SID = os.Getenv("TWILIO_AUTH_SID") - // client := twilio.NewRestClient() - // params := &verify.CreateVerificationParams{} - // params.SetTo(email) - // params.SetChannel("email") - // params.SetCustomCode(expectedResponse) // Code is needed to check if email was verified - // _, err = client.VerifyV2.CreateVerification(AUTH_SID, params) - // if err != nil { - // return common.StringError(err) - // } + // Encrypt required data to Base64 string and insert it in an email hyperlink key := os.Getenv("STRING_ENCRYPTION_KEY") code, err := common.Encrypt(EmailVerification{Timestamp: time.Now().Unix(), Email: email, UserID: instrument.UserID}, key) if err != nil { return common.StringError(err) } + baseURL := "http://localhost:5555/" + env := os.Getenv("ENV") + if env == "dev" { + baseURL = "https://app.dev.string-api.xyz/" + } else { + baseURL = "https://app.string-api.xyz/" + } + from := mail.NewEmail("String Authentication", "auth@string.xyz") subject := "String Email Authentication" to := mail.NewEmail("New String User", email) textContent := "Click the link below to complete your e-mail authentication!" - htmlContent := "

Verify Email Now
" + htmlContent := "

Verify Email Now
" message := mail.NewSingleEmail(from, subject, to, textContent, htmlContent) client := sendgrid.NewSendClient(os.Getenv("SENDGRID_API_KEY")) - response, err := client.Send(message) + _, err = client.Send(message) if err != nil { return common.StringError(err) } - fmt.Printf("\nResponse Status Code = %+v", response.StatusCode) - fmt.Printf("\nResponse Body = %+v", response.Body) - fmt.Printf("\nResponse Headers = %+v", response.Headers) // success return nil } From a08b430711d84a5f14a2bd48c5afcc66f0521f07 Mon Sep 17 00:00:00 2001 From: Auroter Date: Mon, 31 Oct 2022 13:59:18 -0700 Subject: [PATCH 04/12] Fix Unit21 crash --- pkg/service/auth.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/service/auth.go b/pkg/service/auth.go index d6d4aac1..1d66cb4e 100644 --- a/pkg/service/auth.go +++ b/pkg/service/auth.go @@ -8,7 +8,6 @@ import ( "time" "github.com/String-xyz/string-api/pkg/internal/common" - "github.com/String-xyz/string-api/pkg/internal/unit21" "github.com/String-xyz/string-api/pkg/model" "github.com/String-xyz/string-api/pkg/repository" "github.com/golang-jwt/jwt/v4" @@ -99,12 +98,12 @@ func (a auth) Register(m UserRegister) (JWT, error) { return JWT{}, common.StringError(err) } - // Share with Unit21 - entityRepo := unit21.NewEntity(a.userRepo, a.contactRepo) - _, err = entityRepo.Create(user) // Discard Unit21 ID - if err != nil { - return JWT{}, common.StringError(err) - } + // // Share with Unit21 + // entityRepo := unit21.NewEntity(a.userRepo, a.contactRepo) + // _, err = entityRepo.Create(user) // Discard Unit21 ID + // if err != nil { + // return JWT{}, common.StringError(err) + // } return a.GenerateJWT(user) } From e45cff3df2774c19ca17f7247edc909061c98d68 Mon Sep 17 00:00:00 2001 From: Auroter Date: Mon, 31 Oct 2022 14:13:58 -0700 Subject: [PATCH 05/12] removed erroneous printf --- pkg/internal/common/sign.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/internal/common/sign.go b/pkg/internal/common/sign.go index 070b3c04..f0568a82 100644 --- a/pkg/internal/common/sign.go +++ b/pkg/internal/common/sign.go @@ -4,7 +4,6 @@ import ( "crypto/ecdsa" "encoding/json" "errors" - "fmt" "os" "github.com/ethereum/go-ethereum/common" @@ -26,7 +25,6 @@ func EVMSign(data interface{}) (string, error) { if err != nil { return "", StringError(err) } - fmt.Printf("\nSIGNED=%+v", hexutil.Encode(signature)) return hexutil.Encode(signature), nil } From 2467d23da41bdeddad0ac13bda1661be5dd33771 Mon Sep 17 00:00:00 2001 From: Auroter Date: Mon, 31 Oct 2022 14:39:13 -0700 Subject: [PATCH 06/12] Added wallet address checksum validation on User Create --- pkg/internal/common/evm.go | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/pkg/internal/common/evm.go b/pkg/internal/common/evm.go index e717eb76..15835ddb 100644 --- a/pkg/internal/common/evm.go +++ b/pkg/internal/common/evm.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/params" "github.com/lmittmann/w3" + "golang.org/x/crypto/sha3" ) func ParseEncoding(function *w3.Func, signature string, params []string) ([]byte, error) { @@ -86,11 +87,13 @@ func IsWallet(addr string) bool { RPC := "https://rpc.ankr.com/eth" // temporarily just use ETH mainnet geth, _ := ethclient.Dial(RPC) - re := regexp.MustCompile("^0x[0-9a-fA-F]{40}$") - valid := re.MatchString(addr) - if !valid { + if !validAddress(addr) { + return false + } + if !validChecksum(addr) { return false } + address := common.HexToAddress(addr) bytecode, err := geth.CodeAt(context.Background(), address, nil) if err != nil { @@ -99,3 +102,27 @@ func IsWallet(addr string) bool { isContract := len(bytecode) > 0 return !isContract } + +func validChecksum(addr string) bool { + lowerCase := strings.ToLower(addr)[2:] + hash := sha3.NewLegacyKeccak256() + hash.Write([]byte(lowerCase)) + hashBytes := hash.Sum(nil) + + valid := "0x" + for i, b := range lowerCase { + c := string(b) + if b < '0' || b > '9' { + if hashBytes[i/2]&byte(128-i%2*120) != 0 { + c = string(b - 32) + } + } + valid += c + } + return addr == valid +} + +func validAddress(addr string) bool { + re := regexp.MustCompile("^0x[0-9a-fA-F]{40}$") + return re.MatchString(addr) +} From 7acee8d7dc08dc39e86ee94c69ad728694db04c8 Mon Sep 17 00:00:00 2001 From: Auroter Date: Tue, 1 Nov 2022 12:27:38 -0700 Subject: [PATCH 07/12] addressed some feedback --- api/handler/user.go | 2 +- pkg/service/user.go | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/api/handler/user.go b/api/handler/user.go index eeedb801..31b6e9c2 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -37,7 +37,7 @@ func (u user) GetStatus(c echo.Context) error { res, err := u.Service.GetStatus(body) if err != nil { lg.Err(err).Msg("user getstatus") - return c.String(http.StatusOK, "User Service Failed") + return c.String(http.StatusNotFound, "User Not Found") } return c.JSON(http.StatusOK, res) } diff --git a/pkg/service/user.go b/pkg/service/user.go index 74330156..58f1d79c 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -46,7 +46,7 @@ func NewUser(repos UserRepos) User { } func (u user) GetStatus(request UserRequest) (model.UserOnboardingStatus, error) { - res := model.UserOnboardingStatus{Status: "Not Found"} + res := model.UserOnboardingStatus{Status: "not found"} addr := request.WalletAddress if addr == "" { return res, common.StringError(errors.New("no wallet address provided")) @@ -59,8 +59,11 @@ func (u user) GetStatus(request UserRequest) (model.UserOnboardingStatus, error) if err != nil { return res, common.StringError(err) } - res.Status = associatedUser.Status - return res, nil + if associatedUser.Status != "" { + res.Status = associatedUser.Status + return res, nil + } + return res, common.StringError(errors.New("not found")) } func (u user) Create(request UserRequest) error { @@ -86,7 +89,7 @@ func (u user) Create(request UserRequest) error { } // Optionally verify signature - status := "Created" + status := "unverified" if request.Signature != "" { valid, err := common.ValidateExternalEVMSignature(request.Signature, addr, addr) // they signed their own address. // it's like writing your name on your hand and then xeroxing it @@ -96,18 +99,18 @@ func (u user) Create(request UserRequest) error { if !valid { return common.StringError(errors.New("signature invalid")) } - status = "Validated" + status = "validated" } // Initialize a new user - user := model.User{Type: "String User", Status: "Created"} // Validated status pertains to specific instrument + user := model.User{Type: "string-user", Status: "unverified"} // Validated status pertains to specific instrument user, err = u.repos.User.Create(user) if err != nil { return common.StringError(err) } // Create a new wallet instrument and associate it with the new user - instrument = model.Instrument{Type: "Crypto Wallet", Status: status, Network: "EVM", PublicKey: addr, UserID: user.ID} + instrument = model.Instrument{Type: "crypto-wallet", Status: status, Network: "EVM", PublicKey: addr, UserID: user.ID} instrument, err = u.repos.Instrument.Create(instrument) if err != nil { return common.StringError(err) @@ -130,7 +133,7 @@ func (u user) Sign(request UserRequest) error { if err != nil { return common.StringError(err) } - if instrument.Status == "Validated" { + if instrument.Status == "validated" { return common.StringError(errors.New("wallet already validated")) } @@ -146,7 +149,7 @@ func (u user) Sign(request UserRequest) error { if !valid { return common.StringError(errors.New("signature invalid")) } - validated := "Validated" + validated := "validated" status := model.UpdateStatus{Status: &validated} err = u.repos.Instrument.Update(instrument.ID, status) if err != nil { @@ -158,6 +161,7 @@ func (u user) Sign(request UserRequest) error { func (u user) Authenticate(request UserRequest) error { addr := request.WalletAddress email := request.EmailAddress + // TODO: Use go-playground/validator if addr == "" || email == "" { return common.StringError(errors.New("missing wallet/email")) } @@ -185,6 +189,8 @@ func (u user) Authenticate(request UserRequest) error { env := os.Getenv("ENV") if env == "dev" { baseURL = "https://app.dev.string-api.xyz/" + } else if env == "local" { + baseURL = "http://localhost:5555/" } else { baseURL = "https://app.string-api.xyz/" } From 36aafd9b035fe25c0a187641a20480ecfc0a6590 Mon Sep 17 00:00:00 2001 From: Auroter Date: Tue, 1 Nov 2022 12:39:06 -0700 Subject: [PATCH 08/12] Better Onboarding response messages --- api/handler/user.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/api/handler/user.go b/api/handler/user.go index 31b6e9c2..26a3136f 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -18,6 +18,10 @@ type User interface { RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) } +type ResultMessage struct { + Status string +} + type user struct { Service service.User Group *echo.Group @@ -54,7 +58,7 @@ func (u user) Create(c echo.Context) error { lg.Err(err).Msg("user create") return c.String(http.StatusOK, "User Service Failed") } - return c.JSON(http.StatusOK, nil) + return c.JSON(http.StatusOK, ResultMessage{Status: "User Created"}) } func (u user) Sign(c echo.Context) error { @@ -67,9 +71,9 @@ func (u user) Sign(c echo.Context) error { err = u.Service.Sign(body) if err != nil { lg.Err(err).Msg("user sign") - return c.String(http.StatusOK, "User Service Failed") + return c.String(http.StatusBadRequest, "Signing Wallet Failed") } - return c.JSON(http.StatusOK, nil) + return c.JSON(http.StatusOK, ResultMessage{Status: "Wallet Signed"}) } func (u user) Authenticate(c echo.Context) error { @@ -86,18 +90,18 @@ func (u user) Authenticate(c echo.Context) error { err = u.Service.ReceiveEmailAuthentication(token) if err != nil { lg.Err(err).Msg("user authenticate") - return c.String(http.StatusOK, "User Service Failed") + return c.String(http.StatusBadRequest, "Invalid Token") } - return c.JSON(http.StatusOK, nil) + return c.JSON(http.StatusOK, ResultMessage{Status: "Email Validated Successfully"}) } // User needs a token err = u.Service.Authenticate(body) if err != nil { lg.Err(err).Msg("user authenticate") - return c.String(http.StatusOK, "User Service Failed") + return c.String(http.StatusBadRequest, "Could Not Send Email Verification") } - return c.JSON(http.StatusOK, nil) + return c.JSON(http.StatusOK, ResultMessage{Status: "Email Validation Sent"}) } func (u user) Name(c echo.Context) error { @@ -110,9 +114,9 @@ func (u user) Name(c echo.Context) error { err = u.Service.Name(body) if err != nil { lg.Err(err).Msg("user name") - return c.String(http.StatusOK, "User Service Failed") + return c.String(http.StatusBadRequest, "Could Not Update Name") } - return c.JSON(http.StatusOK, nil) + return c.JSON(http.StatusOK, ResultMessage{Status: "Name Updated Successfully"}) } func (u user) RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) { From ce1e205f0fd7604afa12271fc66a6c51a832b48c Mon Sep 17 00:00:00 2001 From: Auroter Date: Tue, 1 Nov 2022 13:59:45 -0700 Subject: [PATCH 09/12] adding StringMap --- pkg/model/custom.go | 23 ++++++++++++++++++++ pkg/model/entity.go | 52 ++++++++++++++++++++++----------------------- 2 files changed, 49 insertions(+), 26 deletions(-) create mode 100644 pkg/model/custom.go diff --git a/pkg/model/custom.go b/pkg/model/custom.go new file mode 100644 index 00000000..d18e5dba --- /dev/null +++ b/pkg/model/custom.go @@ -0,0 +1,23 @@ +package model + +import ( + "database/sql/driver" + "encoding/json" + "errors" +) + +type StringMap map[string]string + +func (sm StringMap) Value() (driver.Value, error) { + return json.Marshal(sm) +} + +func (sm *StringMap) Scan(src interface{}) error { + switch t := src.(type) { + case string: + return json.Unmarshal([]byte(t), sm) + case []byte: + return json.Unmarshal([]byte(t), sm) + } + return errors.New("unknown type") +} diff --git a/pkg/model/entity.go b/pkg/model/entity.go index 42b925c1..d77c5aeb 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -10,16 +10,16 @@ import ( // See STRING_USER in Migrations 0001 type User struct { - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` - Type string `json:"type" db:"type"` - Status string `json:"status" db:"status"` - Tags types.JSONText `json:"tags" db:"tags"` - FirstName string `json:"firstName" db:"first_name"` - MiddleName string `json:"middleName" db:"middle_name"` - LastName string `json:"lastName" db:"last_name"` + ID string `json:"id" db:"id"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` + Type string `json:"type" db:"type"` + Status string `json:"status" db:"status"` + Tags StringMap `json:"tags" db:"tags"` + FirstName string `json:"firstName" db:"first_name"` + MiddleName string `json:"middleName" db:"middle_name"` + LastName string `json:"lastName" db:"last_name"` } // See PLATFORM in Migrations 0001 @@ -91,20 +91,20 @@ type Contact struct { // See LOCATION in Migrations 0002 type Location struct { - ID string `json:"id" db:"id"` - UserID string `json:"userId" db:"user_id"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - Type string `json:"type" db:"type"` - Status string `json:"status" db:"status"` - Tags types.JSONText `json:"tags" db:"tags"` - BuildingNumber string `json:"buildingNumber" db:"building_number"` - UnitNumber string `json:"unitNumber" db:"unit_number"` - StreetName string `json:"streetName" db:"street_name"` - City string `json:"city" db:"city"` - State string `json:"state" db:"state"` - PostalCode string `json:"postalCode" db:"postal_code"` - Country string `json:"country" db:"country"` + ID string `json:"id" db:"id"` + UserID string `json:"userId" db:"user_id"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + Type string `json:"type" db:"type"` + Status string `json:"status" db:"status"` + Tags StringMap `json:"tags" db:"tags"` + BuildingNumber string `json:"buildingNumber" db:"building_number"` + UnitNumber string `json:"unitNumber" db:"unit_number"` + StreetName string `json:"streetName" db:"street_name"` + City string `json:"city" db:"city"` + State string `json:"state" db:"state"` + PostalCode string `json:"postalCode" db:"postal_code"` + Country string `json:"country" db:"country"` } // See INSTRUMENT in Migrations 0002 @@ -115,7 +115,7 @@ type Instrument struct { DeactivatedAt *time.Time `json:"deactivatedAt" db:"deactivated_at"` Type string `json:"type" db:"type"` Status string `json:"status" db:"status"` - Tags types.JSONText `json:"tags" db:"tags"` + Tags StringMap `json:"tags" db:"tags"` Network string `json:"network" db:"network"` PublicKey string `json:"publicKey" db:"public_key"` Last4 string `json:"last4" db:"last_4"` @@ -163,7 +163,7 @@ type Transaction struct { UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` Type string `json:"type" db:"type"` Status string `json:"status" db:"status"` - Tags types.JSONText `json:"tags" db:"tags"` // TODO: Fix this alongside Unit21 integration + Tags StringMap `json:"tags" db:"tags"` // TODO: Fix this alongside Unit21 integration DeviceID string `json:"deviceId" db:"device_id"` IPAddress string `json:"ipAddress" db:"ip_address"` PlatformID string `json:"platformId" db:"platform_id"` From e783dceb937babfbe631e7735fc0a15b76ab5e79 Mon Sep 17 00:00:00 2001 From: Auroter Date: Tue, 1 Nov 2022 15:26:15 -0700 Subject: [PATCH 10/12] added StringArray --- pkg/model/custom.go | 3 ++ pkg/model/entity.go | 68 ++++++++++++++++++++++----------------------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/pkg/model/custom.go b/pkg/model/custom.go index d18e5dba..e2a4326c 100644 --- a/pkg/model/custom.go +++ b/pkg/model/custom.go @@ -4,9 +4,12 @@ import ( "database/sql/driver" "encoding/json" "errors" + + "github.com/lib/pq" ) type StringMap map[string]string +type StringArray pq.StringArray func (sm StringMap) Value() (driver.Value, error) { return json.Marshal(sm) diff --git a/pkg/model/entity.go b/pkg/model/entity.go index d77c5aeb..37241b36 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -4,8 +4,6 @@ import ( "database/sql" "encoding/json" "time" - - "github.com/jmoiron/sqlx/types" ) // See STRING_USER in Migrations 0001 @@ -63,17 +61,17 @@ type Asset struct { // See DEVICE in Migrations 0002 type Device struct { - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - LastUsedAt time.Time `json:"lastUsedAt" db:"last_used_at"` - ValidatedAt time.Time `json:"validatedAt" db:"validated_at"` - DeactivatedAt time.Time `json:"deactivatedAt" db:"deactivated_at"` - Type string `json:"type" db:"type"` - Description string `json:"description" db:"description"` - Fingerprint string `json:"fingerprint" db:"fingerprint"` - IpAddresses types.JSONText `json:"ipAddresses" db:"ip_addresses"` - UserID string `json:"userId" db:"user_id"` + ID string `json:"id" db:"id"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + LastUsedAt time.Time `json:"lastUsedAt" db:"last_used_at"` + ValidatedAt time.Time `json:"validatedAt" db:"validated_at"` + DeactivatedAt time.Time `json:"deactivatedAt" db:"deactivated_at"` + Type string `json:"type" db:"type"` + Description string `json:"description" db:"description"` + Fingerprint string `json:"fingerprint" db:"fingerprint"` + IpAddresses StringArray `json:"ipAddresses" db:"ip_addresses"` + UserID string `json:"userId" db:"user_id"` } // See CONTACT in Migrations 0002 @@ -158,28 +156,28 @@ type TxLeg struct { // See TRANSACTION in Migrations 0003 type Transaction struct { - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - Type string `json:"type" db:"type"` - Status string `json:"status" db:"status"` - Tags StringMap `json:"tags" db:"tags"` // TODO: Fix this alongside Unit21 integration - DeviceID string `json:"deviceId" db:"device_id"` - IPAddress string `json:"ipAddress" db:"ip_address"` - PlatformID string `json:"platformId" db:"platform_id"` - TransactionHash string `json:"transactionHash" db:"transaction_hash"` - NetworkID string `json:"networkId" db:"network_id"` - NetworkFee string `json:"networkFee" db:"network_fee"` - ContractParams types.JSONText `json:"contractParameters" db:"contract_params"` - ContractFunc string `json:"contractFunc" db:"contract_func"` - TransactionAmount string `json:"transactionAmount" db:"transaction_amount"` - OriginTXLegID string `json:"originTXLegId" db:"origin_tx_leg_id"` - ReceiptTXLegID string `json:"receiptTXLegId" db:"receipt_tx_leg_id"` - ResponseTXLegID string `json:"responseTXLegId" db:"response_tx_leg_id"` - DestinationTXLegID string `json:"destinationTXLegId" db:"destination_tx_leg_id"` - ProcessingFee string `json:"processingFee" db:"processing_fee"` - ProcessingFeeAsset string `json:"processingFeeAsset" db:"processing_fee_asset"` - StringFee string `json:"stringFee" db:"string_fee"` + ID string `json:"id" db:"id"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + Type string `json:"type" db:"type"` + Status string `json:"status" db:"status"` + Tags StringMap `json:"tags" db:"tags"` // TODO: Fix this alongside Unit21 integration + DeviceID string `json:"deviceId" db:"device_id"` + IPAddress string `json:"ipAddress" db:"ip_address"` + PlatformID string `json:"platformId" db:"platform_id"` + TransactionHash string `json:"transactionHash" db:"transaction_hash"` + NetworkID string `json:"networkId" db:"network_id"` + NetworkFee string `json:"networkFee" db:"network_fee"` + ContractParams StringArray `json:"contractParameters" db:"contract_params"` + ContractFunc string `json:"contractFunc" db:"contract_func"` + TransactionAmount string `json:"transactionAmount" db:"transaction_amount"` + OriginTXLegID string `json:"originTXLegId" db:"origin_tx_leg_id"` + ReceiptTXLegID string `json:"receiptTXLegId" db:"receipt_tx_leg_id"` + ResponseTXLegID string `json:"responseTXLegId" db:"response_tx_leg_id"` + DestinationTXLegID string `json:"destinationTXLegId" db:"destination_tx_leg_id"` + ProcessingFee string `json:"processingFee" db:"processing_fee"` + ProcessingFeeAsset string `json:"processingFeeAsset" db:"processing_fee_asset"` + StringFee string `json:"stringFee" db:"string_fee"` } type AuthStrategy struct { From 192419e7d2c9ca92f379e72d747493f9bdcb9cd1 Mon Sep 17 00:00:00 2001 From: Auroter Date: Tue, 1 Nov 2022 15:45:18 -0700 Subject: [PATCH 11/12] Allow email verification without protection --- api/api.go | 12 ++++++++++ api/handler/user.go | 11 ---------- api/handler/verification.go | 44 +++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 api/handler/verification.go diff --git a/api/api.go b/api/api.go index 80244630..67c37e5a 100644 --- a/api/api.go +++ b/api/api.go @@ -32,6 +32,7 @@ func Start(config APIConfig) { platformRoute(config, e) transactRoute(config, authService, e) userRoute(config, authService, e) + verificationRoute(config, e) e.Logger.Fatal(e.Start(":" + config.Port)) } @@ -86,3 +87,14 @@ func userRoute(config APIConfig, auth service.Auth, e *echo.Echo) { handler := handler.NewUser(e, service) handler.RegisterRoutes(e.Group("/user"), middleware.APIKeyAuth(auth), middleware.BearerAuth()) } + +func verificationRoute(config APIConfig, e *echo.Echo) { + repos := service.UserRepos{ + User: repository.NewUser(config.DB), + Contact: repository.NewContact(config.DB), + Instrument: repository.NewInstrument(config.DB), + } + service := service.NewUser(repos) + handler := handler.NewUser(e, service) + handler.RegisterRoutes(e.Group("/verification")) +} diff --git a/api/handler/user.go b/api/handler/user.go index 26a3136f..d07fff47 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -84,17 +84,6 @@ func (u user) Authenticate(c echo.Context) error { return c.String(http.StatusBadRequest, "Bad Request") } - // Token was provided - token := c.QueryParam("token") - if token != "" { - err = u.Service.ReceiveEmailAuthentication(token) - if err != nil { - lg.Err(err).Msg("user authenticate") - return c.String(http.StatusBadRequest, "Invalid Token") - } - return c.JSON(http.StatusOK, ResultMessage{Status: "Email Validated Successfully"}) - } - // User needs a token err = u.Service.Authenticate(body) if err != nil { diff --git a/api/handler/verification.go b/api/handler/verification.go new file mode 100644 index 00000000..b38c0b83 --- /dev/null +++ b/api/handler/verification.go @@ -0,0 +1,44 @@ +package handler + +import ( + "net/http" + + "github.com/String-xyz/string-api/pkg/service" + "github.com/labstack/echo/v4" + "github.com/rs/zerolog" +) + +type Verification interface { + Authenticate(c echo.Context) error // Takes e-mail and wallet addr of user, validates email with twilio + RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) +} + +type verification struct { + Service service.User + Group *echo.Group +} + +func NewVerification(route *echo.Echo, service service.User) Verification { + return &verification{service, nil} +} + +func (v verification) Authenticate(c echo.Context) error { + lg := c.Get("logger").(*zerolog.Logger) + // Token was provided + token := c.QueryParam("token") + err := v.Service.ReceiveEmailAuthentication(token) + if err != nil { + lg.Err(err).Msg("user authenticate") + return c.String(http.StatusBadRequest, "Invalid Token") + } + return c.JSON(http.StatusOK, ResultMessage{Status: "Email Validated Successfully"}) +} + +func (v verification) RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) { + if g == nil { + panic("No group attached to the User Handler") + } + v.Group = g + g.Use(ms...) + g.POST("/email", v.Authenticate) +} From 131b42c6b15db8f6c54cc13a74f9c56e6763d3a9 Mon Sep 17 00:00:00 2001 From: Auroter Date: Tue, 1 Nov 2022 15:52:44 -0700 Subject: [PATCH 12/12] remove base / in /user/ --- api/handler/user.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/handler/user.go b/api/handler/user.go index d07fff47..65bd7ad1 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -114,9 +114,9 @@ func (u user) RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) { } u.Group = g g.Use(ms...) - g.GET("/", u.GetStatus) - g.POST("/", u.Create) - g.PUT("/", u.Sign) + g.GET("", u.GetStatus) + g.POST("", u.Create) + g.PUT("", u.Sign) g.POST("/email", u.Authenticate) g.POST("/name", u.Name) }