Require authentication to participate in chat (#4762)

* feat(chat): require authentication to participate in chat

* fix: it's pretty much impossible to bypass the auth requirement, addressing review feedback anyway

* feat(chat): render chat text input as disabled if chat auth is required

* Commit updated API documentation

---------

Co-authored-by: Owncast <owncast@owncast.online>
This commit is contained in:
Gabe Kangas
2026-01-28 11:49:07 -08:00
committed by GitHub
co-authored by Owncast
parent 83c8b2b3d5
commit 93b482871f
29 changed files with 5194 additions and 5143 deletions
+20
View File
@@ -4,9 +4,11 @@ import MessageFilled from '@ant-design/icons/MessageFilled';
import { FC, useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import classnames from 'classnames';
import { useTranslation } from 'next-export-i18n';
import ActionButtons from './ActionButtons';
import { LOCAL_STORAGE_KEYS, getLocalStorage, setLocalStorage } from '../../../utils/localStorage';
import { canPushNotificationsBeSupported } from '../../../utils/browserPushNotifications';
import { Localization } from '../../../types/localization';
import {
clientConfigStateAtom,
@@ -19,6 +21,7 @@ import {
serverStatusState,
isChatAvailableSelector,
visibleChatMessagesSelector,
chatAuthenticatedAtom,
} from '../../stores/ClientConfigStore';
import { ClientConfig } from '../../../interfaces/client-config.model';
@@ -97,6 +100,7 @@ const ExternalModal = ({ externalActionToDisplay, setExternalActionToDisplay })
};
export const Content: FC = () => {
const { t } = useTranslation();
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const chatState = useRecoilValue<ChatState>(chatStateAtom);
@@ -106,6 +110,7 @@ export const Content: FC = () => {
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
const online = useRecoilValue<boolean>(isOnlineSelector);
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
const isUserAuthenticated = useRecoilValue<boolean>(chatAuthenticatedAtom);
const { viewerCount, lastConnectTime, lastDisconnectTime, streamTitle } =
useRecoilValue<ServerStatus>(serverStatusState);
@@ -118,6 +123,7 @@ export const Content: FC = () => {
externalActions,
offlineMessage,
chatDisabled,
chatRequireAuthentication,
federation,
notifications,
} = clientConfig;
@@ -219,6 +225,16 @@ export const Content: FC = () => {
const showChat = isChatAvailable && !chatDisabled && chatState === ChatState.VISIBLE;
// Determine if chat input should be enabled based on authentication requirements.
// Moderators bypass the authentication requirement.
const chatInputEnabled = !!(
isChatAvailable &&
(!chatRequireAuthentication || isUserAuthenticated || currentUser?.isModerator)
);
const chatInputDisabledMessage = chatRequireAuthentication
? t(Localization.Frontend.Chat.authenticateToChat)
: t(Localization.Frontend.chatDisabled);
return (
<div className={styles.main}>
<div className={styles.mainColumn}>
@@ -325,6 +341,8 @@ export const Content: FC = () => {
isModerator={currentUser.isModerator}
chatAvailable={isChatAvailable}
showInput={!!currentUser}
inputEnabled={chatInputEnabled}
inputDisabledPlaceholder={chatInputDisabledMessage}
desktop
/>
)}
@@ -351,6 +369,8 @@ export const Content: FC = () => {
messages={messages}
currentUser={currentUser}
handleClose={() => setShowChatModal(false)}
inputEnabled={chatInputEnabled}
inputDisabledPlaceholder={chatInputDisabledMessage}
/>
)}
{isMobile && isChatAvailable && !chatDisabled && (