diff --git a/config/default.json b/config/default.json index 2909f1f81..2ce3c53eb 100644 --- a/config/default.json +++ b/config/default.json @@ -745,6 +745,9 @@ "eventLogGroupId": "", "enabled": false, "botToken": "", + "clientId": "", + "clientSecret": "", + "redirectUri": "http://localhost:8080/auth/telegram/callback", "groups": [], "trialPeriod": { "start": { diff --git a/config/local.example.json b/config/local.example.json index 7ec7939d2..86ff57158 100644 --- a/config/local.example.json +++ b/config/local.example.json @@ -83,6 +83,17 @@ "blockedGuilds": [], "allowedUsers": [], "clientPrompt": "none" + }, + { + "enabled": false, + "type": "telegram", + "name": "telegram", + "botToken": "", + "clientId": "", + "clientSecret": "", + "redirectUri": "http://localhost:8080/auth/telegram/callback", + "groups": [], + "allowedUsers": [] } ], "areaRestrictions": [ diff --git a/package.json b/package.json index ae0ad100b..d4a589b3a 100644 --- a/package.json +++ b/package.json @@ -149,6 +149,7 @@ "i18next-browser-languagedetector": "8.0.0", "i18next-fs-backend": "2.6.6", "i18next-http-backend": "3.0.5", + "jose": "^5.9.6", "knex": "3.1.0", "leaflet": "1.9.4", "leaflet-arrowheads": "^1.4.0", @@ -166,6 +167,7 @@ "passport": "^0.6.0", "passport-discord": "https://github.com/tonestrike/passport-discord.git", "passport-local": "^1.0.0", + "passport-oauth2": "^1.8.0", "react": "19.2.8", "react-dom": "19.2.8", "react-ga4": "^1.4.1", diff --git a/packages/config/.configref b/packages/config/.configref index c0d681f70..4929b665d 100644 --- a/packages/config/.configref +++ b/packages/config/.configref @@ -1 +1 @@ -26052 \ No newline at end of file +26175 \ No newline at end of file diff --git a/packages/locales/lib/human/en.json b/packages/locales/lib/human/en.json index a4c26bcdc..b722b2c23 100644 --- a/packages/locales/lib/human/en.json +++ b/packages/locales/lib/human/en.json @@ -477,6 +477,7 @@ "go_back": "Go Back", "access": "Access", "link_discord": "Link Discord", + "link_telegram": "Link Telegram", "select_webhook_strategy": "Alert Manager", "webhook_strategy_success_0": "Success! Refreshing to fetch alert settings...", "register": "Register", diff --git a/packages/types/lib/augmentations.d.ts b/packages/types/lib/augmentations.d.ts index 0d6ac029c..27f699573 100644 --- a/packages/types/lib/augmentations.d.ts +++ b/packages/types/lib/augmentations.d.ts @@ -51,6 +51,10 @@ declare module '@mui/material/styles' { fuchsia: string red: string } + telegram: { + main: string + contrastText: string + } } interface PaletteOptions { @@ -61,6 +65,10 @@ declare module '@mui/material/styles' { fuchsia: string red: string } + telegram?: { + main: string + contrastText: string + } } } diff --git a/packages/types/lib/blocks.d.ts b/packages/types/lib/blocks.d.ts index b5a2a1e17..1e01505b6 100644 --- a/packages/types/lib/blocks.d.ts +++ b/packages/types/lib/blocks.d.ts @@ -52,6 +52,8 @@ interface CustomTelegram extends BaseBlock { type: 'telegram' telegramBotName: string telegramAuthUrl: string + /** Resolved server side from `telegramAuthUrl`, not set in config */ + telegramOAuth?: boolean } interface CustomLocal extends BaseBlock { diff --git a/server/src/graphql/resolvers.js b/server/src/graphql/resolvers.js index f27431974..e3ac01cbd 100644 --- a/server/src/graphql/resolvers.js +++ b/server/src/graphql/resolvers.js @@ -10,6 +10,7 @@ const { missing, readAndParseJson } = require('@rm/locales') const { buildDefaultFilters } = require('../filters/builder/base') const { filterComponents } = require('../utils/filterComponents') +const { annotateTelegramBlocks } = require('../utils/getTelegramStrategy') const { validateSelectedWebhook } = require('../utils/validateSelectedWebhook') const { PoracleAPI } = require('../services/Poracle') const { geocoder } = require('../services/geocoder') @@ -146,14 +147,16 @@ const resolvers = { components = [], ...rest } = config.getMapConfig(req)[component] + const strategies = config.getSafe('authentication.strategies') + const prepare = (blocks) => + annotateTelegramBlocks( + filterComponents(blocks, !!username, perms.donor), + strategies, + ) return { ...rest, - footerButtons: filterComponents( - footerButtons, - !!username, - perms.donor, - ), - components: filterComponents(components, !!username, perms.donor), + footerButtons: prepare(footerButtons), + components: prepare(components), } } return null diff --git a/server/src/routes/authRouter.js b/server/src/routes/authRouter.js index 71b0a391e..bdb0093f6 100644 --- a/server/src/routes/authRouter.js +++ b/server/src/routes/authRouter.js @@ -70,6 +70,21 @@ const loadAuthStrategies = () => { delete req.session.discordPromptRetry } + // Telegram's OAuth flow reports a cancelled consent screen as an error + // param, which passport-oauth2 turns into a thrown AuthorizationError. + // Send those to /blocked, the same place a rejected login ends up. + if ( + strategy.type === 'telegram' && + typeof req.query.error === 'string' + ) { + log.debug(TAGS.auth, 'Telegram auth was denied:', req.query.error) + return res.redirect( + `/blocked/${encodeURIComponent( + new URLSearchParams({ message: 'access_denied' }).toString(), + )}`, + ) + } + return passport.authenticate( name, getAuthenticateOptions(req), diff --git a/server/src/services/TelegramClient.js b/server/src/services/TelegramClient.js index 30a2df836..51cb7619e 100644 --- a/server/src/services/TelegramClient.js +++ b/server/src/services/TelegramClient.js @@ -1,7 +1,9 @@ // @ts-check const { default: fetch } = require('node-fetch') const { TelegramStrategy } = require('@rainb0w-clwn/passport-telegram-official') +const { createRemoteJWKSet, jwtVerify } = require('jose') const passport = require('passport') +const OAuth2Strategy = require('passport-oauth2') const config = require('@rm/config') @@ -15,8 +17,38 @@ const { AuthClient } = require('./AuthClient') /** * @typedef {import('@rainb0w-clwn/passport-telegram-official/dist/types').PassportTelegramUser} TGUser + * @typedef {Parameters[0]} AuthRequest */ +const TG_ISSUER = 'https://oauth.telegram.org' +const TG_AUTHORIZATION_URL = `${TG_ISSUER}/auth` +const TG_TOKEN_URL = `${TG_ISSUER}/token` +const TG_JWKS_URL = `${TG_ISSUER}/.well-known/jwks.json` + +/** + * Telegram rotates its signing keys, so the set is fetched lazily and cached + * by `jose` rather than pinned at boot. Shared across every telegram strategy + * since the keys are not client specific. + */ +const getJwks = (() => { + /** @type {ReturnType} */ + let jwks + return () => { + if (!jwks) jwks = createRemoteJWKSet(new URL(TG_JWKS_URL)) + return jwks + } +})() + +/** + * JWT claims are `unknown` until narrowed, and the optional ones are simply + * absent when the user has not set them on their Telegram account. + * + * @param {unknown} claim + * @returns {string | undefined} + */ +const claimToString = (claim) => + claim === undefined || claim === null ? undefined : String(claim) + class TelegramClient extends AuthClient { /** @param {TGUser} user */ async getUserGroups(user) { @@ -242,15 +274,99 @@ class TelegramClient extends AuthClient { } } + /** + * Telegram's OpenID Connect provider has no UserInfo endpoint - the profile + * is carried by the `id_token` returned from the token exchange, so it has to + * be verified against the JWKS before anything in it is trusted. + * + * The `sub` claim is an opaque, client specific identifier. The actual + * Telegram user id only arrives as the `id` claim under the `profile` scope, + * and that is what the rest of ReactMap keys off of (`users.telegramId`, + * `strategy.groups`, `strategy.allowedUsers`, the `getChatMember` lookup), so + * `sub` is deliberately ignored. + * + * @param {AuthRequest} req + * @param {Record} params token endpoint response + * @param {(err: any, user?: any, info?: any) => void} done + */ + async oidcHandler(req, params, done) { + try { + if (!params?.id_token) { + throw new Error('No id_token was returned by Telegram') + } + const { payload } = await jwtVerify(params.id_token, getJwks(), { + issuer: TG_ISSUER, + audience: String(this.strategy.clientId), + }) + if (!payload.id) { + throw new Error( + 'The id_token has no `id` claim, the `profile` scope was not granted', + ) + } + const firstName = claimToString(payload.given_name) + const lastName = claimToString(payload.family_name) + + return this.authHandler( + req, + // The OIDC flow has no `hash` or `auth_date` - those belong to the + // legacy widget - so this is not a complete PassportTelegramUser + // @ts-ignore + { + // String, to match both the `telegramId` varchar column and the + // string role ids that `groups` / `allowedUsers` are compared against + id: String(payload.id), + username: claimToString(payload.preferred_username), + first_name: firstName, + last_name: lastName, + name: { givenName: firstName, familyName: lastName }, + photo_url: claimToString(payload.picture), + provider: 'telegram', + }, + done, + ) + } catch (e) { + this.log.error('Unable to validate the Telegram id_token', e) + return done(null, false, { message: 'access_denied' }) + } + } + initPassport() { + const { clientId, clientSecret } = this.strategy + + if (!clientId || !clientSecret) { + // Legacy hash signed Login Widget, still supported by Telegram + passport.use( + this.rmStrategy, + new TelegramStrategy( + { + botToken: this.strategy.botToken, + passReqToCallback: true, + }, + (req, profile, done) => this.authHandler(req, profile, done), + ), + ) + return + } + passport.use( this.rmStrategy, - new TelegramStrategy( + new OAuth2Strategy( { - botToken: this.strategy.botToken, + authorizationURL: TG_AUTHORIZATION_URL, + tokenURL: TG_TOKEN_URL, + clientID: clientId, + clientSecret, + callbackURL: this.strategy.redirectUri, + // `profile` is required, it is the only source of the Telegram user id + scope: ['openid', 'profile'], + state: true, + pkce: 'S256', passReqToCallback: true, }, - (req, profile, done) => this.authHandler(req, profile, done), + // The 6 argument arity is what makes passport-oauth2 hand us `params`, + // which is where the id_token lives + (req, _accessToken, _refreshToken, params, _profile, done) => + this.oidcHandler(req, params, done), ), ) } diff --git a/server/src/utils/getServerSettings.js b/server/src/utils/getServerSettings.js index 790845db0..5e75852ac 100644 --- a/server/src/utils/getServerSettings.js +++ b/server/src/utils/getServerSettings.js @@ -4,6 +4,7 @@ const config = require('@rm/config') const { clientOptions } = require('../ui/clientOptions') const { advMenus } = require('../ui/advMenus') const { drawer } = require('../ui/drawer') +const { isTelegramOAuth } = require('./getTelegramStrategy') /** * @@ -52,6 +53,12 @@ function getServerSettings(req) { loggedIn: !!req.user, excludeList: authentication.excludeFromTutorial, methods: authentication.methods, + // Resolved per domain, since customRoutes is part of the domain's map + // config and each domain can target a different telegram strategy + telegramOAuth: isTelegramOAuth( + mapConfig.customRoutes.telegramAuthUrl, + authentication.strategies, + ), }, database: { settings: { diff --git a/server/src/utils/getTelegramStrategy.js b/server/src/utils/getTelegramStrategy.js new file mode 100644 index 000000000..e3030bea7 --- /dev/null +++ b/server/src/utils/getTelegramStrategy.js @@ -0,0 +1,96 @@ +// @ts-check + +/** + * The client renders a single Telegram login control per domain, pointed at + * `map.customRoutes.telegramAuthUrl`. Which flow that control has to use - the + * OAuth/OIDC redirect or the legacy hash signed widget - is a property of the + * one strategy sitting behind that route, not of the strategy list as a whole, + * since a config can enable several Telegram strategies at once and a + * multiDomain setup can point each domain at a different one. + * + * @param {string} authUrl the configured `telegramAuthUrl` + * @returns {string | null} the strategy name the route resolves to + */ +function getStrategyNameFromAuthUrl(authUrl) { + if (!authUrl) return null + // Tolerate absolute URLs, the config allows either form + const pathname = authUrl.startsWith('http') + ? URL.canParse(authUrl) + ? new URL(authUrl).pathname + : '' + : authUrl + const segments = pathname.split('/').filter(Boolean) + const authIndex = segments.lastIndexOf('auth') + // `/auth/` and `/auth//callback` are the two shapes authRouter + // registers, so the name is always the segment right after `auth` + return authIndex === -1 ? null : (segments[authIndex + 1] ?? null) +} + +/** + * Resolves the Telegram strategy that a login control points at. + * + * @param {string} authUrl the configured `telegramAuthUrl` + * @param {import('@rm/types').StrategyConfig[]} strategies + * @returns {import('@rm/types').StrategyConfig | null} + */ +function getTelegramStrategy(authUrl, strategies) { + const enabled = strategies.filter((s) => s.enabled && s.type === 'telegram') + if (!enabled.length) return null + + const name = getStrategyNameFromAuthUrl(authUrl) + const byName = name ? enabled.find((s) => s.name === name) : undefined + if (byName) return byName + + // A custom or proxied auth URL will not resolve by name. With only one + // Telegram strategy there is no ambiguity, so use it - otherwise there is no + // way to tell which one the route means, and the legacy widget is the safer + // guess since it is what every pre-OAuth config already runs. + return enabled.length === 1 ? enabled[0] : null +} + +/** + * Whether the Telegram strategy behind a login control runs the OAuth/OIDC + * flow. Anything else falls back to the legacy hash signed widget. + * + * @param {string} authUrl the configured `telegramAuthUrl` + * @param {import('@rm/types').StrategyConfig[]} strategies + * @returns {boolean} + */ +function isTelegramOAuth(authUrl, strategies) { + const strategy = getTelegramStrategy(authUrl, strategies) + return !!(strategy?.clientId && strategy?.clientSecret) +} + +/** + * Custom login page blocks carry their own `telegramAuthUrl`, which can point + * at a different strategy than the domain's `customRoutes` default, so each + * block gets its flow resolved from its own route rather than inheriting the + * page level flag. + * + * @param {import("@rm/types").CustomComponent[]} components + * @param {import('@rm/types').StrategyConfig[]} strategies + * @returns {import("@rm/types").CustomComponent[]} + */ +function annotateTelegramBlocks(components, strategies) { + return (Array.isArray(components) ? components : []).map((component) => { + if ('components' in component && Array.isArray(component.components)) { + return { + ...component, + components: annotateTelegramBlocks(component.components, strategies), + } + } + return component.type === 'telegram' + ? { + ...component, + telegramOAuth: isTelegramOAuth(component.telegramAuthUrl, strategies), + } + : component + }) +} + +module.exports = { + annotateTelegramBlocks, + getStrategyNameFromAuthUrl, + getTelegramStrategy, + isTelegramOAuth, +} diff --git a/server/test/telegramStrategyResolution.test.js b/server/test/telegramStrategyResolution.test.js new file mode 100644 index 000000000..3f39f3b29 --- /dev/null +++ b/server/test/telegramStrategyResolution.test.js @@ -0,0 +1,191 @@ +const assert = require('node:assert/strict') +const { test } = require('node:test') + +const { + annotateTelegramBlocks, + getStrategyNameFromAuthUrl, + getTelegramStrategy, + isTelegramOAuth, +} = require('../src/utils/getTelegramStrategy') + +/** @param {object} overrides */ +const telegram = (overrides) => ({ + name: 'telegram', + type: 'telegram', + enabled: true, + botToken: '1:A', + clientId: '', + clientSecret: '', + ...overrides, +}) + +const LEGACY = telegram({ name: 'telegram' }) +const OAUTH = telegram({ + name: 'telegram-oauth', + clientId: '123', + clientSecret: 'shh', +}) + +test('resolves the strategy name from both route shapes', () => { + assert.equal( + getStrategyNameFromAuthUrl('/auth/telegram/callback'), + 'telegram', + ) + assert.equal(getStrategyNameFromAuthUrl('/auth/telegram'), 'telegram') + assert.equal( + getStrategyNameFromAuthUrl('https://map.example/auth/tg-two/callback'), + 'tg-two', + ) + assert.equal(getStrategyNameFromAuthUrl(''), null) + assert.equal(getStrategyNameFromAuthUrl('/login'), null) + assert.equal(getStrategyNameFromAuthUrl('http://['), null) +}) + +test('picks the strategy the auth url points at, not just any telegram one', () => { + const strategies = [LEGACY, OAUTH] + + assert.equal( + getTelegramStrategy('/auth/telegram/callback', strategies).name, + 'telegram', + ) + assert.equal( + getTelegramStrategy('/auth/telegram-oauth/callback', strategies).name, + 'telegram-oauth', + ) +}) + +test('a second OAuth strategy does not flip the legacy route to OAuth', () => { + const strategies = [LEGACY, OAUTH] + + // the regression: `some()` over all strategies reported OAuth here, so the + // login page rendered a redirect link for a route running the hash widget + assert.equal(isTelegramOAuth('/auth/telegram/callback', strategies), false) + assert.equal( + isTelegramOAuth('/auth/telegram-oauth/callback', strategies), + true, + ) +}) + +test('a single telegram strategy resolves even from an unrecognized url', () => { + assert.equal(isTelegramOAuth('/custom/proxy/path', [OAUTH]), true) + assert.equal(isTelegramOAuth('/custom/proxy/path', [LEGACY]), false) +}) + +test('an ambiguous unrecognized url falls back to the legacy widget', () => { + assert.equal(isTelegramOAuth('/custom/proxy/path', [LEGACY, OAUTH]), false) +}) + +test('disabled strategies are ignored', () => { + const disabledOAuth = { ...OAUTH, name: 'telegram', enabled: false } + + assert.equal( + getTelegramStrategy('/auth/telegram/callback', [disabledOAuth]), + null, + ) + assert.equal( + isTelegramOAuth('/auth/telegram/callback', [disabledOAuth]), + false, + ) + assert.equal(isTelegramOAuth('/auth/telegram/callback', []), false) +}) + +test('half configured credentials stay on the legacy widget', () => { + const idOnly = telegram({ clientId: '123' }) + const secretOnly = telegram({ clientSecret: 'shh' }) + + assert.equal(isTelegramOAuth('/auth/telegram/callback', [idOnly]), false) + assert.equal(isTelegramOAuth('/auth/telegram/callback', [secretOnly]), false) +}) + +test('non telegram strategies never match', () => { + const discord = { + name: 'discord', + type: 'discord', + enabled: true, + clientId: '123', + clientSecret: 'shh', + } + + assert.equal(getTelegramStrategy('/auth/discord/callback', [discord]), null) + assert.equal(isTelegramOAuth('/auth/discord/callback', [discord]), false) +}) + +test('annotates custom login page blocks from their own auth url', () => { + const strategies = [LEGACY, OAUTH] + // the shape people actually have in their loginPage config + const blocks = [ + { + type: 'telegram', + gridSizes: { sm: 6 }, + telegramBotName: 'CandyMapBot', + telegramAuthUrl: '/auth/telegram/callback', + gridStyle: { marginTop: 20, textDecoration: 'none' }, + }, + { + type: 'telegram', + telegramBotName: 'CandyMapBot', + telegramAuthUrl: '/auth/telegram-oauth/callback', + }, + { type: 'discord', link: '/auth/discord/callback' }, + ] + + const [legacyBlock, oauthBlock, discordBlock] = annotateTelegramBlocks( + blocks, + strategies, + ) + + assert.equal(legacyBlock.telegramOAuth, false) + assert.equal(oauthBlock.telegramOAuth, true) + assert.equal('telegramOAuth' in discordBlock, false) + // every other key on the block survives untouched + assert.deepEqual(legacyBlock.gridSizes, { sm: 6 }) + assert.equal(legacyBlock.telegramBotName, 'CandyMapBot') + assert.deepEqual(legacyBlock.gridStyle, { + marginTop: 20, + textDecoration: 'none', + }) +}) + +test('annotates telegram blocks nested inside parent blocks', () => { + const annotated = annotateTelegramBlocks( + [ + { + type: 'parent', + components: [ + { type: 'telegram', telegramAuthUrl: '/auth/telegram/callback' }, + { + type: 'parent', + components: [ + { + type: 'telegram', + telegramAuthUrl: '/auth/telegram-oauth/callback', + }, + ], + }, + ], + }, + ], + [LEGACY, OAUTH], + ) + + assert.equal(annotated[0].components[0].telegramOAuth, false) + assert.equal(annotated[0].components[1].components[0].telegramOAuth, true) +}) + +test('annotating does not mutate the config objects it is given', () => { + const block = { + type: 'telegram', + telegramAuthUrl: '/auth/telegram-oauth/callback', + } + const blocks = [block] + + annotateTelegramBlocks(blocks, [LEGACY, OAUTH]) + + assert.equal('telegramOAuth' in block, false) + assert.equal(blocks[0], block) +}) + +test('annotating tolerates a missing or empty component list', () => { + assert.deepEqual(annotateTelegramBlocks(undefined, [OAUTH]), []) + assert.deepEqual(annotateTelegramBlocks([], [OAUTH]), []) +}) diff --git a/src/assets/theme.js b/src/assets/theme.js index cc0be35b7..a54f0fda5 100644 --- a/src/assets/theme.js +++ b/src/assets/theme.js @@ -192,6 +192,10 @@ export function useCustomTheme() { fuchsia: '#EB459E', red: '#ED4245', }, + telegram: { + main: '#2AABEE', + contrastText: '#fff', + }, }, components, }), diff --git a/src/components/Config.jsx b/src/components/Config.jsx index 6fe50a58b..7595f60e9 100644 --- a/src/components/Config.jsx +++ b/src/components/Config.jsx @@ -110,6 +110,7 @@ export function Config({ children }) { loggedIn: !!data.user?.loggedIn, perms: data.user ? data.user.perms : {}, methods: data.authentication.methods || [], + telegramOAuth: !!data.authentication.telegramOAuth, username: data.user?.username || '', data: data.user?.data ? typeof data.user?.data === 'string' diff --git a/src/components/auth/Telegram.jsx b/src/components/auth/Telegram.jsx index 31ffdb601..ee38a689f 100644 --- a/src/components/auth/Telegram.jsx +++ b/src/components/auth/Telegram.jsx @@ -1,7 +1,16 @@ // @ts-check import * as React from 'react' +import Button from '@mui/material/Button' +import { useTranslation } from 'react-i18next' + +import { useMemory } from '@store/useMemory' + +import { I } from '../I' /** + * Legacy hash signed Login Widget. Telegram has archived its documentation in + * favor of the OAuth/OIDC flow, but it still works, so it stays the default for + * anyone who has not set a `clientId`/`clientSecret` on their strategy. * * @param {{ botName: string, authUrl: string }} props * @returns @@ -34,3 +43,56 @@ export function TelegramWidget({ botName, authUrl }) { return
} + +/** + * OAuth/OIDC entry point. Like Discord, the href points at the callback route, + * which passport redirects away from when there is no `code` in the query. + * + * @param {{ children?: string, bgcolor?: string } & import('@mui/material/Button').ButtonProps} props + * @returns {React.JSX.Element} + */ +export function TelegramButton({ + href = '/auth/telegram/callback', + children = 'login', + size = 'large', + bgcolor = 'telegram.main', + ...props +}) { + const { t } = useTranslation() + + return ( + // TODO: Augment Mui Types + + ) +} + +/** + * Renders whichever Telegram flow the route in `authUrl` is running. + * + * Custom login page blocks carry their own `telegramAuthUrl`, which can point + * at a different strategy than the domain default, so they pass the flow the + * server resolved for that block. Everything else uses the flow resolved for + * `customRoutes.telegramAuthUrl`. + * + * @param {{ botName: string, authUrl: string, telegramOAuth?: boolean } & Omit[0], 'href'>} props + * @returns + */ +export function TelegramLogin({ botName, authUrl, telegramOAuth, ...props }) { + const domainDefault = useMemory((s) => s.auth.telegramOAuth) + const isOAuth = telegramOAuth ?? domainDefault + + return isOAuth ? ( + + ) : ( + + ) +} diff --git a/src/features/builder/components/Generator.jsx b/src/features/builder/components/Generator.jsx index eb52e499b..4d5527425 100644 --- a/src/features/builder/components/Generator.jsx +++ b/src/features/builder/components/Generator.jsx @@ -6,7 +6,7 @@ import Grid from '@mui/material/Unstable_Grid2' import { DiscordButton } from '@components/auth/Discord' import { LocalLogin } from '@components/auth/Local' -import { TelegramWidget } from '@components/auth/Telegram' +import { TelegramLogin } from '@components/auth/Telegram' import { Img } from '@components/Img' import { LocaleSelection } from '@components/inputs/LocaleSelection' @@ -46,9 +46,10 @@ export function Generator({ block, defaultReturn = null }) { return case 'telegram': return ( - ) case 'discord': diff --git a/src/features/profile/LinkAccounts.jsx b/src/features/profile/LinkAccounts.jsx index ba89f7b87..3408e4364 100644 --- a/src/features/profile/LinkAccounts.jsx +++ b/src/features/profile/LinkAccounts.jsx @@ -11,7 +11,7 @@ import { useMemory } from '@store/useMemory' import { Query } from '@services/queries' import { METHODS } from '@assets/constants' import { DiscordButton } from '@components/auth/Discord' -import { TelegramWidget } from '@components/auth/Telegram' +import { TelegramLogin } from '@components/auth/Telegram' import { Notification } from '@components/Notification' import { getProperName } from '@utils/strings' @@ -36,10 +36,13 @@ export function LinkAccounts() { {METHODS.map((method, i) => { if (!auth.methods.includes(method)) return null const Component = i ? ( - + size="medium" + > + link_telegram + ) : ( {t('link_discord')} diff --git a/src/pages/login/Methods.jsx b/src/pages/login/Methods.jsx index d2ecf117a..4776af004 100644 --- a/src/pages/login/Methods.jsx +++ b/src/pages/login/Methods.jsx @@ -4,7 +4,7 @@ import Grid from '@mui/material/Unstable_Grid2' import { useTranslation } from 'react-i18next' import { DiscordButton } from '@components/auth/Discord' -import { TelegramWidget } from '@components/auth/Telegram' +import { TelegramLogin } from '@components/auth/Telegram' import { LocalLogin } from '@components/auth/Local' import { useMemory } from '@store/useMemory' @@ -46,7 +46,7 @@ function Telegram() { ) return ( - + ) } diff --git a/src/store/useMemory.js b/src/store/useMemory.js index d4ed1630c..67a4903bd 100644 --- a/src/store/useMemory.js +++ b/src/store/useMemory.js @@ -18,6 +18,7 @@ import { create } from 'zustand' * perms: Partial, * loggedIn: boolean, * methods: import('@rm/types').Strategy[], + * telegramOAuth: boolean, * strategy: import('@rm/types').Strategy | '', * userBackupLimits: number, * excludeList: string[], @@ -92,6 +93,7 @@ export const useMemory = create(() => ({ loggedIn: false, perms: {}, methods: [], + telegramOAuth: false, username: '', data: {}, counts: { diff --git a/yarn.lock b/yarn.lock index f6208cfcd..8044e5c7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6280,6 +6280,11 @@ jiti@^2.4.1: resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.5.1.tgz#bd099c1c2be1c59bbea4e5adcd127363446759d0" integrity sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w== +jose@^5.9.6: + version "5.10.0" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.10.0.tgz#c37346a099d6467c401351a9a0c2161e0f52c4be" + integrity sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -8078,7 +8083,7 @@ passport-local@^1.0.0: dependencies: passport-strategy "1.x.x" -passport-oauth2@^1.5.0, passport-oauth2@^1.7.0: +passport-oauth2@^1.5.0, passport-oauth2@^1.7.0, passport-oauth2@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.8.0.tgz#55725771d160f09bbb191828d5e3d559eee079c8" integrity sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==