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:
38
webroot/img/authenticated.svg
Normal file
38
webroot/img/authenticated.svg
Normal 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
BIN
webroot/img/indieauth.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
2
webroot/img/user-settings.svg
Normal file
2
webroot/img/user-settings.svg
Normal 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 |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
3
webroot/js/chat/indieauth.js
Normal file
3
webroot/js/chat/indieauth.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { URL_CHAT_INDIEAUTH_BEGIN } from '../utils/constants.js';
|
||||
|
||||
export async function beginIndieAuthFlow() {}
|
||||
192
webroot/js/components/auth-indieauth.js
Normal file
192
webroot/js/components/auth-indieauth.js
Normal 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;
|
||||
}
|
||||
44
webroot/js/components/chat-settings-modal.js
Normal file
44
webroot/js/components/chat-settings-modal.js
Normal 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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user