IndieAuth support (#1811)

* Able to authenticate user against IndieAuth. For #1273

* WIP server indieauth endpoint. For https://github.com/owncast/owncast/issues/1272

* Add migration to remove access tokens from user

* Add authenticated bool to user for display purposes

* Add indieauth modal and auth flair to display names. For #1273

* Validate URLs and display errors

* Renames, cleanups

* Handle relative auth endpoint paths. Add error handling for missing redirects.

* Disallow using display names in use by registered users. Closes #1810

* Verify code verifier via code challenge on callback

* Use relative path to authorization_endpoint

* Post-rebase fixes

* Use a timestamp instead of a bool for authenticated

* Propertly handle and display error in modal

* Use auth'ed timestamp to derive authenticated flag to display in chat

* don't redirect unless a URL is present

avoids redirecting to `undefined` if there was an error

* improve error message if owncast server URL isn't set

* fix IndieAuth PKCE implementation

use SHA256 instead of SHA1, generates a longer code verifier (must be 43-128 chars long), fixes URL-safe SHA256 encoding

* return real profile data for IndieAuth response

* check the code verifier in the IndieAuth server

* Linting

* Add new chat settings modal anad split up indieauth ui

* Remove logging error

* Update the IndieAuth modal UI. For #1273

* Add IndieAuth repsonse error checking

* Disable IndieAuth client if server URL is not set.

* Add explicit error messages for specific error types

* Fix bad logic

* Return OAuth-keyed error responses for indieauth server

* Display IndieAuth error in plain text with link to return to main page

* Remove redundant check

* Add additional detail to error

* Hide IndieAuth details behind disclosure details

* Break out migration into two steps because some people have been runing dev in production

* Add auth option to user dropdown

Co-authored-by: Aaron Parecki <aaron@parecki.com>
This commit is contained in:
Gabe Kangas
2022-04-21 14:55:26 -07:00
committed by GitHub
parent b86537fa91
commit b835de2dc4
47 changed files with 1844 additions and 274 deletions

View File

@@ -0,0 +1,38 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" version="1.1" width="512" height="512" x="0" y="0" viewBox="0 0 32 32" style="enable-background:new 0 0 512 512" xml:space="preserve" class=""><g><title xmlns="http://www.w3.org/2000/svg"/>
<g xmlns="http://www.w3.org/2000/svg">
<g id="check_x5F_alt">
<path style="" d="M16,0C7.164,0,0,7.164,0,16s7.164,16,16,16s16-7.164,16-16S24.836,0,16,0z M13.52,23.383 L6.158,16.02l2.828-2.828l4.533,4.535l9.617-9.617l2.828,2.828L13.52,23.383z" fill="#ffffff" data-original="#030104" class=""/>
</g>
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
<g xmlns="http://www.w3.org/2000/svg">
</g>
</g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
webroot/img/indieauth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" version="1.1" width="512" height="512" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><title xmlns="http://www.w3.org/2000/svg"/><circle xmlns="http://www.w3.org/2000/svg" cx="9" cy="5" r="5" fill="#ffffff" data-original="#000000"/><path xmlns="http://www.w3.org/2000/svg" d="m11.534 20.8c-.521-.902-.417-2.013.203-2.8-.62-.787-.724-1.897-.203-2.8l.809-1.4c.445-.771 1.275-1.25 2.166-1.25.122 0 .242.009.361.026.033-.082.075-.159.116-.237-.54-.213-1.123-.339-1.736-.339h-8.5c-2.619 0-4.75 2.131-4.75 4.75v3.5c0 .414.336.75.75.75h10.899z" fill="#ffffff" data-original="#000000"/><path xmlns="http://www.w3.org/2000/svg" d="m21.703 18.469c.02-.155.047-.309.047-.469 0-.161-.028-.314-.047-.469l.901-.682c.201-.152.257-.43.131-.649l-.809-1.4c-.126-.218-.395-.309-.627-.211l-1.037.437c-.253-.193-.522-.363-.819-.487l-.138-1.101c-.032-.25-.244-.438-.496-.438h-1.617c-.252 0-.465.188-.496.438l-.138 1.101c-.297.124-.567.295-.819.487l-1.037-.437c-.232-.098-.501-.008-.627.211l-.809 1.4c-.126.218-.07.496.131.649l.901.682c-.02.155-.047.309-.047.469 0 .161.028.314.047.469l-.901.682c-.201.152-.257.43-.131.649l.809 1.401c.126.218.395.309.627.211l1.037-.438c.253.193.522.363.819.487l.138 1.101c.031.25.243.438.495.438h1.617c.252 0 .465-.188.496-.438l.138-1.101c.297-.124.567-.295.819-.487l1.037.437c.232.098.501.008.627-.211l.809-1.401c.126-.218.07-.496-.131-.649zm-3.703 1.531c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2z" fill="#ffffff" data-original="#000000"/></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,42 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Owncast</title>
<base target="_blank" />
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
/>
<head>
<title>Owncast</title>
<base target="_blank" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<link
rel="apple-touch-icon"
sizes="57x57"
href="/img/favicon/apple-icon-57x57.png"
/>
<link
rel="apple-touch-icon"
sizes="60x60"
href="/img/favicon/apple-icon-60x60.png"
/>
<link
rel="apple-touch-icon"
sizes="72x72"
href="/img/favicon/apple-icon-72x72.png"
/>
<link
rel="apple-touch-icon"
sizes="76x76"
href="/img/favicon/apple-icon-76x76.png"
/>
<link
rel="apple-touch-icon"
sizes="114x114"
href="/img/favicon/apple-icon-114x114.png"
/>
<link
rel="apple-touch-icon"
sizes="120x120"
href="/img/favicon/apple-icon-120x120.png"
/>
<link
rel="apple-touch-icon"
sizes="144x144"
href="/img/favicon/apple-icon-144x144.png"
/>
<link
rel="apple-touch-icon"
sizes="152x152"
href="/img/favicon/apple-icon-152x152.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/img/favicon/apple-icon-180x180.png"
/>
<link
rel="icon"
type="image/png"
sizes="192x192"
href="/img/favicon/android-icon-192x192.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/img/favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="96x96"
href="/img/favicon/favicon-96x96.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/img/favicon/favicon-16x16.png"
/>
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" sizes="57x57" href="/img/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/img/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/img/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/img/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/img/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/img/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/img/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/img/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/img/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/img/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<link rel="authorization_endpoint" href="/api/auth/provider/indieauth" />
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/img/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<meta name="msapplication-TileColor" content="#ffffff" />
<meta
name="msapplication-TileImage"
content="/img/favicon/ms-icon-144x144.png"
/>
<meta name="theme-color" content="#ffffff" />
<link href="/js/web_modules/tailwindcss/dist/tailwind.min.css" rel="stylesheet" />
<link
href="/js/web_modules/tailwindcss/dist/tailwind.min.css"
rel="stylesheet"
/>
<link href="/js/web_modules/videojs/video-js.min.css" rel="stylesheet" />
<link href="/js/web_modules/@videojs/themes/fantasy/index.css" rel="stylesheet" />
<link href="/js/web_modules/videojs/video-js.min.css" rel="stylesheet" />
<link
href="/js/web_modules/@videojs/themes/fantasy/index.css"
rel="stylesheet"
/>
<link href="/styles/video.css" rel="stylesheet" />
<link href="/styles/chat.css" rel="stylesheet" />
<link href="/styles/user-content.css" rel="stylesheet" />
<link href="/styles/app.css" rel="stylesheet" />
<link href="/styles/video.css" rel="stylesheet" />
<link href="/styles/chat.css" rel="stylesheet" />
<link href="/styles/user-content.css" rel="stylesheet" />
<link href="/styles/app.css" rel="stylesheet" />
<!-- The following script tags are not required for the app to run,
<!-- The following script tags are not required for the app to run,
however they will make it load a lot faster (fewer round trips) when HTTP/2 is used.
If you wish to re-generate this list, run the following shell command
@@ -48,105 +117,145 @@
<script type="preload" src="/js/components/platform-logos-list.js"></script>
<script type="preload" src="/js/components/chat/chat-input.js"></script>
<script type="preload" src="/js/components/chat/message.js"></script>
<script type="preload" src="/js/components/chat/content-editable.js"></script>
<script
type="preload"
src="/js/components/chat/content-editable.js"
></script>
<script type="preload" src="/js/components/chat/chat.js"></script>
<script type="preload" src="/js/components/chat/chat-message-view.js"></script>
<script
type="preload"
src="/js/components/chat/chat-message-view.js"
></script>
<script type="preload" src="/js/components/chat/username.js"></script>
<script type="preload" src="/js/components/external-action-modal.js"></script>
<script
type="preload"
src="/js/components/external-action-modal.js"
></script>
<script type="preload" src="/js/components/player.js"></script>
<script type="preload" src="/js/components/video-poster.js"></script>
<script type="preload" src="/js/app.js"></script>
<script type="preload" src="/js/web_modules/preact.js"></script>
<script type="preload" src="/js/web_modules/micromodal/dist/micromodal.min.js"></script>
<script type="preload" src="/js/web_modules/common/_commonjsHelpers-8c19dec8.js"></script>
<script type="preload" src="/js/web_modules/markjs/dist/mark.es6.min.js"></script>
<script type="preload" src="/js/web_modules/@joeattardi/emoji-button.js"></script>
<script
type="preload"
src="/js/web_modules/micromodal/dist/micromodal.min.js"
></script>
<script
type="preload"
src="/js/web_modules/common/_commonjsHelpers-8c19dec8.js"
></script>
<script
type="preload"
src="/js/web_modules/markjs/dist/mark.es6.min.js"
></script>
<script
type="preload"
src="/js/web_modules/@joeattardi/emoji-button.js"
></script>
<script type="preload" src="/js/web_modules/htm.js"></script>
<script type="preload" src="/js/web_modules/videojs/dist/video.min.js"></script>
<script
type="preload"
src="/js/web_modules/videojs/dist/video.min.js"
></script>
<script type="preload" src="/js/chat/register.js"></script>
<script type="preload" src="/js/utils/helpers.js"></script>
<script type="preload" src="/js/utils/user-colors.js"></script>
<script type="preload" src="/js/utils/constants.js"></script>
<script type="preload" src="/js/utils/chat.js"></script>
<script type="preload" src="/js/utils/websocket.js"></script>
</head>
</head>
<body id="app-body" class="scrollbar-hidden bg-gray-300 text-gray-800">
<div id="app">
<div id="loading-logo-container">
<img id="loading-logo" src="/logo">
<body id="app-body" class="scrollbar-hidden bg-gray-300 text-gray-800">
<div id="app">
<div id="loading-logo-container">
<img id="loading-logo" src="/logo" />
</div>
</div>
</div>
<script type="module">
import { h, render } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
<script type="module">
import { h, render } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import App from '/js/app.js';
render(html`<${App} />`, document.getElementById("app"), document.getElementById("loading-logo-container"));
</script>
import App from '/js/app.js';
render(
html`<${App} />`,
document.getElementById('app'),
document.getElementById('loading-logo-container')
);
</script>
<noscript>
<style>
.noscript {
text-align: center;
padding: 30px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-size: large;
}
<noscript>
<style>
.noscript {
text-align: center;
padding: 30px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-size: large;
}
.noscript a {
display: inline;
color: blue;
text-decoration: underline;
}
.noscript a {
display: inline;
color: blue;
text-decoration: underline;
}
#app {
display: none;
}
#app {
display: none;
}
.logo {
height: 200px;
margin: 30px;
}
h2 {
margin-top: 25px;
margin-bottom: 5px;
font-weight: bold;
}
</style>
<div class="noscript">
<img class="logo" src="/logo" />
<br />
<p>
This website is powered by <a href="https://owncast.online" rel="noopener noreferrer" target="_blank">Owncast</a>.
</p>
<p>
Owncast uses JavaScript for playing the HTTP Live Streaming (HLS) video, and its chat client. But your web browser does not seem to support JavaScript, or you have it disabled.
</p>
<p>
For the best experience, you should use a different browser with JavaScript support. If you have disabled JavaScript in your browser, you can re-enable it.
</p>
<h2>
How can I watch this stream without JavaScript?
</h2>
<p>
You can open the URL of this website in your media player (such as <a href="https://mpv.io" rel="noopener noreferrer" target="_blank">mpv</a> or <a href="https://www.videolan.org/vlc/" rel="noopener noreferrer" target="_blank">VLC</a>) to watch the stream.
</p>
<h2>
How can I chat with the others without JavaScript?
</h2>
<p>
Currently, there is no option to use the chat without JavaScript.
</p>
</div>
</noscript>
</body>
.logo {
height: 200px;
margin: 30px;
}
h2 {
margin-top: 25px;
margin-bottom: 5px;
font-weight: bold;
}
</style>
<div class="noscript">
<img class="logo" src="/logo" />
<br />
<p>
This website is powered by
<a
href="https://owncast.online"
rel="noopener noreferrer"
target="_blank"
>Owncast</a
>.
</p>
<p>
Owncast uses JavaScript for playing the HTTP Live Streaming (HLS)
video, and its chat client. But your web browser does not seem to
support JavaScript, or you have it disabled.
</p>
<p>
For the best experience, you should use a different browser with
JavaScript support. If you have disabled JavaScript in your browser,
you can re-enable it.
</p>
<h2>How can I watch this stream without JavaScript?</h2>
<p>
You can open the URL of this website in your media player (such as
<a href="https://mpv.io" rel="noopener noreferrer" target="_blank"
>mpv</a
>
or
<a
href="https://www.videolan.org/vlc/"
rel="noopener noreferrer"
target="_blank"
>VLC</a
>) to watch the stream.
</p>
<h2>How can I chat with the others without JavaScript?</h2>
<p>Currently, there is no option to use the chat without JavaScript.</p>
</div>
</noscript>
</body>
</html>

View File

@@ -27,6 +27,8 @@ import FediverseFollowModal, {
import { NotifyButton, NotifyModal } from './components/notification.js';
import { isPushNotificationSupported } from './notification/registerWeb.js';
import ChatSettingsModal from './components/chat-settings-modal.js';
import {
addNewlines,
checkUrlPathForDisplay,
@@ -110,6 +112,9 @@ export default class App extends Component {
externalActionModalData: null,
fediverseModalData: null,
// authentication options
indieAuthEnabled: false,
// routing & tabbing
section: '',
sectionId: '',
@@ -144,6 +149,8 @@ export default class App extends Component {
this.closeFediverseFollowModal = this.closeFediverseFollowModal.bind(this);
this.displayNotificationModal = this.displayNotificationModal.bind(this);
this.closeNotificationModal = this.closeNotificationModal.bind(this);
this.showAuthModal = this.showAuthModal.bind(this);
this.closeAuthModal = this.closeAuthModal.bind(this);
// player events
this.handlePlayerReady = this.handlePlayerReady.bind(this);
@@ -268,8 +275,14 @@ export default class App extends Component {
}
setConfigData(data = {}) {
const { name, summary, chatDisabled, socketHostOverride, notifications } =
data;
const {
name,
summary,
chatDisabled,
socketHostOverride,
notifications,
authentication,
} = data;
window.document.title = name;
this.socketHostOverride = socketHostOverride;
@@ -281,10 +294,12 @@ export default class App extends Component {
}
this.hasConfiguredChat = true;
const { indieAuthEnabled } = authentication;
this.setState({
canChat: !chatDisabled,
notifications,
indieAuthEnabled,
configData: {
...data,
summary: summary && addNewlines(summary),
@@ -618,6 +633,17 @@ export default class App extends Component {
}
}
showAuthModal() {
const data = {
title: 'Chat',
};
this.setState({ authModalData: data });
}
closeAuthModal() {
this.setState({ authModalData: null });
}
handleWebsocketMessage(e) {
if (e.type === SOCKET_MESSAGE_TYPES.ERROR_USER_DISABLED) {
// User has been actively disabled on the backend. Turn off chat for them.
@@ -637,10 +663,10 @@ export default class App extends Component {
// When connected the user will return an event letting us know what our
// user details are so we can display them properly.
const { user } = e;
const { displayName } = user;
const { displayName, authenticated } = user;
this.setState({
username: displayName,
authenticated,
isModerator: checkIsModerator(e),
});
}
@@ -724,17 +750,20 @@ export default class App extends Component {
streamTitle,
touchKeyboardActive,
username,
authenticated,
viewerCount,
websocket,
windowHeight,
windowWidth,
fediverseModalData,
authModalData,
externalActionModalData,
notificationModalData,
notifications,
lastDisconnectTime,
section,
sectionId,
indieAuthEnabled,
} = state;
const {
@@ -864,11 +893,32 @@ export default class App extends Component {
/>`}
/>`;
const authModal =
authModalData &&
html`
<${ExternalActionModal}
onClose=${this.closeAuthModal}
action=${authModalData}
useIframe=${false}
customContent=${html`<${ChatSettingsModal}
name=${name}
logo=${logo}
onUsernameChange=${this.handleUsernameChange}
username=${username}
accessToken=${this.state.accessToken}
authenticated=${authenticated}
onClose=${this.closeAuthModal}
indieAuthEnabled=${indieAuthEnabled}
/>`}
/>
`;
const chat = this.state.websocket
? html`
<${Chat}
websocket=${websocket}
username=${username}
authenticated=${authenticated}
chatInputEnabled=${chatInputEnabled && !chatDisabled}
instanceTitle=${name}
accessToken=${accessToken}
@@ -911,6 +961,8 @@ export default class App extends Component {
});
}
const authIcon = '/img/user-settings.svg';
return html`
<div
id="app-container"
@@ -942,9 +994,11 @@ export default class App extends Component {
>
</h1>
<${ChatMenu} username=${username} isModerator=${isModerator} onUsernameChange=${
this.handleUsernameChange
} onFocus=${this.handleFormFocus} onBlur=${
<${ChatMenu} username=${username} isModerator=${isModerator} showAuthModal=${
indieAuthEnabled && this.showAuthModal
} onUsernameChange=${this.handleUsernameChange} onFocus=${
this.handleFormFocus
} onBlur=${
this.handleFormBlur
} chatDisabled=${chatDisabled} noVideoContent=${noVideoContent} handleChatPanelToggle=${
this.handleChatPanelToggle
@@ -1027,7 +1081,7 @@ export default class App extends Component {
</footer>
${chat} ${externalActionModal} ${fediverseFollowModal}
${notificationModal}
${notificationModal} ${authModal}
</div>
`;
}

View File

@@ -0,0 +1,3 @@
import { URL_CHAT_INDIEAUTH_BEGIN } from '../utils/constants.js';
export async function beginIndieAuthFlow() {}

View File

@@ -0,0 +1,192 @@
import { h, Component } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
export default class IndieAuthForm extends Component {
constructor(props) {
super(props);
this.submitButtonPressed = this.submitButtonPressed.bind(this);
this.state = {
errorMessage: null,
loading: false,
valid: false,
};
}
async submitButtonPressed() {
const { accessToken, authenticated } = this.props;
const { host, valid } = this.state;
if (!valid) {
return;
}
const url = `/api/auth/indieauth?accessToken=${accessToken}`;
const data = { authHost: host };
this.setState({ loading: true });
try {
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) {
this.setState({ errorMessage: content.message, loading: false });
return;
} else if (!content.redirect) {
this.setState({
errorMessage: 'Auth provider did not return a redirect URL.',
loading: false,
});
return;
}
if (content.redirect) {
const redirect = content.redirect;
window.location = redirect;
}
} catch (e) {
console.error(e);
this.setState({ errorMessage: e, loading: false });
}
}
onInput = (e) => {
const { value } = e.target;
let valid = validateURL(value);
this.setState({ host: value, valid });
};
render() {
const { errorMessage, loading, host, valid } = this.state;
const { authenticated } = this.props;
const buttonState = valid ? '' : 'cursor-not-allowed opacity-50';
const loaderStyle = loading ? 'flex' : 'none';
const message = !authenticated
? `While you can chat completely anonymously you can also add
authentication so you can rejoin with the same chat persona from any
device or browser.`
: html`<span
><b>You are already authenticated</b>. However, you can add other
external sites 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 error = errorMessage
? html` <div
class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
role="alert"
>
<div class="font-bold mb-2">There was an error.</div>
<div class="block mt-2">
<div>${errorMessageText}</div>
</div>
</div>`
: null;
return html` <div>
<p class="text-gray-700">${message}</p>
<p>${error}</p>
<div class="mb34">
<label
class="block text-gray-700 text-sm font-semibold mt-6"
for="username"
>
Your domain
</label>
<input
onInput=${this.onInput}
type="url"
value=${host}
class="border bg-white rounded w-full py-2 px-3 mb-2 mt-2 text-indigo-700 leading-tight focus:outline-none focus:shadow-outline"
id="username"
type="text"
placeholder="https://yoursite.com"
/>
<button
class="bg-indigo-500 hover:bg-indigo-600 text-white font-bold py-2 mt-6 px-4 rounded focus:outline-none focus:shadow-outline ${buttonState}"
type="button"
onClick=${this.submitButtonPressed}
>
Authenticate with your domain
</button>
</div>
<p class="mt-4">
<details>
<summary class="cursor-pointer">
Learn more about <span class="text-blue-500">IndieAuth</span>
</summary>
<div class="inline">
<p class="mt-4">
IndieAuth allows for a completely independent and decentralized
way of identifying yourself using your own domain.
</p>
<p class="mt-4">
If you run an Owncast instance, you can use that domain here.
Otherwise, ${' '}
<a class="underline" href="https://indieauth.net/#providers"
>learn more about how you can support IndieAuth</a
>.
</p>
</div>
</details>
</p>
<p class="mt-4">
<b>Note:</b> This is for authentication purposes only, and no personal
information will be accessed or stored.
</p>
<div
id="follow-loading-spinner-container"
style="display: ${loaderStyle}"
>
<img id="follow-loading-spinner" src="/img/loading.gif" />
<p class="text-gray-700 text-lg">Authenticating.</p>
<p class="text-gray-600 text-lg">Please wait...</p>
</div>
</div>`;
}
}
function validateURL(url) {
if (!url) {
return false;
}
try {
const u = new URL(url);
if (!u) {
return false;
}
if (u.protocol !== 'https:') {
return false;
}
} catch (e) {
return false;
}
return true;
}

View File

@@ -0,0 +1,44 @@
import { h, Component } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
import TabBar from './tab-bar.js';
import IndieAuthForm from './auth-indieauth.js';
const html = htm.bind(h);
export default class ChatSettingsModal extends Component {
render() {
const {
accessToken,
authenticated,
username,
onUsernameChange,
indieAuthEnabled,
} = this.props;
const TAB_CONTENT = [
{
label: html`<span style=${{ display: 'flex', alignItems: 'center' }}
><img
style=${{
display: 'inline',
height: '0.8em',
marginRight: '5px',
}}
src="/img/indieauth.png"
/>
IndieAuth</span
>`,
content: html`<${IndieAuthForm}}
accessToken=${accessToken}
authenticated=${authenticated}
/>`,
},
];
return html`
<div class="bg-gray-100 bg-center bg-no-repeat p-5">
<${TabBar} tabs=${TAB_CONTENT} ariaLabel="Chat settings" />
</div>
`;
}
}

View File

@@ -20,6 +20,7 @@ export const ChatMenu = (props) => {
noVideoContent,
handleChatPanelToggle,
onUsernameChange,
showAuthModal,
onFocus,
onBlur,
} = props;
@@ -34,6 +35,15 @@ export const ChatMenu = (props) => {
if (chatMenuOpen) setView('main');
}, [chatMenuOpen]);
const authMenuItem =
showAuthModal &&
html`<li>
<button type="button" id="chat-auth" onClick=${showAuthModal}>
Authenticate
<span><${ChatIcon} /></span>
</button>
</li>`;
return html`
<${Context.Provider} value=${props}>
<div class="chat-menu p-2 relative shadow-lg" ref=${chatMenuRef}>
@@ -55,7 +65,7 @@ export const ChatMenu = (props) => {
>
${username}
</span>
<${CaretDownIcon} className="w-8 h-8"/>
<${CaretDownIcon} className="w-8 h-8"/>
</button>
${
chatMenuOpen &&
@@ -74,6 +84,7 @@ export const ChatMenu = (props) => {
onBlur=${onBlur}
/>
</li>
${authMenuItem}
<li>
<button
type="button"

View File

@@ -49,7 +49,8 @@ export default class ChatMessageView extends Component {
if (!user) {
return null;
}
const { displayName, displayColor, createdAt, isBot } = user;
const { displayName, displayColor, createdAt, isBot, authenticated } = user;
const isAuthorModerator = checkIsModerator(message);
const isMessageModeratable =
@@ -78,7 +79,7 @@ export default class ChatMessageView extends Component {
isMessageModeratable ? 'moderatable' : ''
}`;
const messageAuthorFlair = isAuthorModerator
const isModeratorFlair = isAuthorModerator
? html`<img
class="flair"
title="Moderator"
@@ -95,6 +96,14 @@ export default class ChatMessageView extends Component {
/>`
: null;
const authorAuthenticatedFlair = authenticated
? html`<img
class="flair"
title="Authenticated"
src="/img/authenticated.svg"
/>`
: null;
return html`
<div
style=${backgroundStyle}
@@ -107,7 +116,8 @@ export default class ChatMessageView extends Component {
class="message-author font-bold"
title=${userMetadata}
>
${isBotFlair} ${messageAuthorFlair} ${displayName}
${isBotFlair} ${authorAuthenticatedFlair} ${isModeratorFlair}
${displayName}
</div>
${isMessageModeratable &&
html`<${ModeratorActions}

View File

@@ -102,6 +102,10 @@ export default class UsernameForm extends Component {
},
};
const moderatorFlag = html`
<img src="/img/moderator-nobackground.svg" class="moderator-flag" />
`;
return html`
<div id="user-info">
<button

View File

@@ -21,6 +21,7 @@ export const URL_PLAYBACK_METRICS = `/api/metrics/playback`;
export const URL_REGISTER_NOTIFICATION = `/api/notifications/register`;
export const URL_REGISTER_EMAIL_NOTIFICATION = `/api/notifications/register/email`;
export const URL_CHAT_INDIEAUTH_BEGIN = `/api/auth/indieauth`;
export const TIMER_STATUS_UPDATE = 5000; // ms
export const TIMER_DISABLE_CHAT_AFTER_OFFLINE = 5 * 60 * 1000; // 5 mins