feat: Enable visitor user with password option
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -83,6 +83,7 @@ export interface Settings {
|
||||
moveSubtitlesToVideoFolder?: boolean;
|
||||
moveThumbnailsToVideoFolder?: boolean;
|
||||
visitorPassword?: string;
|
||||
visitorUserEnabled?: boolean;
|
||||
infiniteScroll?: boolean;
|
||||
videoColumns?: number;
|
||||
cloudflaredTunnelEnabled?: boolean;
|
||||
|
||||
@@ -686,6 +686,7 @@ export const ar = {
|
||||
admin: "مشرف",
|
||||
visitorSignIn: "تسجيل دخول الزائر",
|
||||
visitorUser: "المستخدم الزائر",
|
||||
enableVisitorUser: "تفعيل المستخدم الزائر",
|
||||
visitorUserHelper:
|
||||
"قم تمكين حساب زائر منفصل مع وصول للقراءة فقط. يمكن للزوار عرض المحتوى ولكن لا يمكنهم إجراء تغييرات.",
|
||||
visitorPassword: "كلمة مرور الزائر",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -706,6 +706,7 @@ export const ja = {
|
||||
admin: "管理者",
|
||||
visitorSignIn: "ビジターログイン",
|
||||
visitorUser: "ビジターユーザー",
|
||||
enableVisitorUser: "ビジターユーザーを有効にする",
|
||||
visitorUserHelper:
|
||||
"読み取り専用アクセス権を持つ別のビジターアカウントを有効にします。ビジターはコンテンツを閲覧できますが、変更することはできません。",
|
||||
visitorPassword: "ビジターパスワード",
|
||||
|
||||
@@ -687,6 +687,7 @@ export const ko = {
|
||||
admin: "관리자",
|
||||
visitorSignIn: "방문자 로그인",
|
||||
visitorUser: "방문자 사용자",
|
||||
enableVisitorUser: "방문자 사용자 활성화",
|
||||
visitorUserHelper:
|
||||
"읽기 전용 액세스 권한이 있는 별도의 방문자 계정을 활성화합니다. 방문자는 콘텐츠를 볼 수 있지만 변경할 수는 없습니다.",
|
||||
visitorPassword: "방문자 비밀번호",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -701,6 +701,7 @@ export const ru = {
|
||||
admin: "Администратор",
|
||||
visitorSignIn: "Вход для посетителей",
|
||||
visitorUser: "Посетитель",
|
||||
enableVisitorUser: "Включить пользователя-посетителя",
|
||||
visitorUserHelper:
|
||||
"Включите отдельную учетную запись посетителя с доступом только для чтения. Посетители могут просматривать контент, но не могут вносить изменения.",
|
||||
visitorPassword: "Пароль посетителя",
|
||||
|
||||
@@ -669,6 +669,7 @@ export const zh = {
|
||||
admin: "管理员",
|
||||
visitorSignIn: "访客登录",
|
||||
visitorUser: "访客用户",
|
||||
enableVisitorUser: "启用访客用户",
|
||||
visitorUserHelper: "启用具有只读权限的单独访客帐户。访客可以查看内容,但不能进行更改。",
|
||||
visitorPassword: "访客密码",
|
||||
visitorPasswordHelper: "设置访客帐户的密码。",
|
||||
|
||||
Reference in New Issue
Block a user