diff --git a/web/components/modals/AuthModal/AuthModal.tsx b/web/components/modals/AuthModal/AuthModal.tsx index 41a3ba6d5..cc277e921 100644 --- a/web/components/modals/AuthModal/AuthModal.tsx +++ b/web/components/modals/AuthModal/AuthModal.tsx @@ -20,7 +20,7 @@ export const AuthModal: FC = () => { const chatDisplayName = useRecoilValue(chatDisplayNameAtom); const authenticated = useRecoilValue(chatAuthenticatedAtom); const accessToken = useRecoilValue(accessTokenAtom); - const federationEnabled = false; + const federationEnabled = true; return (
@@ -54,7 +54,11 @@ export const AuthModal: FC = () => { } key="2" > - +
diff --git a/web/components/modals/FediAuthModal/FediAuthModal.module.scss b/web/components/modals/FediAuthModal/FediAuthModal.module.scss new file mode 100644 index 000000000..224dcf0fd --- /dev/null +++ b/web/components/modals/FediAuthModal/FediAuthModal.module.scss @@ -0,0 +1,22 @@ +.codeInputContainer { + .codeInput { + font-size: 1.6rem; + font-family: monospace; + max-width: 220px; + margin: auto; + text-align: center; + letter-spacing: 6px; + border-color: var(--theme-color-palette-9); + color: var(--theme-color-palette-9); + } + + .submitButton { + max-width: 150px; + margin: auto; + margin-top: 10px; + } + + display: flex; + justify-content: center; + flex-direction: column; +} diff --git a/web/components/modals/FediAuthModal/FediAuthModal.stories.tsx b/web/components/modals/FediAuthModal/FediAuthModal.stories.tsx index e642e081c..b6274c500 100644 --- a/web/components/modals/FediAuthModal/FediAuthModal.stories.tsx +++ b/web/components/modals/FediAuthModal/FediAuthModal.stories.tsx @@ -3,12 +3,6 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'; import { FediAuthModal } from './FediAuthModal'; import FediAuthModalMock from '../../../stories/assets/mocks/fediauth-modal.png'; -const Example = () => ( -
- -
-); - export default { title: 'owncast/Modals/FediAuth', component: FediAuthModal, @@ -21,8 +15,18 @@ export default { }, } as ComponentMeta; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const Template: ComponentStory = args => ; +const Template: ComponentStory = args => ; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const Basic = Template.bind({}); +export const NotYetAuthenticated = Template.bind({}); +NotYetAuthenticated.args = { + displayName: 'fake-user', + authenticated: false, + accessToken: '', +}; + +export const Authenticated = Template.bind({}); +Authenticated.args = { + displayName: 'fake-user', + authenticated: true, + accessToken: '', +}; diff --git a/web/components/modals/FediAuthModal/FediAuthModal.tsx b/web/components/modals/FediAuthModal/FediAuthModal.tsx index d055b56a6..4ebf84766 100644 --- a/web/components/modals/FediAuthModal/FediAuthModal.tsx +++ b/web/components/modals/FediAuthModal/FediAuthModal.tsx @@ -1,3 +1,175 @@ -import { FC } from 'react'; +import { Alert, Button, Input, Space, Spin, Collapse } from 'antd'; +import React, { FC, useState } from 'react'; +import styles from './FediAuthModal.module.scss'; +import { validateAccount } from '../../../utils/validators'; -export const FediAuthModal: FC = () =>
Component goes here
; +const { Panel } = Collapse; + +export type FediAuthModalProps = { + authenticated: boolean; + displayName: string; + accessToken: string; +}; + +export const FediAuthModal: FC = ({ + authenticated, + displayName, + accessToken, +}) => { + const [errorMessage, setErrorMessage] = useState(null); + const [loading, setLoading] = useState(false); + const [valid, setValid] = useState(false); + const [account, setAccount] = useState(''); + const [code, setCode] = useState(''); + const [verifyingCode, setVerifyingCode] = useState(true); + + const message = !authenticated ? ( + + Receive a direct message on the Fediverse to link your account to{' '} + {displayName}, or login as a previously linked chat user. + + ) : ( + + You are already authenticated. However, you can add other domains or log in as a + different user. + + ); + + let errorMessageText = errorMessage; + if (errorMessageText) { + if (errorMessageText.includes('url does not support indieauth')) { + errorMessageText = 'The provided URL is either invalid or does not support IndieAuth.'; + } + } + + const validate = (acct: string) => { + setValid(validateAccount(acct)); + }; + + const onInput = (e: React.ChangeEvent) => { + setAccount(e.target.value); + validate(e.target.value); + }; + + const makeRequest = async (url, data) => { + const rawResponse = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + const content = await rawResponse.json(); + if (content.message) { + setErrorMessage(content.message); + setLoading(false); + } + }; + + const submitCodePressed = async () => { + setLoading(true); + const url = `/api/auth/fediverse/verify?accessToken=${accessToken}`; + const data = { code }; + + try { + await makeRequest(url, data); + + // Success. Reload the page. + window.location.href = '/'; + } catch (e) { + console.error(e); + setErrorMessage(e); + } + setLoading(false); + }; + + const submitAccountPressed = async () => { + if (!valid) { + return; + } + + setLoading(true); + setErrorMessage(null); + const url = `/api/auth/fediverse?accessToken=${accessToken}`; + const normalizedAccount = account.replace(/^@+/, ''); + const data = { account: normalizedAccount }; + + try { + await makeRequest(url, data); + setVerifyingCode(true); + } catch (e) { + console.error(e); + setErrorMessage(e); + } + setLoading(false); + }; + + const inputCodeStep = ( +
+ Paste in the code that was sent to your Fediverse account. If you did not receive a code, make + sure you can accept direct messages. +
+ setCode(e.target.value)} + className={styles.codeInput} + placeholder="123456" + maxLength={6} + /> + +
+
+ ); + + const inputAccountStep = ( + <> +
Your Fediverse Account
+ 0 ? 'error' : undefined} + onPressEnter={submitAccountPressed} + enterButton={ + + } + /> + + ); + + return ( + + + {message} + {errorMessageText && ( + + )} + {verifyingCode ? inputCodeStep : inputAccountStep} + + +

xxxxxx

+
+
+
+ Note: This is for authentication purposes only, and no personal + information will be accessed or stored. +
+
+
+ ); +}; diff --git a/web/components/ui/Modal/Modal.module.scss b/web/components/ui/Modal/Modal.module.scss index 6d9375440..8a7213a69 100644 --- a/web/components/ui/Modal/Modal.module.scss +++ b/web/components/ui/Modal/Modal.module.scss @@ -9,4 +9,5 @@ height: 100%; padding: 2vw; background-color: var(--theme-color-components-modal-content-background); + color: var(--theme-color-components-modal-content-text); } diff --git a/web/utils/validators.ts b/web/utils/validators.ts new file mode 100644 index 000000000..1675c72ca --- /dev/null +++ b/web/utils/validators.ts @@ -0,0 +1,7 @@ +// eslint-disable-next-line import/prefer-default-export +export function validateAccount(account) { + const a = account.replace(/^@+/, ''); + const regex = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return regex.test(String(a).toLowerCase()); +}