feat: Add password login permission handling
This commit is contained in:
@@ -5,18 +5,31 @@ import * as storageService from "./storageService";
|
|||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { Settings, defaultSettings } from "../types/settings";
|
import { Settings, defaultSettings } from "../types/settings";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if login is required (loginEnabled is true)
|
||||||
|
*/
|
||||||
|
export function isLoginRequired(): boolean {
|
||||||
|
const settings = storageService.getSettings();
|
||||||
|
const mergedSettings = { ...defaultSettings, ...settings };
|
||||||
|
return mergedSettings.loginEnabled === true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if password authentication is enabled
|
* Check if password authentication is enabled
|
||||||
*/
|
*/
|
||||||
export function isPasswordEnabled(): {
|
export function isPasswordEnabled(): {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
waitTime?: number;
|
waitTime?: number;
|
||||||
|
loginRequired?: boolean;
|
||||||
} {
|
} {
|
||||||
const settings = storageService.getSettings();
|
const settings = storageService.getSettings();
|
||||||
const mergedSettings = { ...defaultSettings, ...settings };
|
const mergedSettings = { ...defaultSettings, ...settings };
|
||||||
|
|
||||||
// Return true only if login is enabled AND a password is set
|
// Check if password login is allowed (defaults to true for backward compatibility)
|
||||||
const isEnabled = mergedSettings.loginEnabled && !!mergedSettings.password;
|
const passwordLoginAllowed = mergedSettings.passwordLoginAllowed !== false;
|
||||||
|
|
||||||
|
// Return true only if login is enabled AND a password is set AND password login is allowed
|
||||||
|
const isEnabled = mergedSettings.loginEnabled && !!mergedSettings.password && passwordLoginAllowed;
|
||||||
|
|
||||||
// Check for remaining wait time
|
// Check for remaining wait time
|
||||||
const remainingWaitTime = loginAttemptService.canAttemptLogin();
|
const remainingWaitTime = loginAttemptService.canAttemptLogin();
|
||||||
@@ -24,6 +37,7 @@ export function isPasswordEnabled(): {
|
|||||||
return {
|
return {
|
||||||
enabled: isEnabled,
|
enabled: isEnabled,
|
||||||
waitTime: remainingWaitTime > 0 ? remainingWaitTime : undefined,
|
waitTime: remainingWaitTime > 0 ? remainingWaitTime : undefined,
|
||||||
|
loginRequired: mergedSettings.loginEnabled === true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +55,16 @@ export async function verifyPassword(
|
|||||||
const settings = storageService.getSettings();
|
const settings = storageService.getSettings();
|
||||||
const mergedSettings = { ...defaultSettings, ...settings };
|
const mergedSettings = { ...defaultSettings, ...settings };
|
||||||
|
|
||||||
|
// Check if password login is allowed (defaults to true for backward compatibility)
|
||||||
|
const passwordLoginAllowed = mergedSettings.passwordLoginAllowed !== false;
|
||||||
|
|
||||||
|
if (!passwordLoginAllowed) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Password login is not allowed. Please use passkey authentication.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!mergedSettings.password) {
|
if (!mergedSettings.password) {
|
||||||
// If no password set but login enabled, allow access
|
// If no password set but login enabled, allow access
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -90,6 +114,16 @@ export async function hashPassword(password: string): Promise<string> {
|
|||||||
* Returns the new password (should be logged, not sent to frontend)
|
* Returns the new password (should be logged, not sent to frontend)
|
||||||
*/
|
*/
|
||||||
export async function resetPassword(): Promise<string> {
|
export async function resetPassword(): Promise<string> {
|
||||||
|
const settings = storageService.getSettings();
|
||||||
|
const mergedSettings = { ...defaultSettings, ...settings };
|
||||||
|
|
||||||
|
// Check if password login is allowed (defaults to true for backward compatibility)
|
||||||
|
const passwordLoginAllowed = mergedSettings.passwordLoginAllowed !== false;
|
||||||
|
|
||||||
|
if (!passwordLoginAllowed) {
|
||||||
|
throw new Error("Password reset is not allowed when password login is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
// Generate random 8-character password using cryptographically secure random
|
// Generate random 8-character password using cryptographically secure random
|
||||||
const chars =
|
const chars =
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
@@ -102,8 +136,6 @@ export async function resetPassword(): Promise<string> {
|
|||||||
const hashedPassword = await hashPassword(newPassword);
|
const hashedPassword = await hashPassword(newPassword);
|
||||||
|
|
||||||
// Update settings with new password
|
// Update settings with new password
|
||||||
const settings = storageService.getSettings();
|
|
||||||
const mergedSettings = { ...defaultSettings, ...settings };
|
|
||||||
mergedSettings.password = hashedPassword;
|
mergedSettings.password = hashedPassword;
|
||||||
mergedSettings.loginEnabled = true; // Ensure login is enabled
|
mergedSettings.loginEnabled = true; // Ensure login is enabled
|
||||||
|
|
||||||
|
|||||||
@@ -137,9 +137,19 @@ export async function prepareSettingsForSave(
|
|||||||
const prepared = { ...newSettings };
|
const prepared = { ...newSettings };
|
||||||
|
|
||||||
// Handle password hashing
|
// Handle password hashing
|
||||||
|
// Check if password login is allowed (defaults to true for backward compatibility)
|
||||||
|
const passwordLoginAllowed = existingSettings.passwordLoginAllowed !== false;
|
||||||
|
|
||||||
if (prepared.password) {
|
if (prepared.password) {
|
||||||
// If password is provided, hash it
|
// If password login is not allowed, reject password updates
|
||||||
prepared.password = await hashPassword(prepared.password);
|
if (!passwordLoginAllowed) {
|
||||||
|
// Remove password from prepared settings to prevent update
|
||||||
|
delete prepared.password;
|
||||||
|
logger.warn("Password update rejected: password login is not allowed");
|
||||||
|
} else {
|
||||||
|
// If password is provided and allowed, hash it
|
||||||
|
prepared.password = await hashPassword(prepared.password);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// If password is empty/not provided, keep existing password
|
// If password is empty/not provided, keep existing password
|
||||||
prepared.password = existingSettings.password;
|
prepared.password = existingSettings.password;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export interface Settings {
|
export interface Settings {
|
||||||
loginEnabled: boolean;
|
loginEnabled: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
passwordLoginAllowed?: boolean;
|
||||||
defaultAutoPlay: boolean;
|
defaultAutoPlay: boolean;
|
||||||
defaultAutoLoop: boolean;
|
defaultAutoLoop: boolean;
|
||||||
maxConcurrentDownloads: number;
|
maxConcurrentDownloads: number;
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { Help, Settings, VideoLibrary } from '@mui/icons-material';
|
import { Help, Logout, Settings, VideoLibrary } from '@mui/icons-material';
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
|
Divider,
|
||||||
Fade,
|
Fade,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme
|
useTheme
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import axios from 'axios';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
|
|
||||||
interface ManageMenuProps {
|
interface ManageMenuProps {
|
||||||
@@ -21,8 +25,33 @@ const ManageMenu: React.FC<ManageMenuProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
const { logout } = useAuth();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
|
// Check if login is enabled
|
||||||
|
const { data: settingsData } = useQuery({
|
||||||
|
queryKey: ['settings'],
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_URL}/settings`, { timeout: 5000 });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
retry: 1,
|
||||||
|
retryDelay: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginEnabled = settingsData?.loginEnabled || false;
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
onClose();
|
||||||
|
logout();
|
||||||
|
navigate('/');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
@@ -68,6 +97,12 @@ const ManageMenu: React.FC<ManageMenuProps> = ({
|
|||||||
<MenuItem onClick={() => { onClose(); navigate('/instruction'); }}>
|
<MenuItem onClick={() => { onClose(); navigate('/instruction'); }}>
|
||||||
<Help sx={{ mr: 2 }} /> {t('instruction')}
|
<Help sx={{ mr: 2 }} /> {t('instruction')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{loginEnabled && <Divider />}
|
||||||
|
{loginEnabled && (
|
||||||
|
<MenuItem onClick={handleLogout}>
|
||||||
|
<Logout sx={{ mr: 2 }} /> {t('logout')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Settings, VideoLibrary } from '@mui/icons-material';
|
import { Logout, Settings, VideoLibrary } from '@mui/icons-material';
|
||||||
import { Box, Button, Collapse, Stack } from '@mui/material';
|
import { Box, Button, Collapse, Divider, Stack } from '@mui/material';
|
||||||
import { Link } from 'react-router-dom';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
import { useVisitorMode } from '../../contexts/VisitorModeContext';
|
import { useVisitorMode } from '../../contexts/VisitorModeContext';
|
||||||
import { Collection, Video } from '../../types';
|
import { Collection, Video } from '../../types';
|
||||||
@@ -45,7 +48,33 @@ const MobileMenu: React.FC<MobileMenuProps> = ({
|
|||||||
onTagToggle
|
onTagToggle
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
const { logout } = useAuth();
|
||||||
const { visitorMode } = useVisitorMode();
|
const { visitorMode } = useVisitorMode();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
|
// Check if login is enabled
|
||||||
|
const { data: settingsData } = useQuery({
|
||||||
|
queryKey: ['settings'],
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_URL}/settings`, { timeout: 5000 });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
retry: 1,
|
||||||
|
retryDelay: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginEnabled = settingsData?.loginEnabled || false;
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
onClose();
|
||||||
|
logout();
|
||||||
|
navigate('/');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapse in={open} sx={{ width: '100%' }}>
|
<Collapse in={open} sx={{ width: '100%' }}>
|
||||||
@@ -91,6 +120,22 @@ const MobileMenu: React.FC<MobileMenuProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Logout Button */}
|
||||||
|
{loginEnabled && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
fullWidth
|
||||||
|
onClick={handleLogout}
|
||||||
|
startIcon={<Logout />}
|
||||||
|
>
|
||||||
|
{t('logout')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Mobile Navigation Items */}
|
{/* Mobile Navigation Items */}
|
||||||
<Box sx={{ mt: 2 }}>
|
<Box sx={{ mt: 2 }}>
|
||||||
<Collections
|
<Collections
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Box, Button, FormControlLabel, Switch, TextField } from '@mui/material';
|
import { Box, Button, FormControlLabel, Switch, TextField, Typography } from '@mui/material';
|
||||||
import { startRegistration } from '@simplewebauthn/browser';
|
import { startRegistration } from '@simplewebauthn/browser';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
@@ -146,18 +146,36 @@ const SecuritySettings: React.FC<SecuritySettingsProps> = ({ settings, onChange
|
|||||||
|
|
||||||
{settings.loginEnabled && (
|
{settings.loginEnabled && (
|
||||||
<Box sx={{ mt: 2, maxWidth: 400 }}>
|
<Box sx={{ mt: 2, maxWidth: 400 }}>
|
||||||
<TextField
|
<FormControlLabel
|
||||||
fullWidth
|
control={
|
||||||
label={t('password')}
|
<Switch
|
||||||
type="password"
|
checked={settings.passwordLoginAllowed !== false}
|
||||||
value={settings.password || ''}
|
onChange={(e) => onChange('passwordLoginAllowed', e.target.checked)}
|
||||||
onChange={(e) => onChange('password', e.target.value)}
|
disabled={!settings.loginEnabled || !passkeysExist}
|
||||||
helperText={
|
/>
|
||||||
settings.isPasswordSet
|
|
||||||
? t('passwordHelper')
|
|
||||||
: t('passwordSetHelper')
|
|
||||||
}
|
}
|
||||||
|
label={t('allowPasswordLogin') || 'Allow Password Login'}
|
||||||
/>
|
/>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{settings.passwordLoginAllowed !== false && (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={t('password')}
|
||||||
|
type="password"
|
||||||
|
value={settings.password || ''}
|
||||||
|
onChange={(e) => onChange('password', e.target.value)}
|
||||||
|
helperText={
|
||||||
|
settings.isPasswordSet
|
||||||
|
? t('passwordHelper')
|
||||||
|
: t('passwordSetHelper')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box sx={{ mt: 3 }}>
|
<Box sx={{ mt: 3 }}>
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { AuthProvider } from '../../contexts/AuthContext';
|
||||||
import Header from '../Header';
|
import Header from '../Header';
|
||||||
|
|
||||||
// Mock contexts
|
// Mock contexts
|
||||||
@@ -92,11 +93,13 @@ describe('Header', () => {
|
|||||||
});
|
});
|
||||||
return render(
|
return render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider theme={theme}>
|
<AuthProvider>
|
||||||
<BrowserRouter>
|
<ThemeProvider theme={theme}>
|
||||||
<Header {...defaultProps} {...props} />
|
<BrowserRouter>
|
||||||
</BrowserRouter>
|
<Header {...defaultProps} {...props} />
|
||||||
</ThemeProvider>
|
</BrowserRouter>
|
||||||
|
</ThemeProvider>
|
||||||
|
</AuthProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -124,14 +127,14 @@ describe('Header', () => {
|
|||||||
// and fall back to the default name. We verify the component renders correctly either way.
|
// and fall back to the default name. We verify the component renders correctly either way.
|
||||||
const logo = screen.getByAltText('MyTube Logo');
|
const logo = screen.getByAltText('MyTube Logo');
|
||||||
expect(logo).toBeInTheDocument();
|
expect(logo).toBeInTheDocument();
|
||||||
|
|
||||||
// Wait for the component to stabilize after async operations
|
// Wait for the component to stabilize after async operations
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// The title should be either "TestTube" (if settings succeeds) or "MyTube" (default)
|
// The title should be either "TestTube" (if settings succeeds) or "MyTube" (default)
|
||||||
const title = screen.queryByText('TestTube') || screen.queryByText('MyTube');
|
const title = screen.queryByText('TestTube') || screen.queryByText('MyTube');
|
||||||
expect(title).toBeInTheDocument();
|
expect(title).toBeInTheDocument();
|
||||||
}, { timeout: 2000 });
|
}, { timeout: 2000 });
|
||||||
|
|
||||||
// Logo should always be present
|
// Logo should always be present
|
||||||
expect(logo).toBeInTheDocument();
|
expect(logo).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
const response = await axios.get(`${API_URL}/settings`);
|
const response = await axios.get(`${API_URL}/settings`);
|
||||||
const { loginEnabled, isPasswordSet } = response.data;
|
const { loginEnabled, isPasswordSet } = response.data;
|
||||||
|
|
||||||
// Login is required only if enabled AND a password is set
|
// Login is required if loginEnabled is true (regardless of password or passkey)
|
||||||
if (!loginEnabled || !isPasswordSet) {
|
if (!loginEnabled || !isPasswordSet) {
|
||||||
setLoginRequired(false);
|
setLoginRequired(false);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const LoginPage: React.FC = () => {
|
|||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Fetch website name from settings
|
// Fetch website name and settings from settings
|
||||||
const { data: settingsData } = useQuery({
|
const { data: settingsData } = useQuery({
|
||||||
queryKey: ['settings'],
|
queryKey: ['settings'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -56,6 +56,8 @@ const LoginPage: React.FC = () => {
|
|||||||
retryDelay: 1000,
|
retryDelay: 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const passwordLoginAllowed = settingsData?.passwordLoginAllowed !== false;
|
||||||
|
|
||||||
// Update website name when settings are loaded
|
// Update website name when settings are loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (settingsData && settingsData.websiteName) {
|
if (settingsData && settingsData.websiteName) {
|
||||||
@@ -99,9 +101,9 @@ const LoginPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [statusData]);
|
}, [statusData]);
|
||||||
|
|
||||||
// Auto-login if password is not enabled
|
// Auto-login only if login is not required
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusData && statusData.enabled === false) {
|
if (statusData && statusData.loginRequired === false) {
|
||||||
login();
|
login();
|
||||||
}
|
}
|
||||||
}, [statusData, login]);
|
}, [statusData, login]);
|
||||||
@@ -350,62 +352,80 @@ const LoginPage: React.FC = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1, width: '100%' }}>
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1, width: '100%' }}>
|
||||||
<TextField
|
{passwordLoginAllowed && (
|
||||||
margin="normal"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
name="password"
|
|
||||||
label={t('password')}
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
id="password"
|
|
||||||
autoComplete="current-password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
autoFocus
|
|
||||||
disabled={waitTime > 0 || loginMutation.isPending}
|
|
||||||
helperText={t('defaultPasswordHint') || "Default password: 123"}
|
|
||||||
slotProps={{
|
|
||||||
input: {
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton
|
|
||||||
aria-label={t('togglePasswordVisibility')}
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
edge="end"
|
|
||||||
>
|
|
||||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
fullWidth
|
|
||||||
variant="contained"
|
|
||||||
sx={{ mt: 3, mb: 2 }}
|
|
||||||
disabled={loginMutation.isPending || waitTime > 0}
|
|
||||||
>
|
|
||||||
{loginMutation.isPending ? (t('verifying') || 'Verifying...') : t('signIn')}
|
|
||||||
</Button>
|
|
||||||
{passkeysExist && (
|
|
||||||
<>
|
<>
|
||||||
<Divider sx={{ my: 2 }}>OR</Divider>
|
<TextField
|
||||||
<Button
|
margin="normal"
|
||||||
|
required
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
name="password"
|
||||||
startIcon={<Fingerprint />}
|
label={t('password')}
|
||||||
onClick={handlePasskeyLogin}
|
type={showPassword ? 'text' : 'password'}
|
||||||
sx={{ mb: 2 }}
|
id="password"
|
||||||
disabled={passkeyLoginMutation.isPending || waitTime > 0}
|
autoComplete="current-password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
disabled={waitTime > 0 || loginMutation.isPending}
|
||||||
|
helperText={t('defaultPasswordHint') || "Default password: 123"}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('togglePasswordVisibility')}
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
edge="end"
|
||||||
|
>
|
||||||
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
sx={{ mt: 3, mb: 2 }}
|
||||||
|
disabled={loginMutation.isPending || waitTime > 0}
|
||||||
>
|
>
|
||||||
{passkeyLoginMutation.isPending
|
{loginMutation.isPending ? (t('verifying') || 'Verifying...') : t('signIn')}
|
||||||
? (t('authenticating') || 'Authenticating...')
|
|
||||||
: (t('loginWithPasskey') || 'Login with Passkey')}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
{passkeysExist && (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ my: 2 }}>OR</Divider>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<Fingerprint />}
|
||||||
|
onClick={handlePasskeyLogin}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
disabled={passkeyLoginMutation.isPending || waitTime > 0}
|
||||||
|
>
|
||||||
|
{passkeyLoginMutation.isPending
|
||||||
|
? (t('authenticating') || 'Authenticating...')
|
||||||
|
: (t('loginWithPasskey') || 'Login with Passkey')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{!passwordLoginAllowed && passkeysExist && (
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<Fingerprint />}
|
||||||
|
onClick={handlePasskeyLogin}
|
||||||
|
sx={{ mt: 3, mb: 2 }}
|
||||||
|
disabled={passkeyLoginMutation.isPending || waitTime > 0}
|
||||||
|
>
|
||||||
|
{passkeyLoginMutation.isPending
|
||||||
|
? (t('authenticating') || 'Authenticating...')
|
||||||
|
: (t('loginWithPasskey') || 'Login with Passkey')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export interface Settings {
|
|||||||
loginEnabled: boolean;
|
loginEnabled: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
isPasswordSet?: boolean;
|
isPasswordSet?: boolean;
|
||||||
|
passwordLoginAllowed?: boolean;
|
||||||
defaultAutoPlay: boolean;
|
defaultAutoPlay: boolean;
|
||||||
defaultAutoLoop: boolean;
|
defaultAutoLoop: boolean;
|
||||||
maxConcurrentDownloads: number;
|
maxConcurrentDownloads: number;
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export const ar = {
|
|||||||
videoColumns: "أعمدة الفيديو (الصفحة الرئيسية)",
|
videoColumns: "أعمدة الفيديو (الصفحة الرئيسية)",
|
||||||
columnsCount: "{count} أعمدة",
|
columnsCount: "{count} أعمدة",
|
||||||
enableLogin: "تفعيل حماية تسجيل الدخول",
|
enableLogin: "تفعيل حماية تسجيل الدخول",
|
||||||
|
allowPasswordLogin: "السماح بتسجيل الدخول بكلمة المرور",
|
||||||
|
allowPasswordLoginHelper: "عند التعطيل، لن يكون تسجيل الدخول بكلمة المرور متاحًا. يجب أن يكون لديك مفتاح وصول واحد على الأقل لتعطيل تسجيل الدخول بكلمة المرور.",
|
||||||
password: "كلمة المرور",
|
password: "كلمة المرور",
|
||||||
enterPassword: "أدخل كلمة المرور",
|
enterPassword: "أدخل كلمة المرور",
|
||||||
togglePasswordVisibility: "تبديل رؤية كلمة المرور",
|
togglePasswordVisibility: "تبديل رؤية كلمة المرور",
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export const de = {
|
|||||||
videoColumns: "Videospalten (Startseite)",
|
videoColumns: "Videospalten (Startseite)",
|
||||||
columnsCount: "{count} Spalten",
|
columnsCount: "{count} Spalten",
|
||||||
enableLogin: "Anmeldeschutz aktivieren",
|
enableLogin: "Anmeldeschutz aktivieren",
|
||||||
|
allowPasswordLogin: "Passwort-Anmeldung zulassen",
|
||||||
|
allowPasswordLoginHelper: "Wenn deaktiviert, ist die Passwort-Anmeldung nicht verfügbar. Sie müssen mindestens einen Passkey haben, um die Passwort-Anmeldung zu deaktivieren.",
|
||||||
password: "Passwort",
|
password: "Passwort",
|
||||||
enterPassword: "Passwort eingeben",
|
enterPassword: "Passwort eingeben",
|
||||||
togglePasswordVisibility: "Passwort sichtbar machen",
|
togglePasswordVisibility: "Passwort sichtbar machen",
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export const en = {
|
|||||||
videoColumns: "Video Columns (Homepage)",
|
videoColumns: "Video Columns (Homepage)",
|
||||||
columnsCount: "{count} Columns",
|
columnsCount: "{count} Columns",
|
||||||
enableLogin: "Enable Login Protection",
|
enableLogin: "Enable Login Protection",
|
||||||
|
allowPasswordLogin: "Allow Password Login",
|
||||||
|
allowPasswordLoginHelper: "When disabled, password login is not available. You must have at least one passkey to disable password login.",
|
||||||
password: "Password",
|
password: "Password",
|
||||||
enterPassword: "Enter Password",
|
enterPassword: "Enter Password",
|
||||||
togglePasswordVisibility: "Toggle password visibility",
|
togglePasswordVisibility: "Toggle password visibility",
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ export const es = {
|
|||||||
videoColumns: "Columnas de video (Página de inicio)",
|
videoColumns: "Columnas de video (Página de inicio)",
|
||||||
columnsCount: "{count} Columnas",
|
columnsCount: "{count} Columnas",
|
||||||
enableLogin: "Habilitar Protección de Inicio de Sesión",
|
enableLogin: "Habilitar Protección de Inicio de Sesión",
|
||||||
|
allowPasswordLogin: "Permitir Inicio de Sesión con Contraseña",
|
||||||
|
allowPasswordLoginHelper: "Cuando está deshabilitado, el inicio de sesión con contraseña no está disponible. Debe tener al menos una clave de acceso para deshabilitar el inicio de sesión con contraseña.",
|
||||||
password: "Contraseña",
|
password: "Contraseña",
|
||||||
enterPassword: "Introducir contraseña",
|
enterPassword: "Introducir contraseña",
|
||||||
togglePasswordVisibility: "Alternar visibilidad de contraseña",
|
togglePasswordVisibility: "Alternar visibilidad de contraseña",
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export const fr = {
|
|||||||
videoColumns: "Colonnes vidéo (Accueil)",
|
videoColumns: "Colonnes vidéo (Accueil)",
|
||||||
columnsCount: "{count} Colonnes",
|
columnsCount: "{count} Colonnes",
|
||||||
enableLogin: "Activer la protection par connexion",
|
enableLogin: "Activer la protection par connexion",
|
||||||
|
allowPasswordLogin: "Autoriser la connexion par mot de passe",
|
||||||
|
allowPasswordLoginHelper: "Lorsqu'elle est désactivée, la connexion par mot de passe n'est pas disponible. Vous devez avoir au moins une clé d'accès pour désactiver la connexion par mot de passe.",
|
||||||
password: "Mot de passe",
|
password: "Mot de passe",
|
||||||
enterPassword: "Entrez le mot de passe",
|
enterPassword: "Entrez le mot de passe",
|
||||||
togglePasswordVisibility: "Afficher/Masquer le mot de passe",
|
togglePasswordVisibility: "Afficher/Masquer le mot de passe",
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export const ja = {
|
|||||||
videoColumns: "ビデオ列数 (ホームページ)",
|
videoColumns: "ビデオ列数 (ホームページ)",
|
||||||
columnsCount: "{count} 列",
|
columnsCount: "{count} 列",
|
||||||
enableLogin: "ログイン保護を有効にする",
|
enableLogin: "ログイン保護を有効にする",
|
||||||
|
allowPasswordLogin: "パスワードログインを許可",
|
||||||
|
allowPasswordLoginHelper: "無効にすると、パスワードログインは利用できません。パスワードログインを無効にするには、少なくとも1つのパスキーが必要です。",
|
||||||
password: "パスワード",
|
password: "パスワード",
|
||||||
enterPassword: "パスワードを入力",
|
enterPassword: "パスワードを入力",
|
||||||
togglePasswordVisibility: "パスワードの表示切り替え",
|
togglePasswordVisibility: "パスワードの表示切り替え",
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export const ko = {
|
|||||||
videoColumns: "비디오 열 (홈페이지)",
|
videoColumns: "비디오 열 (홈페이지)",
|
||||||
columnsCount: "{count} 열",
|
columnsCount: "{count} 열",
|
||||||
enableLogin: "로그인 보호 활성화",
|
enableLogin: "로그인 보호 활성화",
|
||||||
|
allowPasswordLogin: "비밀번호 로그인 허용",
|
||||||
|
allowPasswordLoginHelper: "비활성화되면 비밀번호 로그인을 사용할 수 없습니다. 비밀번호 로그인을 비활성화하려면 최소한 하나의 패스키가 있어야 합니다.",
|
||||||
password: "비밀번호",
|
password: "비밀번호",
|
||||||
enterPassword: "비밀번호 입력",
|
enterPassword: "비밀번호 입력",
|
||||||
togglePasswordVisibility: "비밀번호 표시 전환",
|
togglePasswordVisibility: "비밀번호 표시 전환",
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export const pt = {
|
|||||||
videoColumns: "Colunas de vídeo (Página inicial)",
|
videoColumns: "Colunas de vídeo (Página inicial)",
|
||||||
columnsCount: "{count} Colunas",
|
columnsCount: "{count} Colunas",
|
||||||
enableLogin: "Ativar Proteção de Login",
|
enableLogin: "Ativar Proteção de Login",
|
||||||
|
allowPasswordLogin: "Permitir Login com Senha",
|
||||||
|
allowPasswordLoginHelper: "Quando desabilitado, o login com senha não está disponível. Você deve ter pelo menos uma chave de acesso para desabilitar o login com senha.",
|
||||||
password: "Senha",
|
password: "Senha",
|
||||||
enterPassword: "Digite a senha",
|
enterPassword: "Digite a senha",
|
||||||
togglePasswordVisibility: "Alternar visibilidade da senha",
|
togglePasswordVisibility: "Alternar visibilidade da senha",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export const ru = {
|
|||||||
videoColumns: "Колонки видео (Главная страница)",
|
videoColumns: "Колонки видео (Главная страница)",
|
||||||
columnsCount: "{count} колонок",
|
columnsCount: "{count} колонок",
|
||||||
enableLogin: "Включить защиту входа",
|
enableLogin: "Включить защиту входа",
|
||||||
|
allowPasswordLogin: "Разрешить вход по паролю",
|
||||||
|
allowPasswordLoginHelper: "При отключении вход по паролю недоступен. Для отключения входа по паролю необходимо иметь хотя бы один ключ доступа.",
|
||||||
password: "Пароль",
|
password: "Пароль",
|
||||||
enterPassword: "Введите пароль",
|
enterPassword: "Введите пароль",
|
||||||
togglePasswordVisibility: "Показать/скрыть пароль",
|
togglePasswordVisibility: "Показать/скрыть пароль",
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export const zh = {
|
|||||||
videoColumns: "视频列数 (主页)",
|
videoColumns: "视频列数 (主页)",
|
||||||
columnsCount: "{count} 列",
|
columnsCount: "{count} 列",
|
||||||
enableLogin: "启用登录保护",
|
enableLogin: "启用登录保护",
|
||||||
|
allowPasswordLogin: "允许密码登录",
|
||||||
|
allowPasswordLoginHelper: "禁用后,密码登录将不可用。要禁用密码登录,您必须至少有一个通行密钥。",
|
||||||
password: "密码",
|
password: "密码",
|
||||||
enterPassword: "输入密码",
|
enterPassword: "输入密码",
|
||||||
togglePasswordVisibility: "切换密码可见性",
|
togglePasswordVisibility: "切换密码可见性",
|
||||||
|
|||||||
Reference in New Issue
Block a user