From 69b110e36bc56a7ae4d7697d20d1c035e6909e17 Mon Sep 17 00:00:00 2001 From: Ksinia Date: Tue, 17 Aug 2021 22:02:08 +0200 Subject: [PATCH 01/10] Add telegram auth to frontend --- frontend/app/common/types.ts | 16 +++- frontend/app/components/auth/auth.api.ts | 17 +++- frontend/app/components/auth/auth.hooks.ts | 8 +- .../app/components/auth/auth.messsages.ts | 24 +++++ frontend/app/components/auth/auth.module.css | 5 + frontend/app/components/auth/auth.tsx | 95 +++++++++++++++++-- .../auth/components/assets/telegram.svg | 1 + .../auth/components/oauth.consts.ts | 1 + .../app/components/auth/components/oauth.tsx | 55 ++++++++--- frontend/app/locales/be.json | 6 ++ frontend/app/locales/bg.json | 6 ++ frontend/app/locales/bp.json | 6 ++ frontend/app/locales/de.json | 6 ++ frontend/app/locales/en.json | 6 ++ frontend/app/locales/es.json | 6 ++ frontend/app/locales/fi.json | 6 ++ frontend/app/locales/fr.json | 6 ++ frontend/app/locales/it.json | 8 +- frontend/app/locales/ja.json | 6 ++ frontend/app/locales/ko.json | 6 ++ frontend/app/locales/pl.json | 6 ++ frontend/app/locales/ru.json | 6 ++ frontend/app/locales/tr.json | 6 ++ frontend/app/locales/ua.json | 6 ++ frontend/app/locales/vi.json | 6 ++ frontend/app/locales/zh.json | 6 ++ frontend/app/store/user/actions.ts | 11 ++- frontend/app/store/user/reducers.ts | 17 +++- frontend/app/store/user/types.ts | 9 +- frontend/package.json | 2 + 30 files changed, 338 insertions(+), 27 deletions(-) create mode 100644 frontend/app/components/auth/components/assets/telegram.svg diff --git a/frontend/app/common/types.ts b/frontend/app/common/types.ts index 69097e9e..bb5a9c52 100644 --- a/frontend/app/common/types.ts +++ b/frontend/app/common/types.ts @@ -95,7 +95,16 @@ export interface Tree { info: PostInfo; } -export type OAuthProvider = 'facebook' | 'twitter' | 'google' | 'yandex' | 'github' | 'microsoft' | 'patreon' | 'dev'; +export type OAuthProvider = + | 'facebook' + | 'twitter' + | 'google' + | 'yandex' + | 'github' + | 'microsoft' + | 'patreon' + | 'telegram' + | 'dev'; export type FormProvider = 'email' | 'anonymous'; export type Provider = OAuthProvider | FormProvider; @@ -158,3 +167,8 @@ export interface ApiError { /** in-depth explanation */ error: string; } + +export interface TelegramParams { + bot: string; + token: string; +} diff --git a/frontend/app/components/auth/auth.api.ts b/frontend/app/components/auth/auth.api.ts index 8b0b7a25..b7decf72 100644 --- a/frontend/app/components/auth/auth.api.ts +++ b/frontend/app/components/auth/auth.api.ts @@ -1,9 +1,10 @@ -import type { User } from 'common/types'; +import type { User, TelegramParams } from 'common/types'; import { authFetcher } from 'common/fetcher'; import { siteId } from 'common/settings'; const EMAIL_SIGNIN_ENDPOINT = '/email/login'; +const TELEGRAM_SIGNIN_ENDPOINT = '/telegram/login'; export function anonymousSignin(user: string): Promise { return authFetcher.get('/anonymous/login', { user, aud: siteId }); @@ -23,6 +24,20 @@ export function verifyEmailSignin(token: string): Promise { return authFetcher.get(EMAIL_SIGNIN_ENDPOINT, { token }); } +/** + * First step of two of `telegram` authorization + */ +export function getTelegramSigninParams(): Promise { + return authFetcher.get(TELEGRAM_SIGNIN_ENDPOINT); +} + +/** + * Second step of two of `telegram` authorization + */ +export function verifyTelegramSignin(token: string): Promise { + return authFetcher.get(TELEGRAM_SIGNIN_ENDPOINT, { token }); +} + export function logout(): Promise { return authFetcher.get('/logout'); } diff --git a/frontend/app/components/auth/auth.hooks.ts b/frontend/app/components/auth/auth.hooks.ts index 5384d072..49da72b9 100644 --- a/frontend/app/components/auth/auth.hooks.ts +++ b/frontend/app/components/auth/auth.hooks.ts @@ -33,7 +33,13 @@ export function useDropdown(disableClosing?: boolean) { } function handleClickOutside(evt: MouseEvent) { - if (disableClosing || dropdownElement?.contains(evt.target as HTMLDivElement)) { + if ( + disableClosing || + dropdownElement?.contains(evt.target as HTMLDivElement) || + (evt.target as Element).classList?.contains('telegram-auth') + // telegram button is gone from dropdown render by the time of this check + // without that condition click on telegram button considered as outside click + ) { return; } diff --git a/frontend/app/components/auth/auth.messsages.ts b/frontend/app/components/auth/auth.messsages.ts index de97e803..263a726c 100644 --- a/frontend/app/components/auth/auth.messsages.ts +++ b/frontend/app/components/auth/auth.messsages.ts @@ -57,6 +57,30 @@ export const messages = defineMessages({ id: 'auth.submit', defaultMessage: 'Submit', }, + telegramLink: { + id: 'auth.telegram-link', + defaultMessage: 'by the link', + }, + telegramCheck: { + id: 'auth.telegram-check', + defaultMessage: 'Check', + }, + telegramMessage1: { + id: 'auth.telegram-message-1', + defaultMessage: 'Open the Telegram', + }, + telegramOptionalQR: { + id: 'auth.telegram-optional-qr', + defaultMessage: 'or by scanning the QR code', + }, + telegramMessage2: { + id: 'auth.telegram-message-2', + defaultMessage: 'and click “Start” there.', + }, + telegramMessage3: { + id: 'auth.telegram-message-3', + defaultMessage: 'Afterwards, click “Check” below.', + }, openProfile: { id: 'auth.open-profile', defaultMessage: 'Open My Profile', diff --git a/frontend/app/components/auth/auth.module.css b/frontend/app/components/auth/auth.module.css index 594b4042..6c05d2ce 100644 --- a/frontend/app/components/auth/auth.module.css +++ b/frontend/app/components/auth/auth.module.css @@ -195,3 +195,8 @@ color: var(--error-color); line-height: 1.2; } + +.qr { + display: block; + margin: 0 auto 1.5rem auto; +} diff --git a/frontend/app/components/auth/auth.tsx b/frontend/app/components/auth/auth.tsx index 4eefbb69..f237e325 100644 --- a/frontend/app/components/auth/auth.tsx +++ b/frontend/app/components/auth/auth.tsx @@ -2,9 +2,10 @@ import clsx from 'clsx'; import { h, Fragment } from 'preact'; import { useState } from 'preact/hooks'; import { useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; +import QRCode from 'qrcode.react'; -import { setUser } from 'store/user/actions'; +import { setTelegramParams, setUser } from 'store/user/actions'; import { Input } from 'components/input'; import { CrossIcon } from 'components/icons/cross'; import { TextareaAutosize } from 'components/textarea-autosize'; @@ -16,19 +17,22 @@ import { OAuth } from './components/oauth'; import { messages } from './auth.messsages'; import { useDropdown } from './auth.hooks'; import { getProviders, getTokenInvalidReason } from './auth.utils'; -import { emailSignin, verifyEmailSignin, anonymousSignin } from './auth.api'; +import { emailSignin, verifyEmailSignin, anonymousSignin, verifyTelegramSignin } from './auth.api'; +import { StoreState } from 'store'; import styles from './auth.module.css'; export function Auth() { const intl = useIntl(); const dispatch = useDispatch(); + const telegramParams = useSelector((s: StoreState) => s.telegramParams); const [oauthProviders, formProviders] = getProviders(); // UI State const [isLoading, setLoading] = useState(false); const [view, setView] = useState(formProviders[0]); - const [ref, isDropdownShown, toggleDropdownState] = useDropdown(view === 'token'); + const [isTelegramShown, toggleTelegram] = useState(false); + const [ref, isDropdownShown, toggleDropdownState] = useDropdown(view === 'token' || isTelegramShown); // Errors const [invalidReason, setInvalidReason] = useState(null); @@ -42,6 +46,7 @@ export function Auth() { evt.preventDefault(); setView(formProviders[0]); toggleDropdownState(); + toggleTelegram(false); } function handleProviderChange(evt: Event) { @@ -96,6 +101,23 @@ export function Auth() { setLoading(false); } + async function handleTelegramSubmit(evt: Event) { + evt.preventDefault(); + setLoading(true); + setInvalidReason(null); + if (telegramParams) { + try { + const user = await verifyTelegramSignin(telegramParams.token); + dispath(setUser(user)); + toggleTelegram(false); + dispath(setTelegramParams(null)); + } catch (e) { + setInvalidReason(e.message || e.error); + } + setLoading(false); + } + } + function handleShowEmailStep(evt: Event) { evt.preventDefault(); setView('email'); @@ -122,7 +144,68 @@ export function Auth() { {isDropdownShown && (
- {isTokenView ? ( + {isTelegramShown && telegramParams ? ( + <> +
+
+ +
+ +
+

+ {intl.formatMessage(messages.telegramMessage1)}{' '} + + {intl.formatMessage(messages.telegramLink)} + + {window.screen.width >= 768 && ` ${intl.formatMessage(messages.telegramOptionalQR)}`}{' '} + {intl.formatMessage(messages.telegramMessage2)} +
+ {intl.formatMessage(messages.telegramMessage3)} +

+ {window.screen.width >= 768 && ( + + )} + + {errorMessage &&
{errorMessage}
} + + ) : isTokenView ? ( <>
@@ -171,7 +254,7 @@ export function Auth() {
{intl.formatMessage(messages.oauthSource)}
- + )} {hasOAuthProviders && hasFormProviders && ( diff --git a/frontend/app/components/auth/components/assets/telegram.svg b/frontend/app/components/auth/components/assets/telegram.svg new file mode 100644 index 00000000..080818fa --- /dev/null +++ b/frontend/app/components/auth/components/assets/telegram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/components/auth/components/oauth.consts.ts b/frontend/app/components/auth/components/oauth.consts.ts index ac39dff9..e10e80b8 100644 --- a/frontend/app/components/auth/components/oauth.consts.ts +++ b/frontend/app/components/auth/components/oauth.consts.ts @@ -13,6 +13,7 @@ export const OAUTH_DATA = { dark: require('./assets/github-dark.svg').default as string, }, }, + telegram: require('./assets/telegram.svg').default as string, } as const; export const OAUTH_PROVIDERS = Object.keys(OAUTH_DATA); diff --git a/frontend/app/components/auth/components/oauth.tsx b/frontend/app/components/auth/components/oauth.tsx index d48466a7..1e0709ac 100644 --- a/frontend/app/components/auth/components/oauth.tsx +++ b/frontend/app/components/auth/components/oauth.tsx @@ -1,12 +1,13 @@ import { h, JSX } from 'preact'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import clsx from 'clsx'; import { useIntl } from 'react-intl'; import type { OAuthProvider } from 'common/types'; import { siteId } from 'common/settings'; import { useTheme } from 'hooks/useTheme'; -import { setUser } from 'store/user/actions'; +import { setUser, setTelegramParams } from 'store/user/actions'; +import { StoreState } from 'store'; import { messages } from 'components/auth/auth.messsages'; @@ -14,16 +15,19 @@ import { oauthSignin } from './oauth.api'; import { BASE_URL } from 'common/constants.config'; import { getButtonVariant, getProviderData } from './oauth.utils'; import styles from './oauth.module.css'; +import { getTelegramSigninParams } from '../auth.api'; const location = encodeURIComponent(`${window.location.origin}${window.location.pathname}?selfClose`); type Props = { providers: OAuthProvider[]; + toggleTelegram?: (showTelegram: boolean) => void; }; -export function OAuth({ providers }: Props) { +export function OAuth({ providers, toggleTelegram }: Props) { const intl = useIntl(); const dispatch = useDispatch(); + const telegramParams = useSelector((s: StoreState) => s.telegramParams); const theme = useTheme(); const buttonVariant = getButtonVariant(providers.length); const handleOauthClick: JSX.GenericEventHandler = async (evt) => { @@ -39,6 +43,18 @@ export function OAuth({ providers }: Props) { dispatch(setUser(user)); }; + const handleTelegramClick: JSX.EventHandler> = async (evt) => { + evt.preventDefault(); + if (!telegramParams) { + const params = await getTelegramSigninParams(); + if (params === null) { + return; + } + dispath(setTelegramParams(params)); + } + toggleTelegram && toggleTelegram(true); + }; + return (