11 changed files with 258 additions and 15 deletions
@ -1,6 +0,0 @@ |
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
|
||||||
interface Props {} |
|
||||||
|
|
||||||
export default function AuthModal(props: Props) { |
|
||||||
return <div>Component goes here</div>; |
|
||||||
} |
|
@ -0,0 +1,11 @@ |
|||||||
|
.tabContent { |
||||||
|
flex-direction: row; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
|
||||||
|
.icon { |
||||||
|
height: 15px; |
||||||
|
padding-right: 5px; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,64 @@ |
|||||||
|
import { Tabs } from 'antd'; |
||||||
|
import { useRecoilValue } from 'recoil'; |
||||||
|
import IndieAuthModal from '../IndieAuthModal'; |
||||||
|
import FediAuthModal from '../FediAuthModal'; |
||||||
|
|
||||||
|
import FediverseIcon from '../../../assets/images/fediverse-black.png'; |
||||||
|
import IndieAuthIcon from '../../../assets/images/indieauth.png'; |
||||||
|
|
||||||
|
import s from './AuthModal.module.scss'; |
||||||
|
import { |
||||||
|
chatDisplayNameAtom, |
||||||
|
chatAuthenticatedAtom, |
||||||
|
accessTokenAtom, |
||||||
|
} from '../../stores/ClientConfigStore'; |
||||||
|
|
||||||
|
const { TabPane } = Tabs; |
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */ |
||||||
|
interface Props {} |
||||||
|
|
||||||
|
export default function AuthModal(props: Props) { |
||||||
|
const chatDisplayName = useRecoilValue<string>(chatDisplayNameAtom); |
||||||
|
const authenticated = useRecoilValue<boolean>(chatAuthenticatedAtom); |
||||||
|
const accessToken = useRecoilValue<string>(accessTokenAtom); |
||||||
|
const federationEnabled = false; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<Tabs |
||||||
|
defaultActiveKey="1" |
||||||
|
type="card" |
||||||
|
size="small" |
||||||
|
renderTabBar={federationEnabled ? null : () => null} |
||||||
|
> |
||||||
|
<TabPane |
||||||
|
tab={ |
||||||
|
<span className={s.tabContent}> |
||||||
|
<img className={s.icon} src={IndieAuthIcon.src} alt="IndieAuth" /> |
||||||
|
IndieAuth |
||||||
|
</span> |
||||||
|
} |
||||||
|
key="1" |
||||||
|
> |
||||||
|
<IndieAuthModal |
||||||
|
authenticated={authenticated} |
||||||
|
displayName={chatDisplayName} |
||||||
|
accessToken={accessToken} |
||||||
|
/> |
||||||
|
</TabPane> |
||||||
|
<TabPane |
||||||
|
tab={ |
||||||
|
<span className={s.tabContent}> |
||||||
|
<img className={s.icon} src={FediverseIcon.src} alt="Fediverse auth" /> |
||||||
|
FediAuth |
||||||
|
</span> |
||||||
|
} |
||||||
|
key="2" |
||||||
|
> |
||||||
|
<FediAuthModal /> |
||||||
|
</TabPane> |
||||||
|
</Tabs> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -1,6 +1,156 @@ |
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
import { Alert, Button, Input, Space, Spin, Collapse, Typography } from 'antd'; |
||||||
interface Props {} |
import React, { useState } from 'react'; |
||||||
|
import isValidURL from '../../utils/urls'; |
||||||
|
|
||||||
|
const { Panel } = Collapse; |
||||||
|
const { Link } = Typography; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
authenticated: boolean; |
||||||
|
displayName: string; |
||||||
|
accessToken: string; |
||||||
|
} |
||||||
|
|
||||||
export default function IndieAuthModal(props: Props) { |
export default function IndieAuthModal(props: Props) { |
||||||
return <div>Component goes here</div>; |
const { authenticated, displayName: username, accessToken } = props; |
||||||
|
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null); |
||||||
|
const [loading, setLoading] = useState(false); |
||||||
|
const [valid, setValid] = useState(false); |
||||||
|
const [host, setHost] = useState(''); |
||||||
|
|
||||||
|
const message = !authenticated ? ( |
||||||
|
<span> |
||||||
|
Use your own domain to authenticate <span>{username}</span> or login as a previously{' '} |
||||||
|
authenticated chat user using IndieAuth. |
||||||
|
</span> |
||||||
|
) : ( |
||||||
|
<span> |
||||||
|
<b>You are already authenticated</b>. However, you can add other domains or log in as a |
||||||
|
different user. |
||||||
|
</span> |
||||||
|
); |
||||||
|
|
||||||
|
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 = (url: string) => { |
||||||
|
if (!isValidURL(url)) { |
||||||
|
setValid(false); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!url.includes('.')) { |
||||||
|
setValid(false); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
setValid(true); |
||||||
|
}; |
||||||
|
|
||||||
|
const onInput = (e: React.ChangeEvent<HTMLInputElement>) => { |
||||||
|
// Don't allow people to type custom ports or protocols.
|
||||||
|
const char = (e.nativeEvent as any).data; |
||||||
|
if (char === ':') { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
setHost(e.target.value); |
||||||
|
const h = `https://${e.target.value}`; |
||||||
|
validate(h); |
||||||
|
}; |
||||||
|
|
||||||
|
const submitButtonPressed = async () => { |
||||||
|
if (!valid) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
setLoading(true); |
||||||
|
|
||||||
|
try { |
||||||
|
const url = `/api/auth/indieauth?accessToken=${accessToken}`; |
||||||
|
const h = `https://${host}`; |
||||||
|
const data = { authHost: h }; |
||||||
|
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); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (!content.redirect) { |
||||||
|
setErrorMessage('Auth provider did not return a redirect URL.'); |
||||||
|
setLoading(false); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (content.redirect) { |
||||||
|
const { redirect } = content; |
||||||
|
window.location = redirect; |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
setErrorMessage(e.message); |
||||||
|
} |
||||||
|
|
||||||
|
setLoading(false); |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Spin spinning={loading}> |
||||||
|
<Space direction="vertical"> |
||||||
|
{message} |
||||||
|
{errorMessageText && ( |
||||||
|
<Alert message="Error" description={errorMessageText} type="error" showIcon /> |
||||||
|
)} |
||||||
|
<div>Your domain</div> |
||||||
|
<Input.Search |
||||||
|
addonBefore="https://" |
||||||
|
onInput={onInput} |
||||||
|
type="url" |
||||||
|
value={host} |
||||||
|
placeholder="yoursite.com" |
||||||
|
status={!valid && host.length > 0 ? 'error' : undefined} |
||||||
|
onPressEnter={submitButtonPressed} |
||||||
|
enterButton={ |
||||||
|
<Button onClick={submitButtonPressed} disabled={!valid}> |
||||||
|
Authenticate with your domain |
||||||
|
</Button> |
||||||
|
} |
||||||
|
/> |
||||||
|
|
||||||
|
<Collapse ghost> |
||||||
|
<Panel key="header" header="Learn more about using IndieAuth to authenticate with chat."> |
||||||
|
<p> |
||||||
|
IndieAuth allows for a completely independent and decentralized way of identifying |
||||||
|
yourself using your own domain. |
||||||
|
</p> |
||||||
|
|
||||||
|
<p> |
||||||
|
If you run an Owncast instance, you can use that domain here. Otherwise,{' '} |
||||||
|
<Link href="https://indieauth.net/#providers"> |
||||||
|
learn more about how you can support IndieAuth |
||||||
|
</Link> |
||||||
|
. |
||||||
|
</p> |
||||||
|
</Panel> |
||||||
|
</Collapse> |
||||||
|
<div> |
||||||
|
<strong>Note</strong>: This is for authentication purposes only, and no personal |
||||||
|
information will be accessed or stored. |
||||||
|
</div> |
||||||
|
</Space> |
||||||
|
</Spin> |
||||||
|
); |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue