From b6b555345330e1f92f4927c82ff20ab7fa958e74 Mon Sep 17 00:00:00 2001 From: Cronus <105345303+ice-cronus@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:56:53 +0700 Subject: [PATCH] telegram login using tma token / mini-app init data (#189) --- application.yaml | 6 +++ auth/auth.go | 53 +++++++++++++++++++++ auth/contract.go | 45 ++++++++++++++++++ auth/email_link/contract.go | 17 +++---- auth/email_link/link_verify.go | 15 +++--- auth/email_link/token_refresh.go | 64 +++++-------------------- auth/email_link/users.go | 36 ++------------ auth/telegram/DDL.sql | 12 +++++ auth/telegram/contract.go | 65 ++++++++++++++++++++++++++ auth/telegram/sign_in.go | 80 ++++++++++++++++++++++++++++++++ auth/telegram/telegram.go | 25 ++++++++++ auth/telegram/token_refresh.go | 62 +++++++++++++++++++++++++ auth/telegram/users.go | 68 +++++++++++++++++++++++++++ auth/token.go | 52 +++++++++++++++++++++ auth/users.go | 48 +++++++++++++++++++ cmd/eskimo-hut/api/docs.go | 50 ++++++++++++++++++++ cmd/eskimo-hut/api/swagger.json | 50 ++++++++++++++++++++ cmd/eskimo-hut/api/swagger.yaml | 33 +++++++++++++ cmd/eskimo-hut/auth.go | 39 +++++++++++++++- cmd/eskimo-hut/contract.go | 9 +++- cmd/eskimo-hut/eskimo_hut.go | 4 ++ go.mod | 9 ++-- go.sum | 18 +++---- 23 files changed, 744 insertions(+), 116 deletions(-) create mode 100644 auth/auth.go create mode 100644 auth/contract.go create mode 100644 auth/telegram/DDL.sql create mode 100644 auth/telegram/contract.go create mode 100644 auth/telegram/sign_in.go create mode 100644 auth/telegram/telegram.go create mode 100644 auth/telegram/token_refresh.go create mode 100644 auth/telegram/users.go create mode 100644 auth/token.go create mode 100644 auth/users.go diff --git a/application.yaml b/application.yaml index eee98567..3f1613d8 100644 --- a/application.yaml +++ b/application.yaml @@ -82,6 +82,12 @@ kyc/quiz: maxAttemptsAllowed: 3 availabilityWindowSeconds: 600 globalStartDate: '2024-02-03T16:20:52.156534Z' +auth/telegram: + wintr/connectors/storage/v2: *db + telegramTokenExpiration: 1h + telegramBots: + bogusBot: + botToken: bogus auth/email-link: extraLoadBalancersCount: 2 wintr/connectors/storage/v2: *db diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 00000000..1a923f29 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: ice License 1.0 + +package auth + +import ( + "context" + + "github.com/pkg/errors" + + wintrauth "github.com/ice-blockchain/wintr/auth" +) + +func NewRefresher(authClient wintrauth.Client, email, telegram ProviderRefresher) TokenRefresher { + return &tokenRefresher{ + authClient: authClient, + platforms: map[string]ProviderRefresher{ + platformEmail: email, + platformTelegram: telegram, + }, + } +} + +func (c *tokenRefresher) RegenerateTokens(ctx context.Context, previousRefreshToken string) (tokens *Tokens, err error) { + token, err := c.authClient.ParseToken(previousRefreshToken) + if err != nil { + if errors.Is(err, wintrauth.ErrExpiredToken) { + return nil, errors.Wrapf(ErrExpiredToken, "failed to verify due to expired token:%v", previousRefreshToken) + } + if errors.Is(err, wintrauth.ErrInvalidToken) { + return nil, errors.Wrapf(ErrInvalidToken, "failed to verify due to invalid token:%v", previousRefreshToken) + } + + return nil, errors.Wrapf(ErrInvalidToken, "failed to verify token:%v (token:%v)", err.Error(), previousRefreshToken) + } + telegramUserID := "" + if len(token.Claims) > 0 { + if tUserIDInterface, found := token.Claims["telegramUserID"]; found { + telegramUserID = tUserIDInterface.(string) //nolint:errcheck,forcetypeassert // . + } + } + var provider ProviderRefresher + switch { + case telegramUserID != "": + provider = c.platforms[platformTelegram] + case token.Email != "": + provider = c.platforms[platformEmail] + default: + return nil, errors.Wrapf(ErrInvalidToken, "invalid token %v cannot detect both email and telegram", previousRefreshToken) + } + tokens, err = provider.RefreshToken(ctx, token) + + return tokens, errors.Wrapf(err, "failed to refresh tokens for %#v", token) +} diff --git a/auth/contract.go b/auth/contract.go new file mode 100644 index 00000000..b36c423e --- /dev/null +++ b/auth/contract.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: ice License 1.0 + +package auth + +import ( + "context" + "errors" + + wintrauth "github.com/ice-blockchain/wintr/auth" +) + +type ( + Tokens struct { + RefreshToken string `json:"refreshToken,omitempty" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // . + AccessToken string `json:"accessToken,omitempty" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // . + } + TokenRefresher interface { + RegenerateTokens(ctx context.Context, prevToken string) (tokens *Tokens, err error) + } + ProviderRefresher interface { + RefreshToken(ctx context.Context, token *wintrauth.IceToken) (*Tokens, error) + } +) + +const ( + IceIDPrefix = "ice_" +) + +var ( + ErrInvalidToken = errors.New("invalid token") + ErrExpiredToken = errors.New("expired token") +) + +// Private API. +type ( + tokenRefresher struct { + authClient wintrauth.Client + platforms map[string]ProviderRefresher + } +) + +const ( + platformEmail = "email" + platformTelegram = "telegram" +) diff --git a/auth/email_link/contract.go b/auth/email_link/contract.go index 141f3bd6..09589f30 100644 --- a/auth/email_link/contract.go +++ b/auth/email_link/contract.go @@ -13,8 +13,9 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/pkg/errors" + "github.com/ice-blockchain/eskimo/auth" "github.com/ice-blockchain/eskimo/users" - "github.com/ice-blockchain/wintr/auth" + wintrauth "github.com/ice-blockchain/wintr/auth" "github.com/ice-blockchain/wintr/connectors/storage/v2" "github.com/ice-blockchain/wintr/email" "github.com/ice-blockchain/wintr/time" @@ -29,19 +30,15 @@ type ( Client interface { IceUserIDClient SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUniqueID, language, clientIP string) (loginSession string, err error) - SignIn(ctx context.Context, loginFlowToken, confirmationCode string) (tokens *Tokens, emailConfirmed bool, err error) - RegenerateTokens(ctx context.Context, prevToken string) (tokens *Tokens, err error) + SignIn(ctx context.Context, loginFlowToken, confirmationCode string) (tokens *auth.Tokens, emailConfirmed bool, err error) UpdateMetadata(ctx context.Context, userID string, metadata *users.JSON) (*users.JSON, error) + RefreshToken(ctx context.Context, token *wintrauth.IceToken) (tokens *auth.Tokens, err error) } IceUserIDClient interface { io.Closer IceUserID(ctx context.Context, mail string) (iceID string, err error) Metadata(ctx context.Context, userID, emailAddress string) (metadata string, metadataFields *users.JSON, err error) } - Tokens struct { - RefreshToken string `json:"refreshToken,omitempty" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // . - AccessToken string `json:"accessToken,omitempty" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // . - } Metadata struct { UserID string `json:"userId" example:"1c0b9801-cfb2-4c4e-b48a-db18ce0894f9"` Metadata string `json:"metadata"` @@ -49,8 +46,8 @@ type ( ) var ( - ErrInvalidToken = errors.New("invalid token") - ErrExpiredToken = errors.New("expired token") + ErrInvalidToken = auth.ErrInvalidToken + ErrExpiredToken = auth.ErrExpiredToken ErrNoConfirmationRequired = errors.New("no pending confirmation") ErrUserDataMismatch = errors.New("parameters were not equal to user data in db") @@ -94,7 +91,7 @@ type ( db *storage.DB cfg *config shutdown func() error - authClient auth.Client + authClient wintrauth.Client userModifier UserModifier emailClients []email.Client fromRecipients []fromRecipient diff --git a/auth/email_link/link_verify.go b/auth/email_link/link_verify.go index a9b0d689..3b23cf38 100644 --- a/auth/email_link/link_verify.go +++ b/auth/email_link/link_verify.go @@ -11,13 +11,14 @@ import ( "github.com/hashicorp/go-multierror" "github.com/pkg/errors" + "github.com/ice-blockchain/eskimo/auth" "github.com/ice-blockchain/eskimo/users" - "github.com/ice-blockchain/wintr/auth" + wintrauth "github.com/ice-blockchain/wintr/auth" "github.com/ice-blockchain/wintr/connectors/storage/v2" "github.com/ice-blockchain/wintr/time" ) -func (c *client) SignIn(ctx context.Context, loginSession, confirmationCode string) (tokens *Tokens, emailConfirmed bool, err error) { +func (c *client) SignIn(ctx context.Context, loginSession, confirmationCode string) (tokens *auth.Tokens, emailConfirmed bool, err error) { now := time.Now() var token loginFlowToken if err = parseJwtToken(loginSession, c.cfg.LoginSession.JwtSecret, &token); err != nil { @@ -143,20 +144,20 @@ func (c *client) finishAuthProcess( if emailConfirmed { emailConfirmedAt = "$2" } - mdToUpdate := users.JSON(map[string]any{auth.IceIDClaim: userID}) + mdToUpdate := users.JSON(map[string]any{wintrauth.IceIDClaim: userID}) if md == nil { empty := users.JSON(map[string]any{}) md = &empty } - if _, hasRegisteredWith := (*md)[auth.RegisteredWithProviderClaim]; !hasRegisteredWith { - if firebaseID, hasFirebaseID := (*md)[auth.FirebaseIDClaim]; hasFirebaseID { + if _, hasRegisteredWith := (*md)[wintrauth.RegisteredWithProviderClaim]; !hasRegisteredWith { + if firebaseID, hasFirebaseID := (*md)[wintrauth.FirebaseIDClaim]; hasFirebaseID { if !strings.HasPrefix(firebaseID.(string), iceIDPrefix) && !strings.HasPrefix(userID, iceIDPrefix) { //nolint:forcetypeassert // . - mdToUpdate[auth.RegisteredWithProviderClaim] = auth.ProviderFirebase + mdToUpdate[wintrauth.RegisteredWithProviderClaim] = wintrauth.ProviderFirebase } } } if err := mergo.Merge(&mdToUpdate, md, mergo.WithOverride, mergo.WithTypeCheck); err != nil { - return 0, errors.Wrapf(err, "failed to merge %#v and %v:%v", md, auth.IceIDClaim, userID) + return 0, errors.Wrapf(err, "failed to merge %#v and %v:%v", md, wintrauth.IceIDClaim, userID) } params := []any{id.Email, now.Time, userID, id.DeviceUniqueID, issuedTokenSeq, mdToUpdate} type resp struct { diff --git a/auth/email_link/token_refresh.go b/auth/email_link/token_refresh.go index fc859d74..04f20c24 100644 --- a/auth/email_link/token_refresh.go +++ b/auth/email_link/token_refresh.go @@ -7,24 +7,13 @@ import ( "github.com/pkg/errors" - "github.com/ice-blockchain/wintr/auth" + "github.com/ice-blockchain/eskimo/auth" + wintrauth "github.com/ice-blockchain/wintr/auth" "github.com/ice-blockchain/wintr/connectors/storage/v2" "github.com/ice-blockchain/wintr/time" ) -//nolint:funlen // . -func (c *client) RegenerateTokens(ctx context.Context, previousRefreshToken string) (tokens *Tokens, err error) { - token, err := c.authClient.ParseToken(previousRefreshToken) - if err != nil { - if errors.Is(err, auth.ErrExpiredToken) { - return nil, errors.Wrapf(ErrExpiredToken, "failed to verify due to expired token:%v", previousRefreshToken) - } - if errors.Is(err, auth.ErrInvalidToken) { - return nil, errors.Wrapf(ErrInvalidToken, "failed to verify due to invalid token:%v", previousRefreshToken) - } - - return nil, errors.Wrapf(ErrInvalidToken, "failed to verify token:%v (token:%v)", err.Error(), previousRefreshToken) - } +func (c *client) RefreshToken(ctx context.Context, token *wintrauth.IceToken) (tokens *auth.Tokens, err error) { id := loginID{Email: token.Email, DeviceUniqueID: token.DeviceUniqueID} usr, err := c.getUserByIDOrPk(ctx, token.Subject, &id) if err != nil { @@ -39,12 +28,10 @@ func (c *client) RegenerateTokens(ctx context.Context, previousRefreshToken stri "user's email:%v does not match token's email:%v or deviceID:%v (userID %v)", usr.Email, token.Email, token.DeviceUniqueID, token.Subject) } now := time.Now() - refreshTokenSeq, err := c.incrementRefreshTokenSeq(ctx, &id, token.Subject, token.Seq, now) + refreshTokenSeq, err := auth.IncrementRefreshTokenSeq(ctx, c.db, "email_link_sign_ins", + "email_link_sign_ins.email = $4 AND email_link_sign_ins.device_unique_id = $5", + []any{id.Email, id.DeviceUniqueID}, token.Subject, token.Seq, now) if err != nil { - if storage.IsErr(err, storage.ErrNotFound) { - return nil, errors.Wrapf(ErrInvalidToken, "refreshToken with wrong sequence:%v provided (userID:%v)", token.Seq, token.Subject) - } - return nil, errors.Wrapf(err, "failed to update email link sign ins for email:%v", token.Email) } tokens, err = c.generateTokens(now, usr, refreshTokenSeq) @@ -52,48 +39,19 @@ func (c *client) RegenerateTokens(ctx context.Context, previousRefreshToken stri return tokens, errors.Wrapf(err, "can't generate tokens for userID:%v, email:%v", token.Subject, token.Email) } -func (c *client) incrementRefreshTokenSeq( - ctx context.Context, - id *loginID, - userID string, - currentSeq int64, - now *time.Time, -) (tokenSeq int64, err error) { - params := []any{id.Email, id.DeviceUniqueID, now.Time, userID, currentSeq} - type resp struct { - IssuedTokenSeq int64 - } - sql := `UPDATE email_link_sign_ins - SET token_issued_at = $3, - user_id = $4, - issued_token_seq = COALESCE(email_link_sign_ins.issued_token_seq, 0) + 1, - previously_issued_token_seq = (CASE WHEN COALESCE(email_link_sign_ins.issued_token_seq, 0) = $5 THEN email_link_sign_ins.issued_token_seq ELSE $5 END) - WHERE email_link_sign_ins.email = $1 AND email_link_sign_ins.device_unique_id = $2 - AND email_link_sign_ins.user_id = $4 - AND (email_link_sign_ins.issued_token_seq = $5 - OR (email_link_sign_ins.previously_issued_token_seq <= $5 AND - email_link_sign_ins.previously_issued_token_seq<=COALESCE(email_link_sign_ins.issued_token_seq,0)+1) - ) - RETURNING issued_token_seq` - updatedValue, err := storage.ExecOne[resp](ctx, c.db, sql, params...) - if err != nil { - return 0, errors.Wrapf(err, "failed to assign refreshed token to email link sign ins for params:%#v", params) //nolint:asasalint // Not this output. - } - - return updatedValue.IssuedTokenSeq, nil -} - -func (c *client) generateTokens(now *time.Time, els *emailLinkSignIn, seq int64) (tokens *Tokens, err error) { +func (c *client) generateTokens(now *time.Time, els *emailLinkSignIn, seq int64) (tokens *auth.Tokens, err error) { role := "" if els.Metadata != nil { if roleInterface, found := (*els.Metadata)["role"]; found { role = roleInterface.(string) //nolint:errcheck,forcetypeassert // . } } - refreshToken, accessToken, err := c.authClient.GenerateTokens(now, *els.UserID, els.DeviceUniqueID, els.Email, els.HashCode, seq, role) + refreshToken, accessToken, err := c.authClient.GenerateTokens(now, *els.UserID, els.DeviceUniqueID, els.Email, els.HashCode, seq, role, map[string]any{ + "loginType": "email", + }) if err != nil { return nil, errors.Wrapf(err, "failed to generate tokens for user:%#v", els) } - return &Tokens{AccessToken: accessToken, RefreshToken: refreshToken}, nil + return &auth.Tokens{AccessToken: accessToken, RefreshToken: refreshToken}, nil } diff --git a/auth/email_link/users.go b/auth/email_link/users.go index 4ace4dc4..99f6e20e 100644 --- a/auth/email_link/users.go +++ b/auth/email_link/users.go @@ -7,9 +7,9 @@ import ( "strings" "dario.cat/mergo" - "github.com/google/uuid" "github.com/pkg/errors" + "github.com/ice-blockchain/eskimo/auth" "github.com/ice-blockchain/eskimo/users" "github.com/ice-blockchain/wintr/connectors/storage/v2" "github.com/ice-blockchain/wintr/terror" @@ -30,41 +30,15 @@ func (c *client) getEmailLinkSignInByPk(ctx context.Context, id *loginID, oldEma } func (c *client) findOrGenerateUserID(ctx context.Context, email, oldEmail string) (userID string, err error) { - if ctx.Err() != nil { - return "", errors.Wrap(ctx.Err(), "find or generate user by id or email context failed") - } - randomID := iceIDPrefix + uuid.NewString() searchEmail := email if oldEmail != "" { searchEmail = oldEmail } - - return c.getUserIDFromEmail(ctx, searchEmail, randomID) -} - -func (c *client) getUserIDFromEmail(ctx context.Context, searchEmail, idIfNotFound string) (userID string, err error) { - type dbUserID struct { - ID string - } - sql := `SELECT id FROM ( - SELECT users.id, 1 as idx - FROM users - WHERE email = $1 - UNION ALL - (SELECT COALESCE(user_id, phone_number_to_email_migration_user_id, $2) AS id, 2 as idx - FROM email_link_sign_ins - WHERE email = $1) - ) t ORDER BY idx LIMIT 1` - ids, err := storage.Select[dbUserID](ctx, c.db, sql, searchEmail, idIfNotFound) - if err != nil || len(ids) == 0 { - if storage.IsErr(err, storage.ErrNotFound) || (err == nil && len(ids) == 0) { - return idIfNotFound, nil - } - - return "", errors.Wrapf(err, "failed to find user by email:%v", searchEmail) + if userID, err = auth.FindOrGenerateUserID(ctx, c.db, "email_link_sign_ins", "email", searchEmail); err != nil { + return "", errors.Wrapf(err, "failed to match userID with email %v,%v", email, oldEmail) } - return ids[0].ID, nil + return userID, nil } func (c *client) isUserExist(ctx context.Context, email string) error { @@ -180,7 +154,7 @@ func (c *client) IceUserID(ctx context.Context, email string) (string, error) { if email == "" { return "", nil } - userID, err := c.getUserIDFromEmail(ctx, email, "") + userID, err := auth.GetUserIDFromSearch(ctx, c.db, "email_link_sign_ins", "email", email, "") if err != nil { return "", errors.Wrapf(err, "failed to fetch userID by email:%v", email) } diff --git a/auth/telegram/DDL.sql b/auth/telegram/DDL.sql new file mode 100644 index 00000000..6f6cb506 --- /dev/null +++ b/auth/telegram/DDL.sql @@ -0,0 +1,12 @@ +-- SPDX-License-Identifier: ice License 1.0 + +CREATE TABLE IF NOT EXISTS telegram_sign_ins ( + created_at timestamp NOT NULL, + token_issued_at timestamp, + issued_token_seq BIGINT DEFAULT 0 NOT NULL, + previously_issued_token_seq BIGINT DEFAULT 0 NOT NULL, + telegram_user_id text NOT NULL, + user_id TEXT, + primary key(telegram_user_id)) + WITH (FILLFACTOR = 70); +CREATE INDEX IF NOT EXISTS telegram_sign_ins ON telegram_sign_ins (user_id); \ No newline at end of file diff --git a/auth/telegram/contract.go b/auth/telegram/contract.go new file mode 100644 index 00000000..9c433da0 --- /dev/null +++ b/auth/telegram/contract.go @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: ice License 1.0 + +package telegram + +import ( + "context" + _ "embed" + "time" + + "github.com/ice-blockchain/eskimo/auth" + "github.com/ice-blockchain/eskimo/users" + wintrauth "github.com/ice-blockchain/wintr/auth" + "github.com/ice-blockchain/wintr/connectors/storage/v2" +) + +type ( + Client interface { + SignIn(ctx context.Context, tmaToken string) (tokens *auth.Tokens, err error) + RefreshToken(ctx context.Context, token *wintrauth.IceToken) (tokens *auth.Tokens, err error) + } +) + +var ( + ErrInvalidToken = auth.ErrInvalidToken + ErrExpiredToken = auth.ErrExpiredToken + ErrUserNotFound = storage.ErrNotFound +) + +// Private API. +type ( + client struct { + db *storage.DB + authClient wintrauth.Client + shutdown func() error + cfg *config + } + config struct { + TelegramBots map[telegramBotID]struct { + BotToken telegramBotToken `yaml:"botToken"` + } `yaml:"telegramBots" mapstructure:"telegramBots"` + TelegramTokenExpiration time.Duration `yaml:"telegramTokenExpiration"` + } + telegramBotToken = string + telegramBotID = string + telegramSignIn struct { + CreatedAt *time.Time + TokenIssuedAt *time.Time + Metadata *users.JSON `json:"metadata,omitempty"` + UserID *string `json:"userId" example:"did:ethr:0x4B73C58370AEfcEf86A6021afCDe5673511376B2"` + TelegramUserID string `json:"telegramUserId,omitempty" example:"12345678990" db:"telegram_user_id"` + Email string `json:"email,omitempty" example:"someone1@example.com"` + IssuedTokenSeq int64 `json:"issuedTokenSeq,omitempty" example:"1"` + PreviouslyIssuedTokenSeq int64 `json:"previouslyIssuedTokenSeq,omitempty" example:"1"` + HashCode int64 `json:"hashCode,omitempty" example:"43453546464576547"` + } +) + +const ( + applicationYamlKey = "auth/telegram" +) + +var ( //nolint:gofumpt // . + //go:embed DDL.sql + ddl string +) diff --git a/auth/telegram/sign_in.go b/auth/telegram/sign_in.go new file mode 100644 index 00000000..22f243c3 --- /dev/null +++ b/auth/telegram/sign_in.go @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: ice License 1.0 + +package telegram + +import ( + "context" + "strconv" + + "github.com/pkg/errors" + initdata "github.com/telegram-mini-apps/init-data-golang" + + "github.com/ice-blockchain/eskimo/auth" + "github.com/ice-blockchain/wintr/connectors/storage/v2" + "github.com/ice-blockchain/wintr/time" +) + +func (c *client) SignIn(ctx context.Context, tmaToken string) (tokens *auth.Tokens, err error) { + if vErr := c.verifyTelegramTMAToken(tmaToken); vErr != nil { + return nil, errors.Wrapf(vErr, "failed to verify TMA token %v", tmaToken) + } + tgData, err := initdata.Parse(tmaToken) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse telegramToken %v", tmaToken) + } + now := time.Now() + telegramUserID := strconv.FormatInt(tgData.User.ID, 10) + userID, err := auth.FindOrGenerateUserID(ctx, c.db, "telegram_sign_ins", "telegram_user_id", telegramUserID) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch or generate userID for telegram: %v", telegramUserID) + } + signIn, err := c.upsertTelegramSignIn(ctx, now, userID, telegramUserID) + if err != nil { + return nil, errors.Wrapf(err, "failed to save user information from telegram %#v", tgData.User) + } + + return c.generateTokens(now, signIn, signIn.IssuedTokenSeq) +} + +func (c *client) verifyTelegramTMAToken(tmaToken string) error { + var vErr error + for _, botToken := range c.cfg.TelegramBots { + if vErr = initdata.Validate(tmaToken, botToken.BotToken, c.cfg.TelegramTokenExpiration); vErr == nil { + break + } + } + if vErr != nil { + switch { + case errors.Is(vErr, initdata.ErrSignInvalid): + return ErrInvalidToken + case errors.Is(vErr, initdata.ErrSignMissing): + return ErrInvalidToken + case errors.Is(vErr, initdata.ErrUnexpectedFormat): + return ErrInvalidToken + case errors.Is(vErr, initdata.ErrAuthDateMissing): + return ErrInvalidToken + case errors.Is(vErr, initdata.ErrExpired): + return ErrExpiredToken + default: + return errors.Wrapf(vErr, "failed to validate telegram token %v for unknown reason", tmaToken) + } + } + + return nil +} + +func (c *client) upsertTelegramSignIn(ctx context.Context, now *time.Time, userID, telegramUserID string) (*telegramSignIn, error) { + params := []any{now.Time, telegramUserID, userID} + sql := `INSERT INTO telegram_sign_ins ( + created_at,token_issued_at, telegram_user_id, user_id,issued_token_seq,previously_issued_token_seq) + VALUES ($1, $1, $2, $3, 1, 0) + ON CONFLICT (telegram_user_id) DO UPDATE + SET created_at = EXCLUDED.created_at, + token_issued_at = EXCLUDED.token_issued_at, + issued_token_seq = COALESCE(telegram_sign_ins.issued_token_seq, 0) + 1, + previously_issued_token_seq = COALESCE(telegram_sign_ins.issued_token_seq, 0) + 1 + RETURNING *` + res, err := storage.ExecOne[telegramSignIn](ctx, c.db, sql, params...) + + return res, errors.Wrapf(err, "failed to insert/update telegram sign ins record for telegramUserID:%v", telegramUserID) +} diff --git a/auth/telegram/telegram.go b/auth/telegram/telegram.go new file mode 100644 index 00000000..409d98d0 --- /dev/null +++ b/auth/telegram/telegram.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: ice License 1.0 + +package telegram + +import ( + "context" + + "github.com/pkg/errors" + + wintrauth "github.com/ice-blockchain/wintr/auth" + appcfg "github.com/ice-blockchain/wintr/config" + "github.com/ice-blockchain/wintr/connectors/storage/v2" +) + +func NewClient(ctx context.Context, authClient wintrauth.Client) Client { + var cfg config + appcfg.MustLoadFromKey(applicationYamlKey, &cfg) + db := storage.MustConnect(ctx, ddl, applicationYamlKey) + + return &client{authClient: authClient, cfg: &cfg, db: db, shutdown: db.Close} +} + +func (c *client) Close() error { + return errors.Wrap(c.shutdown(), "closing auth/telegram repository failed") +} diff --git a/auth/telegram/token_refresh.go b/auth/telegram/token_refresh.go new file mode 100644 index 00000000..996d9e78 --- /dev/null +++ b/auth/telegram/token_refresh.go @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: ice License 1.0 + +package telegram + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/eskimo/auth" + wintrauth "github.com/ice-blockchain/wintr/auth" + "github.com/ice-blockchain/wintr/connectors/storage/v2" + "github.com/ice-blockchain/wintr/time" +) + +func (c *client) RefreshToken(ctx context.Context, token *wintrauth.IceToken) (tokens *auth.Tokens, err error) { + telegramUserID := "" + if token.Claims != nil { + if tUserIDInterface, found := token.Claims["telegramUserID"]; found { + telegramUserID = tUserIDInterface.(string) //nolint:errcheck,forcetypeassert // . + } + } + if telegramUserID == "" { + return nil, errors.New("unexpected empty telegramID") + } + usr, err := c.getUserByIDOrTelegram(ctx, token.Subject, telegramUserID) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return nil, errors.Wrapf(ErrUserNotFound, "user with userID:%v or tegegramID:%v not found", token.Subject, telegramUserID) + } + + return nil, errors.Wrapf(err, "failed to get user by userID:%v", token.Subject) + } + now := time.Now() + refreshTokenSeq, err := auth.IncrementRefreshTokenSeq(ctx, c.db, "telegram_sign_ins", + "telegram_sign_ins.telegram_user_id = $4", + []any{telegramUserID}, token.Subject, token.Seq, now) + if err != nil { + return nil, errors.Wrapf(err, "failed to update telegram sign ins for:%v,%v", telegramUserID, token.Subject) + } + tokens, err = c.generateTokens(now, usr, refreshTokenSeq) + + return tokens, errors.Wrapf(err, "can't generate tokens for userID:%v, telegram:%v", token.Subject, telegramUserID) +} + +func (c *client) generateTokens(now *time.Time, signIn *telegramSignIn, seq int64) (tokens *auth.Tokens, err error) { + role := "" + if signIn.Metadata != nil { + if roleInterface, found := (*signIn.Metadata)["role"]; found { + role = roleInterface.(string) //nolint:errcheck,forcetypeassert // . + } + } + refreshToken, accessToken, err := c.authClient.GenerateTokens(now, *signIn.UserID, "", signIn.Email, signIn.HashCode, seq, role, map[string]any{ + "telegramUserID": signIn.TelegramUserID, + "loginType": "telegram", + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to generate tokens for user:%#v", signIn) + } + + return &auth.Tokens{AccessToken: accessToken, RefreshToken: refreshToken}, nil +} diff --git a/auth/telegram/users.go b/auth/telegram/users.go new file mode 100644 index 00000000..54f89516 --- /dev/null +++ b/auth/telegram/users.go @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: ice License 1.0 + +package telegram + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/wintr/connectors/storage/v2" +) + +//nolint:funlen // SQL. +func (c *client) getUserByIDOrTelegram(ctx context.Context, userID, telegramID string) (*telegramSignIn, error) { + if ctx.Err() != nil { + return nil, errors.Wrap(ctx.Err(), "get user by id or telegram failed because context failed") + } + usr, err := storage.Get[telegramSignIn](ctx, c.db, ` + SELECT created_at, + token_issued_at, + COALESCE(previously_issued_token_seq, 0) AS previously_issued_token_seq, + COALESCE(issued_token_seq, 0) AS issued_token_seq, + user_id, + telegram_user_id, + email, + hash_code, + metadata + FROM ( + WITH telegrams AS ( + SELECT + created_at, + token_issued_at, + previously_issued_token_seq, + issued_token_seq, + $1 AS user_id, + telegram_user_id, + '' as email, + COALESCE((account_metadata.metadata -> 'hash_code')::BIGINT,0) AS hash_code, + account_metadata.metadata, + 2 AS idx + FROM telegram_sign_ins + LEFT JOIN account_metadata ON account_metadata.user_id = $1 + WHERE telegram_user_id = $2 + ) + SELECT + telegrams.created_at AS created_at, + telegrams.token_issued_at AS token_issued_at, + telegrams.previously_issued_token_seq AS previously_issued_token_seq, + telegrams.issued_token_seq AS issued_token_seq, + u.id AS user_id, + NULLIF(u.telegram_user_id, u.id) AS telegram_user_id, + COALESCE(NULLIF(u.email, u.id) ,'') AS email, + u.hash_code, + account_metadata.metadata AS metadata, + 1 AS idx + FROM users u + LEFT JOIN telegrams ON telegrams.telegram_user_id = $2 and u.id = telegrams.user_id + LEFT JOIN account_metadata ON u.id = account_metadata.user_id + WHERE u.id = $1 + UNION ALL (select * from telegrams) + ) t WHERE t.telegram_user_id IS NOT NULL ORDER BY idx LIMIT 1 + `, userID, telegramID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get user by telegram:(%v,%v)", userID, telegramID) + } + + return usr, nil +} diff --git a/auth/token.go b/auth/token.go new file mode 100644 index 00000000..717cf498 --- /dev/null +++ b/auth/token.go @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: ice License 1.0 + +package auth + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + + "github.com/ice-blockchain/wintr/connectors/storage/v2" + "github.com/ice-blockchain/wintr/time" +) + +//nolint:funlen,revive // Large SQL. +func IncrementRefreshTokenSeq( + ctx context.Context, + db *storage.DB, + table, searchCondition string, + searchParams []any, + userID string, + currentSeq int64, + now *time.Time, +) (tokenSeq int64, err error) { + params := []any{now.Time, userID, currentSeq} + params = append(params, searchParams...) + type resp struct { + IssuedTokenSeq int64 + } + sql := fmt.Sprintf(`UPDATE %[1]v + SET token_issued_at = $1, + user_id = $2, + issued_token_seq = COALESCE(%[1]v.issued_token_seq, 0) + 1, + previously_issued_token_seq = (CASE WHEN COALESCE(%[1]v.issued_token_seq, 0) = $3 THEN %[1]v.issued_token_seq ELSE $3 END) + WHERE %[2]v + AND %[1]v.user_id = $2 + AND (%[1]v.issued_token_seq = $3 + OR (%[1]v.previously_issued_token_seq <= $3 AND + %[1]v.previously_issued_token_seq<=COALESCE(%[1]v.issued_token_seq,0)+1) + ) + RETURNING issued_token_seq`, table, searchCondition) + updatedValue, err := storage.ExecOne[resp](ctx, db, sql, params...) + if err != nil { + if storage.IsErr(err, storage.ErrNotFound) { + return 0, errors.Wrapf(ErrInvalidToken, "refreshToken with wrong sequence:%v provided (userID:%v)", currentSeq, userID) + } + + return 0, errors.Wrapf(err, "failed to assign refreshed token to %v for params:%#v", table, params) + } + + return updatedValue.IssuedTokenSeq, nil +} diff --git a/auth/users.go b/auth/users.go new file mode 100644 index 00000000..04bbd3e0 --- /dev/null +++ b/auth/users.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: ice License 1.0 + +package auth + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/pkg/errors" + + "github.com/ice-blockchain/wintr/connectors/storage/v2" +) + +func FindOrGenerateUserID(ctx context.Context, db *storage.DB, inProgressTable, searchField, searchValue string) (userID string, err error) { + if ctx.Err() != nil { + return "", errors.Wrap(ctx.Err(), "find or generate user by id or telegram id context failed") + } + randomID := IceIDPrefix + uuid.NewString() + + return GetUserIDFromSearch(ctx, db, inProgressTable, searchField, searchValue, randomID) +} + +//nolint:revive // We parameterize search. +func GetUserIDFromSearch(ctx context.Context, db *storage.DB, inProgressTable, searchField, searchValue, idIfNotFound string) (userID string, err error) { + type dbUserID struct { + ID string + } + sql := fmt.Sprintf(`SELECT id FROM ( + SELECT users.id, 1 as idx + FROM users + WHERE %[1]v = $1 + UNION ALL + (SELECT COALESCE(user_id, $2) AS id, 2 as idx + FROM %[2]v + WHERE %[1]v = $1) + ) t ORDER BY idx LIMIT 1`, searchField, inProgressTable) + ids, err := storage.Select[dbUserID](ctx, db, sql, searchValue, idIfNotFound) + if err != nil || len(ids) == 0 { + if storage.IsErr(err, storage.ErrNotFound) || (err == nil && len(ids) == 0) { + return idIfNotFound, nil + } + + return "", errors.Wrapf(err, "failed to find user by:%v", searchValue) + } + + return ids[0].ID, nil +} diff --git a/cmd/eskimo-hut/api/docs.go b/cmd/eskimo-hut/api/docs.go index 86a04345..c0f9cc5b 100644 --- a/cmd/eskimo-hut/api/docs.go +++ b/cmd/eskimo-hut/api/docs.go @@ -1074,6 +1074,56 @@ const docTemplate = `{ } } }, + "/v1w/auth/signInWithTelegram": { + "post": { + "description": "Issues new access token based on telegram token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "parameters": [ + { + "type": "string", + "default": "tma \u003cAdd telegram token here\u003e", + "description": "Insert your TMA token", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.RefreshedToken" + } + }, + "403": { + "description": "if invalid or expired telegram token provided", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, "/v1w/kyc/checkKYCStep4Status/users/{userId}": { "post": { "description": "Checks the status of the quiz kyc step (4).", diff --git a/cmd/eskimo-hut/api/swagger.json b/cmd/eskimo-hut/api/swagger.json index aa58d723..06884b3e 100644 --- a/cmd/eskimo-hut/api/swagger.json +++ b/cmd/eskimo-hut/api/swagger.json @@ -1067,6 +1067,56 @@ } } }, + "/v1w/auth/signInWithTelegram": { + "post": { + "description": "Issues new access token based on telegram token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "parameters": [ + { + "type": "string", + "default": "tma \u003cAdd telegram token here\u003e", + "description": "Insert your TMA token", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.RefreshedToken" + } + }, + "403": { + "description": "if invalid or expired telegram token provided", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + }, + "504": { + "description": "if request times out", + "schema": { + "$ref": "#/definitions/server.ErrorResponse" + } + } + } + } + }, "/v1w/kyc/checkKYCStep4Status/users/{userId}": { "post": { "description": "Checks the status of the quiz kyc step (4).", diff --git a/cmd/eskimo-hut/api/swagger.yaml b/cmd/eskimo-hut/api/swagger.yaml index e174e5c4..d398bc1f 100644 --- a/cmd/eskimo-hut/api/swagger.yaml +++ b/cmd/eskimo-hut/api/swagger.yaml @@ -1513,6 +1513,39 @@ paths: $ref: '#/definitions/server.ErrorResponse' tags: - Auth + /v1w/auth/signInWithTelegram: + post: + consumes: + - application/json + description: Issues new access token based on telegram token + parameters: + - default: tma + description: Insert your TMA token + in: header + name: Authorization + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.RefreshedToken' + "403": + description: if invalid or expired telegram token provided + schema: + $ref: '#/definitions/server.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/server.ErrorResponse' + "504": + description: if request times out + schema: + $ref: '#/definitions/server.ErrorResponse' + tags: + - Auth /v1w/kyc/checkKYCStep4Status/users/{userId}: post: consumes: diff --git a/cmd/eskimo-hut/auth.go b/cmd/eskimo-hut/auth.go index b2e61bfe..4bfc18bb 100644 --- a/cmd/eskimo-hut/auth.go +++ b/cmd/eskimo-hut/auth.go @@ -12,6 +12,7 @@ import ( "github.com/pkg/errors" emaillink "github.com/ice-blockchain/eskimo/auth/email_link" + telegramauth "github.com/ice-blockchain/eskimo/auth/telegram" "github.com/ice-blockchain/eskimo/users" "github.com/ice-blockchain/wintr/auth" "github.com/ice-blockchain/wintr/log" @@ -28,7 +29,8 @@ func (s *service) setupAuthRoutes(router *server.Router) { POST("auth/signInWithConfirmationCode", server.RootHandler(s.SignIn)). POST("auth/getMetadata", server.RootHandler(s.Metadata)). POST("auth/processFaceRecognitionResult", server.RootHandler(s.ProcessFaceRecognitionResult)). - POST("auth/getValidUserForPhoneNumberMigration", server.RootHandler(s.GetValidUserForPhoneNumberMigration)) + POST("auth/getValidUserForPhoneNumberMigration", server.RootHandler(s.GetValidUserForPhoneNumberMigration)). + POST("auth/signInWithTelegram", server.RootHandler(s.SignInWithTelegram)) } // SendSignInLinkToEmail godoc @@ -162,7 +164,7 @@ func (s *service) RegenerateTokens( //nolint:gocritic // . req *server.Request[RefreshToken, RefreshedToken], ) (*server.Response[RefreshedToken], *server.Response[server.ErrorResponse]) { tokenPayload := strings.TrimPrefix(req.Data.Authorization, "Bearer ") - tokens, err := s.authEmailLinkClient.RegenerateTokens(ctx, tokenPayload) + tokens, err := s.tokenRefresher.RegenerateTokens(ctx, tokenPayload) if err != nil { switch { case errors.Is(err, emaillink.ErrUserNotFound): @@ -485,3 +487,36 @@ func (s *service) GetValidUserForPhoneNumberMigration( //nolint:funlen,revive // return server.OK(minimalUsr), nil } + +// SignInWithTelegram godoc +// +// @Schemes +// @Description Issues new access token based on telegram token +// @Tags Auth +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your TMA token" default(tma ) +// @Success 200 {object} RefreshedToken +// @Failure 403 {object} server.ErrorResponse "if invalid or expired telegram token provided" +// @Failure 500 {object} server.ErrorResponse +// @Failure 504 {object} server.ErrorResponse "if request times out" +// @Router /v1w/auth/signInWithTelegram [POST]. +func (s *service) SignInWithTelegram( //nolint:gocritic // . + ctx context.Context, + req *server.Request[TelegramSignIn, RefreshedToken], +) (*server.Response[RefreshedToken], *server.Response[server.ErrorResponse]) { + tokenPayload := strings.TrimPrefix(req.Data.Authorization, "tma ") + tokens, err := s.telegramAuthClient.SignIn(ctx, tokenPayload) + if err != nil { + switch { + case errors.Is(err, telegramauth.ErrExpiredToken): + return nil, server.Forbidden(err) + case errors.Is(err, telegramauth.ErrInvalidToken): + return nil, server.Forbidden(err) + default: + return nil, server.Unexpected(err) + } + } + + return server.OK(&RefreshedToken{Tokens: tokens}), nil +} diff --git a/cmd/eskimo-hut/contract.go b/cmd/eskimo-hut/contract.go index aa21ff9c..78976313 100644 --- a/cmd/eskimo-hut/contract.go +++ b/cmd/eskimo-hut/contract.go @@ -8,7 +8,9 @@ import ( "github.com/pkg/errors" + "github.com/ice-blockchain/eskimo/auth" emaillink "github.com/ice-blockchain/eskimo/auth/email_link" + telegramauth "github.com/ice-blockchain/eskimo/auth/telegram" facekyc "github.com/ice-blockchain/eskimo/kyc/face" kycquiz "github.com/ice-blockchain/eskimo/kyc/quiz" kycsocial "github.com/ice-blockchain/eskimo/kyc/social" @@ -141,7 +143,7 @@ type ( LoginSession string `json:"loginSession" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // . } RefreshedToken struct { - *emaillink.Tokens + *auth.Tokens } MagicLinkPayload struct { EmailToken string `json:"emailToken" required:"true" allowUnauthorized:"true" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // . @@ -154,6 +156,9 @@ type ( RefreshToken struct { Authorization string `header:"Authorization" swaggerignore:"true" required:"true" allowForbiddenWriteOperation:"true" allowUnauthorized:"true"` } + TelegramSignIn struct { + Authorization string `header:"Authorization" swaggerignore:"true" required:"true" allowForbiddenWriteOperation:"true" allowUnauthorized:"true"` + } StartOrContinueKYCStep4SessionRequestBody struct { QuestionNumber *uint8 `form:"questionNumber" required:"true" swaggerignore:"true" example:"11"` SelectedOption *uint8 `form:"selectedOption" required:"true" swaggerignore:"true" example:"0"` @@ -227,6 +232,8 @@ type ( usersProcessor users.Processor quizRepository kycquiz.Repository authEmailLinkClient emaillink.Client + telegramAuthClient telegramauth.Client + tokenRefresher auth.TokenRefresher socialRepository kycsocial.Repository faceKycClient facekyc.Client } diff --git a/cmd/eskimo-hut/eskimo_hut.go b/cmd/eskimo-hut/eskimo_hut.go index f13ff88b..dbec758f 100644 --- a/cmd/eskimo-hut/eskimo_hut.go +++ b/cmd/eskimo-hut/eskimo_hut.go @@ -9,7 +9,9 @@ import ( "github.com/hashicorp/go-multierror" "github.com/pkg/errors" + "github.com/ice-blockchain/eskimo/auth" emaillink "github.com/ice-blockchain/eskimo/auth/email_link" + telegramauth "github.com/ice-blockchain/eskimo/auth/telegram" "github.com/ice-blockchain/eskimo/cmd/eskimo-hut/api" facekyc "github.com/ice-blockchain/eskimo/kyc/face" kycquiz "github.com/ice-blockchain/eskimo/kyc/quiz" @@ -52,6 +54,8 @@ func (s *service) RegisterRoutes(router *server.Router) { func (s *service) Init(ctx context.Context, cancel context.CancelFunc) { s.usersProcessor = users.StartProcessor(ctx, cancel) s.authEmailLinkClient = emaillink.NewClient(ctx, s.usersProcessor, server.Auth(ctx)) + s.telegramAuthClient = telegramauth.NewClient(ctx, server.Auth(ctx)) + s.tokenRefresher = auth.NewRefresher(server.Auth(ctx), s.authEmailLinkClient, s.telegramAuthClient) s.socialRepository = social.New(ctx, s.usersProcessor) s.quizRepository = kycquiz.NewRepository(ctx, s.usersProcessor) s.faceKycClient = facekyc.New(ctx, s.usersProcessor) diff --git a/go.mod b/go.mod index 39861de0..51ed3255 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/google/uuid v1.6.0 github.com/hashicorp/go-multierror v1.1.1 github.com/ice-blockchain/go-tarantool-client v0.0.0-20230327200757-4fc71fa3f7bb - github.com/ice-blockchain/wintr v1.144.0 + github.com/ice-blockchain/wintr v1.147.0 github.com/imroc/req/v3 v3.43.7 github.com/ip2location/ip2location-go/v9 v9.7.0 github.com/jackc/pgx/v5 v5.6.0 @@ -19,6 +19,7 @@ require ( github.com/prometheus/prometheus v0.53.1 github.com/stretchr/testify v1.9.0 github.com/swaggo/swag v1.16.3 + github.com/telegram-mini-apps/init-data-golang v1.1.5 github.com/testcontainers/testcontainers-go v0.32.0 github.com/zeebo/xxh3 v1.0.2 golang.org/x/mod v0.19.0 @@ -39,7 +40,7 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/Microsoft/hcsshim v0.12.4 // indirect + github.com/Microsoft/hcsshim v0.12.5 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -105,7 +106,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/sys/mount v0.3.3 // indirect - github.com/moby/sys/mountinfo v0.7.1 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -156,7 +157,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.25.0 // indirect - golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect + golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.22.0 // indirect diff --git a/go.sum b/go.sum index 99da179e..311ed941 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,8 @@ github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.12.4 h1:Ev7YUMHAHoWNm+aDSPzc5W9s6E2jyL1szpVDJeZ/Rr4= -github.com/Microsoft/hcsshim v0.12.4/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ= +github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0= +github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8= github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs= @@ -218,8 +218,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ice-blockchain/go-tarantool-client v0.0.0-20230327200757-4fc71fa3f7bb h1:8TnFP3mc7O+tc44kv2e0/TpZKnEVUaKH+UstwfBwRkk= github.com/ice-blockchain/go-tarantool-client v0.0.0-20230327200757-4fc71fa3f7bb/go.mod h1:ZsQU7i3mxhgBBu43Oev7WPFbIjP4TniN/b1UPNGbrq8= -github.com/ice-blockchain/wintr v1.144.0 h1:YQE0olkPdSI6AOlw7r/j5jGI6uLciZQrvXFIkN4C4l4= -github.com/ice-blockchain/wintr v1.144.0/go.mod h1:3HAl5nodsetqQN30q3gUvsxgfq2B7F86Os/II7/5GPQ= +github.com/ice-blockchain/wintr v1.147.0 h1:VQxvK3FWFIbm+X6obrecx7n07cBZoQDf/dQX8gfCcTc= +github.com/ice-blockchain/wintr v1.147.0/go.mod h1:sVnoMxZc86JFoao4Beva7Z2EbNLhX8XlWsAeWNwfbCE= github.com/imroc/req/v3 v3.43.7 h1:dOcNb9n0X83N5/5/AOkiU+cLhzx8QFXjv5MhikazzQA= github.com/imroc/req/v3 v3.43.7/go.mod h1:SQIz5iYop16MJxbo8ib+4LnostGCok8NQf8ToyQc2xA= github.com/ip2location/ip2location-go/v9 v9.7.0 h1:ipwl67HOWcrw+6GOChkEXcreRQR37NabqBd2ayYa4Q0= @@ -275,8 +275,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs= github.com/moby/sys/mount v0.3.3/go.mod h1:PBaEorSNTLG5t/+4EgukEQVlAvVEc6ZjTySwKdqp5K0= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= -github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= -github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -378,6 +378,8 @@ github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+z github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/telegram-mini-apps/init-data-golang v1.1.5 h1:R51eoGSKBQwHoAo8r/n/E0RZ2owF3kmEpdzn7oV7lgI= +github.com/telegram-mini-apps/init-data-golang v1.1.5/go.mod h1:GG4HnRx9ocjD4MjjzOw7gf9Ptm0NvFbDr5xqnfFOYuY= github.com/testcontainers/testcontainers-go v0.15.0 h1:3Ex7PUGFv0b2bBsdOv6R42+SK2qoZnWBd21LvZYhUtQ= github.com/testcontainers/testcontainers-go v0.15.0/go.mod h1:PkohMRH2X8Hib0IWtifVexDfLPVT+tb5E9hsf7cW12w= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= @@ -433,8 +435,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= -golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA= +golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=