feat: Enable visitor user with password option

This commit is contained in:
Peifan Li
2026-01-03 23:28:30 -05:00
parent a9f78647e4
commit ccd2729f71
15 changed files with 100 additions and 48 deletions

View File

@@ -1,3 +1,5 @@
import DeleteIcon from '@mui/icons-material/Delete';
import FingerprintIcon from '@mui/icons-material/Fingerprint';
import { Box, Button, FormControlLabel, Switch, TextField, Typography } from '@mui/material';
import { startRegistration } from '@simplewebauthn/browser';
import { useMutation, useQuery } from '@tanstack/react-query';
@@ -148,23 +150,23 @@ const SecuritySettings: React.FC<SecuritySettingsProps> = ({ settings, onChange
return (
<Box>
<FormControlLabel
control={
<Switch
checked={settings.loginEnabled}
onChange={(e) => onChange('loginEnabled', e.target.checked)}
/>
}
label={t('enableLogin')}
<FormControlLabel
control={
<Switch
checked={settings.loginEnabled}
onChange={(e) => onChange('loginEnabled', e.target.checked)}
/>
}
label={t('enableLogin')}
/>
{settings.loginEnabled && (
<Box sx={{ mt: 2, maxWidth: 400 }}>
<Box sx={{ mt: 2 }}>
{settings.passwordLoginAllowed !== false && (
<TextField
fullWidth
sx={{ mb: 2 }}
sx={{ mb: 2, maxWidth: 400 }}
label={t('password')}
type="password"
value={settings.password || ''}
@@ -177,43 +179,24 @@ const SecuritySettings: React.FC<SecuritySettingsProps> = ({ settings, onChange
/>
)}
<FormControlLabel
control={
<Switch
checked={!passkeysExist ? true : (settings.passwordLoginAllowed !== false)}
onChange={(e) => onChange('passwordLoginAllowed', e.target.checked)}
disabled={!settings.loginEnabled || !passkeysExist}
/>
}
label={t('allowPasswordLogin') || 'Allow Password Login'}
/>
<Box>
<FormControlLabel
control={
<Switch
checked={!passkeysExist ? true : (settings.passwordLoginAllowed !== false)}
onChange={(e) => onChange('passwordLoginAllowed', e.target.checked)}
disabled={!settings.loginEnabled || !passkeysExist}
/>
}
label={t('allowPasswordLogin') || 'Allow Password Login'}
/>
</Box>
<Box sx={{ mt: 1, mb: 2 }}>
<Typography variant="body2" color="text.secondary">
{t('allowPasswordLoginHelper') || 'When disabled, password login is not available. You must have at least one passkey to disable password login.'}
</Typography>
</Box>
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>
{t('visitorUser') || 'Visitor User'}
</Typography>
<Box sx={{ mt: 1, mb: 2 }}>
<Typography variant="body2" color="text.secondary">
{t('visitorUserHelper') || 'Set a password for the Visitor User role. Users logging in with this password will have read-only access and cannot change settings.'}
</Typography>
</Box>
<TextField
fullWidth
sx={{ mb: 2 }}
label={t('visitorPassword') || 'Visitor Password'}
type="text"
value={settings.visitorPassword || ''}
onChange={(e) => onChange('visitorPassword', e.target.value)}
helperText={
settings.isVisitorPasswordSet
? (t('visitorPasswordSetHelper') || 'Password is set. Leave empty to keep it.')
: (t('visitorPasswordHelper') || 'Password for the Visitor User to log in.')
}
/>
<FormControlLabel
control={
@@ -231,10 +214,11 @@ const SecuritySettings: React.FC<SecuritySettingsProps> = ({ settings, onChange
</Typography>
</Box>
<Box sx={{ mt: 3 }}>
<Box sx={{ mt: 3, maxWidth: 400 }}>
<Box sx={{ mb: 2 }}>
<Button
variant="outlined"
startIcon={<FingerprintIcon />}
onClick={handleCreatePasskey}
disabled={!settings.loginEnabled || createPasskeyMutation.isPending}
fullWidth
@@ -247,6 +231,7 @@ const SecuritySettings: React.FC<SecuritySettingsProps> = ({ settings, onChange
<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={() => setShowRemoveModal(true)}
disabled={!settings.loginEnabled || !passkeysExist || removePasskeysMutation.isPending}
fullWidth
@@ -254,6 +239,49 @@ const SecuritySettings: React.FC<SecuritySettingsProps> = ({ settings, onChange
{t('removePasskeys') || 'Remove All Passkeys'}
</Button>
</Box>
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>
{t('visitorUser') || 'Visitor User'}
</Typography>
<Box>
<FormControlLabel
control={
<Switch
checked={settings.visitorUserEnabled !== false}
onChange={(e) => onChange('visitorUserEnabled', e.target.checked)}
disabled={!settings.loginEnabled}
/>
}
label={t('enableVisitorUser') || 'Enable Visitor User'}
sx={{ mt: 1 }}
/>
</Box>
{settings.visitorUserEnabled !== false && (
<>
<Box sx={{ mt: 1, mb: 2 }}>
<Typography variant="body2" color="text.secondary">
{t('visitorUserHelper') || 'Set a password for the Visitor User role. Users logging in with this password will have read-only access and cannot change settings.'}
</Typography>
</Box>
<TextField
fullWidth
sx={{ mb: 2, maxWidth: 400 }}
label={t('visitorPassword') || 'Visitor Password'}
type="text"
value={settings.visitorPassword || ''}
onChange={(e) => onChange('visitorPassword', e.target.value)}
helperText={
settings.isVisitorPasswordSet
? (t('visitorPasswordSetHelper') || 'Password is set. Leave empty to keep it.')
: (t('visitorPasswordHelper') || 'Password for the Visitor User to log in.')
}
/>
</>
)}
</Box>
)}

View File

@@ -66,8 +66,9 @@ const LoginPage: React.FC = () => {
const passwordLoginAllowed = settingsData?.passwordLoginAllowed !== false;
const allowResetPassword = settingsData?.allowResetPassword !== false;
// Show visitor tab if visitorPassword is set (no longer depends on visitorMode setting)
const showVisitorTab = !!settingsData?.isVisitorPasswordSet;
// Show visitor tab if visitor user is enabled AND visitorPassword is set
const visitorUserEnabled = settingsData?.visitorUserEnabled !== false;
const showVisitorTab = visitorUserEnabled && !!settingsData?.isVisitorPasswordSet;
// Update website name when settings are loaded
useEffect(() => {
@@ -202,7 +203,7 @@ const LoginPage: React.FC = () => {
onSuccess: (data) => {
if (data.success) {
setWaitTime(0); // Reset wait time on success
login(data.token, data.role);
login(data.role);
} else {
// Handle failures (incorrect password or too many attempts)
// These are returned as 200 OK with success: false to avoid console errors
@@ -543,7 +544,7 @@ const LoginPage: React.FC = () => {
</Box>
)}
{passkeysExist && (
{passwordLoginAllowed && passkeysExist && (
<>
<Divider sx={{ my: 2 }}>OR</Divider>
<Button

View File

@@ -83,6 +83,7 @@ export interface Settings {
moveSubtitlesToVideoFolder?: boolean;
moveThumbnailsToVideoFolder?: boolean;
visitorPassword?: string;
visitorUserEnabled?: boolean;
infiniteScroll?: boolean;
videoColumns?: number;
cloudflaredTunnelEnabled?: boolean;

View File

@@ -686,6 +686,7 @@ export const ar = {
admin: "مشرف",
visitorSignIn: "تسجيل دخول الزائر",
visitorUser: "المستخدم الزائر",
enableVisitorUser: "تفعيل المستخدم الزائر",
visitorUserHelper:
"قم تمكين حساب زائر منفصل مع وصول للقراءة فقط. يمكن للزوار عرض المحتوى ولكن لا يمكنهم إجراء تغييرات.",
visitorPassword: "كلمة مرور الزائر",

View File

@@ -689,6 +689,7 @@ export const de = {
admin: "Admin",
visitorSignIn: "Besucher-Anmeldung",
visitorUser: "Besucher-Benutzer",
enableVisitorUser: "Besucher-Benutzer aktivieren",
visitorUserHelper:
"Aktivieren Sie ein separates Besucherkonto mit schreibgeschütztem Zugriff. Besucher können Inhalte ansehen, aber keine Änderungen vornehmen.",
visitorPassword: "Besucher-Passwort",

View File

@@ -156,6 +156,7 @@ export const en = {
showYoutubeSearch: "Show YouTube Search Results",
visitorModeReadOnly: "Visitor mode: Read-only",
visitorUser: "Visitor User",
enableVisitorUser: "Enable Visitor User",
visitorUserHelper:
"Enable a separate visitor account with read-only access. Visitors can view content but cannot make changes.",
visitorPassword: "Visitor Password",

View File

@@ -692,6 +692,7 @@ export const es = {
admin: "Administrador",
visitorSignIn: "Inicio de Sesión de Visitante",
visitorUser: "Usuario Visitante",
enableVisitorUser: "Habilitar Usuario Visitante",
visitorUserHelper:
"Habilite una cuenta de visitante separada con acceso de solo lectura. Los visitantes pueden ver el contenido pero no pueden realizar cambios.",
visitorPassword: "Contraseña de Visitante",

View File

@@ -719,6 +719,7 @@ export const fr = {
admin: "Administrateur",
visitorSignIn: "Connexion Visiteur",
visitorUser: "Utilisateur Visiteur",
enableVisitorUser: "Activer l'Utilisateur Visiteur",
visitorUserHelper:
"Activez un compte visiteur séparé avec un accès en lecture seule. Les visiteurs peuvent voir le contenu mais ne peuvent pas effectuer de modifications.",
visitorPassword: "Mot de passe Visiteur",

View File

@@ -706,6 +706,7 @@ export const ja = {
admin: "管理者",
visitorSignIn: "ビジターログイン",
visitorUser: "ビジターユーザー",
enableVisitorUser: "ビジターユーザーを有効にする",
visitorUserHelper:
"読み取り専用アクセス権を持つ別のビジターアカウントを有効にします。ビジターはコンテンツを閲覧できますが、変更することはできません。",
visitorPassword: "ビジターパスワード",

View File

@@ -687,6 +687,7 @@ export const ko = {
admin: "관리자",
visitorSignIn: "방문자 로그인",
visitorUser: "방문자 사용자",
enableVisitorUser: "방문자 사용자 활성화",
visitorUserHelper:
"읽기 전용 액세스 권한이 있는 별도의 방문자 계정을 활성화합니다. 방문자는 콘텐츠를 볼 수 있지만 변경할 수는 없습니다.",
visitorPassword: "방문자 비밀번호",

View File

@@ -707,6 +707,7 @@ export const pt = {
admin: "Admin",
visitorSignIn: "Login de Visitante",
visitorUser: "Usuário Visitante",
enableVisitorUser: "Habilitar Usuário Visitante",
visitorUserHelper:
"Habilite uma conta de visitante separada com acesso somente leitura. Os visitantes podem visualizar o conteúdo, mas não podem fazer alterações.",
visitorPassword: "Senha de Visitante",

View File

@@ -701,6 +701,7 @@ export const ru = {
admin: "Администратор",
visitorSignIn: "Вход для посетителей",
visitorUser: "Посетитель",
enableVisitorUser: "Включить пользователя-посетителя",
visitorUserHelper:
"Включите отдельную учетную запись посетителя с доступом только для чтения. Посетители могут просматривать контент, но не могут вносить изменения.",
visitorPassword: "Пароль посетителя",

View File

@@ -669,6 +669,7 @@ export const zh = {
admin: "管理员",
visitorSignIn: "访客登录",
visitorUser: "访客用户",
enableVisitorUser: "启用访客用户",
visitorUserHelper: "启用具有只读权限的单独访客帐户。访客可以查看内容,但不能进行更改。",
visitorPassword: "访客密码",
visitorPasswordHelper: "设置访客帐户的密码。",