package transcode import ( "context" "scrabble/gateway/internal/backendclient" "scrabble/gateway/internal/vkid" fb "scrabble/pkg/fbs/scrabblefb" ) // Account linking & merge message types. The email ops carry the costly- // email rate flag; the telegram ops validate Login Widget data through the // connector (registered only when the connector is configured). All are // authenticated. The merge ops are the explicit irreversible step, gated in the UI // after a merge_required confirm. const ( MsgLinkEmailRequest = "link.email.request" MsgLinkEmailConfirm = "link.email.confirm" MsgLinkEmailMerge = "link.email.merge" MsgLinkTelegram = "link.telegram.confirm" MsgLinkTelegramMerge = "link.telegram.merge" MsgLinkVK = "link.vk.confirm" MsgLinkVKMerge = "link.vk.merge" MsgLinkUnlink = "link.unlink" MsgEmailChangeRequest = "link.email.change.request" MsgEmailChangeConfirm = "link.email.change.confirm" MsgAccountDeleteReq = "account.delete.request" MsgAccountDeleteConf = "account.delete.confirm" ) // registerLinkOps adds the linking & merge operations. The telegram ops need the // connector's Login Widget validator, so they are registered only when tg is set. func registerLinkOps(r *Registry, backend *backendclient.Client, tg TelegramValidator) { r.ops[MsgLinkEmailRequest] = Op{Handler: linkEmailRequestHandler(backend), Auth: true, Email: true} r.ops[MsgLinkEmailConfirm] = Op{Handler: linkEmailConfirmHandler(backend), Auth: true, Email: true} r.ops[MsgLinkEmailMerge] = Op{Handler: linkEmailMergeHandler(backend), Auth: true, Email: true} r.ops[MsgLinkUnlink] = Op{Handler: linkUnlinkHandler(backend), Auth: true} r.ops[MsgEmailChangeRequest] = Op{Handler: changeEmailRequestHandler(backend), Auth: true, Email: true} r.ops[MsgEmailChangeConfirm] = Op{Handler: changeEmailConfirmHandler(backend), Auth: true, Email: true} r.ops[MsgAccountDeleteReq] = Op{Handler: deleteRequestHandler(backend), Auth: true, Email: true} r.ops[MsgAccountDeleteConf] = Op{Handler: deleteConfirmHandler(backend), Auth: true} if tg != nil { r.ops[MsgLinkTelegram] = Op{Handler: linkTelegramHandler(backend, tg, false), Auth: true} r.ops[MsgLinkTelegramMerge] = Op{Handler: linkTelegramHandler(backend, tg, true), Auth: true} } } func linkEmailRequestHandler(backend *backendclient.Client) Handler { return func(ctx context.Context, req Request) ([]byte, error) { in := fb.GetRootAsLinkEmailRequest(req.Payload, 0) if err := backend.LinkEmailRequest(ctx, req.UserID, string(in.Email())); err != nil { return nil, err } return encodeAck(true), nil } } func linkEmailConfirmHandler(backend *backendclient.Client) Handler { return func(ctx context.Context, req Request) ([]byte, error) { in := fb.GetRootAsLinkEmailConfirm(req.Payload, 0) res, err := backend.LinkEmailConfirm(ctx, req.UserID, string(in.Email()), string(in.Code())) if err != nil { return nil, err } return encodeLinkResult(res), nil } } func linkEmailMergeHandler(backend *backendclient.Client) Handler { return func(ctx context.Context, req Request) ([]byte, error) { in := fb.GetRootAsLinkEmailConfirm(req.Payload, 0) res, err := backend.LinkEmailMerge(ctx, req.UserID, string(in.Email()), string(in.Code())) if err != nil { return nil, err } return encodeLinkResult(res), nil } } // changeEmailRequestHandler mails a confirm-code to a new address for an email change. func changeEmailRequestHandler(backend *backendclient.Client) Handler { return func(ctx context.Context, req Request) ([]byte, error) { in := fb.GetRootAsLinkEmailRequest(req.Payload, 0) if err := backend.ChangeEmailRequest(ctx, req.UserID, string(in.Email())); err != nil { return nil, err } return encodeAck(true), nil } } // changeEmailConfirmHandler verifies the code and switches the account's email, returning // the refreshed link result (status "changed" + the updated profile). func changeEmailConfirmHandler(backend *backendclient.Client) Handler { return func(ctx context.Context, req Request) ([]byte, error) { in := fb.GetRootAsLinkEmailConfirm(req.Payload, 0) res, err := backend.ChangeEmailConfirm(ctx, req.UserID, string(in.Email()), string(in.Code())) if err != nil { return nil, err } return encodeLinkResult(res), nil } } // deleteRequestHandler starts account deletion, returning which step-up the account uses. func deleteRequestHandler(backend *backendclient.Client) Handler { return func(ctx context.Context, req Request) ([]byte, error) { res, err := backend.DeleteRequest(ctx, req.UserID) if err != nil { return nil, err } return encodeDeleteRequestResult(res.Method), nil } } // deleteConfirmHandler verifies the step-up proof and deletes the account. func deleteConfirmHandler(backend *backendclient.Client) Handler { return func(ctx context.Context, req Request) ([]byte, error) { in := fb.GetRootAsAccountDeleteConfirm(req.Payload, 0) if err := backend.DeleteConfirm(ctx, req.UserID, string(in.Code()), string(in.Phrase())); err != nil { return nil, err } return encodeAck(true), nil } } // linkUnlinkHandler detaches a platform identity (telegram|vk) from the caller and // returns the refreshed link result (status "unlinked" + the updated profile). func linkUnlinkHandler(backend *backendclient.Client) Handler { return func(ctx context.Context, req Request) ([]byte, error) { in := fb.GetRootAsLinkUnlinkRequest(req.Payload, 0) res, err := backend.LinkUnlink(ctx, req.UserID, string(in.Kind())) if err != nil { return nil, err } return encodeLinkResult(res), nil } } // linkTelegramHandler validates Login Widget data via the connector and then calls // the backend's link or merge endpoint with the trusted Telegram external id. func linkTelegramHandler(backend *backendclient.Client, tg TelegramValidator, merge bool) Handler { return func(ctx context.Context, req Request) ([]byte, error) { in := fb.GetRootAsLinkTelegramRequest(req.Payload, 0) user, err := tg.ValidateLoginWidget(ctx, string(in.Data())) if err != nil { return nil, err } var res backendclient.LinkResultResp if merge { res, err = backend.LinkTelegramMerge(ctx, req.UserID, user.ExternalID) } else { res, err = backend.LinkTelegram(ctx, req.UserID, user.ExternalID) } if err != nil { return nil, err } return encodeLinkResult(res), nil } } // VKIDExchanger completes a VK ID web authorization-code exchange, returning the // launching user's trusted VK identity. It is satisfied by *vkid.Exchanger and lets the // VK web-link ops resolve a browser VK login, which — unlike the Mini App path — has no // offline launch signature to verify. type VKIDExchanger interface { Exchange(ctx context.Context, code, deviceID, codeVerifier string) (vkid.Identity, error) } // registerVKLinkOps adds the VK web-link ops when a VK ID exchanger is configured; a nil // exchanger leaves them unregistered (VK ID web login not configured). func registerVKLinkOps(r *Registry, backend *backendclient.Client, ex VKIDExchanger) { if ex == nil { return } r.ops[MsgLinkVK] = Op{Handler: linkVKHandler(backend, ex, false), Auth: true} r.ops[MsgLinkVKMerge] = Op{Handler: linkVKHandler(backend, ex, true), Auth: true} } // linkVKHandler completes the VK ID code exchange (server-side, under the app's // protected key) and then calls the backend's link or merge endpoint with the trusted // VK external id. func linkVKHandler(backend *backendclient.Client, ex VKIDExchanger, merge bool) Handler { return func(ctx context.Context, req Request) ([]byte, error) { in := fb.GetRootAsLinkVKRequest(req.Payload, 0) user, err := ex.Exchange(ctx, string(in.Code()), string(in.DeviceId()), string(in.CodeVerifier())) if err != nil { return nil, err } var res backendclient.LinkResultResp if merge { res, err = backend.LinkVKMerge(ctx, req.UserID, user.ExternalID) } else { res, err = backend.LinkVK(ctx, req.UserID, user.ExternalID) } if err != nil { return nil, err } return encodeLinkResult(res), nil } }