11 changed files with 258 additions and 15 deletions
@ -1,6 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -1,6 +1,156 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
||||
interface Props {} |
||||
import { Alert, Button, Input, Space, Spin, Collapse, Typography } from 'antd'; |
||||
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) { |
||||
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