feat: Add endpoint for retrieving reset password cooldown

This commit is contained in:
Peifan Li
2026-01-03 12:58:35 -05:00
parent 9296390b82
commit 092a79f635
14 changed files with 148 additions and 30 deletions

View File

@@ -40,6 +40,20 @@ export const verifyPassword = async (
} }
}; };
/**
* Get the remaining cooldown time for password reset
* Errors are automatically handled by asyncHandler middleware
*/
export const getResetPasswordCooldown = async (
_req: Request,
res: Response
): Promise<void> => {
const remainingCooldown = passwordService.getResetPasswordCooldown();
res.json({
cooldown: remainingCooldown,
});
};
/** /**
* Reset password to a random 8-character string * Reset password to a random 8-character string
* Errors are automatically handled by asyncHandler middleware * Errors are automatically handled by asyncHandler middleware

View File

@@ -20,6 +20,7 @@ import {
} from "../controllers/hookController"; } from "../controllers/hookController";
import { import {
getPasswordEnabled, getPasswordEnabled,
getResetPasswordCooldown,
resetPassword, resetPassword,
verifyPassword, verifyPassword,
} from "../controllers/passwordController"; } from "../controllers/passwordController";
@@ -54,6 +55,7 @@ router.get("/cloudflared/status", asyncHandler(getCloudflaredStatus));
// Password routes // Password routes
router.get("/password-enabled", asyncHandler(getPasswordEnabled)); router.get("/password-enabled", asyncHandler(getPasswordEnabled));
router.get("/reset-password-cooldown", asyncHandler(getResetPasswordCooldown));
router.post("/verify-password", asyncHandler(verifyPassword)); router.post("/verify-password", asyncHandler(verifyPassword));
router.post("/reset-password", asyncHandler(resetPassword)); router.post("/reset-password", asyncHandler(resetPassword));

View File

@@ -109,6 +109,28 @@ export async function hashPassword(password: string): Promise<string> {
return await bcrypt.hash(password, salt); return await bcrypt.hash(password, salt);
} }
const RESET_PASSWORD_COOLDOWN = 60 * 60 * 1000; // 1 hour in milliseconds
/**
* Get the remaining cooldown time for password reset
* Returns the remaining time in milliseconds, or 0 if no cooldown
*/
export function getResetPasswordCooldown(): number {
const settings = storageService.getSettings();
const mergedSettings = { ...defaultSettings, ...settings };
const lastResetTime = (mergedSettings as any).lastPasswordResetTime as number | undefined;
if (!lastResetTime) {
return 0;
}
const timeSinceLastReset = Date.now() - lastResetTime;
const remainingCooldown = RESET_PASSWORD_COOLDOWN - timeSinceLastReset;
return remainingCooldown > 0 ? remainingCooldown : 0;
}
/** /**
* Reset password to a random 8-character string * Reset password to a random 8-character string
* Returns the new password (should be logged, not sent to frontend) * Returns the new password (should be logged, not sent to frontend)
@@ -131,6 +153,13 @@ export async function resetPassword(): Promise<string> {
throw new Error("Password reset is not allowed when password login is disabled"); throw new Error("Password reset is not allowed when password login is disabled");
} }
// Check cooldown period (1 hour)
const remainingCooldown = getResetPasswordCooldown();
if (remainingCooldown > 0) {
const minutes = Math.ceil(remainingCooldown / (60 * 1000));
throw new Error(`Password reset is on cooldown. Please wait ${minutes} minute${minutes !== 1 ? 's' : ''} before trying again.`);
}
// Generate random 8-character password using cryptographically secure random // Generate random 8-character password using cryptographically secure random
const chars = const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@@ -142,9 +171,10 @@ export async function resetPassword(): Promise<string> {
// Hash the new password // Hash the new password
const hashedPassword = await hashPassword(newPassword); const hashedPassword = await hashPassword(newPassword);
// Update settings with new password // Update settings with new password and reset timestamp
mergedSettings.password = hashedPassword; mergedSettings.password = hashedPassword;
mergedSettings.loginEnabled = true; // Ensure login is enabled mergedSettings.loginEnabled = true; // Ensure login is enabled
(mergedSettings as any).lastPasswordResetTime = Date.now();
storageService.saveSettings(mergedSettings); storageService.saveSettings(mergedSettings);

View File

@@ -39,6 +39,7 @@ const LoginPage: React.FC = () => {
const [alertTitle, setAlertTitle] = useState(''); const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState(''); const [alertMessage, setAlertMessage] = useState('');
const [websiteName, setWebsiteName] = useState('MyTube'); const [websiteName, setWebsiteName] = useState('MyTube');
const [resetPasswordCooldown, setResetPasswordCooldown] = useState(0); // in milliseconds
const { t } = useLanguage(); const { t } = useLanguage();
const { login } = useAuth(); const { login } = useAuth();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -97,6 +98,27 @@ const LoginPage: React.FC = () => {
const passkeysExist = passkeysData?.exists || false; const passkeysExist = passkeysData?.exists || false;
// Fetch reset password cooldown from backend
const { data: cooldownData } = useQuery({
queryKey: ['resetPasswordCooldown'],
queryFn: async () => {
try {
const response = await axios.get(`${API_URL}/settings/reset-password-cooldown`, { timeout: 5000 });
return response.data;
} catch (error) {
return { cooldown: 0 };
}
},
retry: 1,
retryDelay: 1000,
enabled: !isCheckingConnection && !isConnectionError,
refetchInterval: (query) => {
// Refetch every second if there's an active cooldown
const cooldown = query.state.data?.cooldown || 0;
return cooldown > 0 ? 1000 : false;
},
});
// Initialize wait time from server response // Initialize wait time from server response
useEffect(() => { useEffect(() => {
if (statusData && statusData.waitTime) { if (statusData && statusData.waitTime) {
@@ -104,6 +126,13 @@ const LoginPage: React.FC = () => {
} }
}, [statusData]); }, [statusData]);
// Update reset password cooldown from server response
useEffect(() => {
if (cooldownData && cooldownData.cooldown !== undefined) {
setResetPasswordCooldown(cooldownData.cooldown);
}
}, [cooldownData]);
// Auto-login only if login is not required // Auto-login only if login is not required
useEffect(() => { useEffect(() => {
if (statusData && statusData.loginRequired === false) { if (statusData && statusData.loginRequired === false) {
@@ -124,6 +153,19 @@ const LoginPage: React.FC = () => {
} }
}, [waitTime]); }, [waitTime]);
// Countdown timer for reset password cooldown (updates local state while server refetches)
useEffect(() => {
if (resetPasswordCooldown > 0) {
const interval = setInterval(() => {
setResetPasswordCooldown((prev) => {
const newTime = prev - 1000;
return newTime > 0 ? newTime : 0;
});
}, 1000);
return () => clearInterval(interval);
}
}, [resetPasswordCooldown]);
// Use dark theme for login page to match app style // Use dark theme for login page to match app style
const theme = getTheme('dark'); const theme = getTheme('dark');
@@ -196,13 +238,22 @@ const LoginPage: React.FC = () => {
setShowResetModal(false); setShowResetModal(false);
setError(''); setError('');
setWaitTime(0); setWaitTime(0);
// Invalidate queries to refresh cooldown status
queryClient.invalidateQueries({ queryKey: ['healthCheck'] }); queryClient.invalidateQueries({ queryKey: ['healthCheck'] });
queryClient.invalidateQueries({ queryKey: ['resetPasswordCooldown'] });
// Show success message // Show success message
showAlert(t('success'), t('resetPasswordSuccess')); showAlert(t('success'), t('resetPasswordSuccess'));
}, },
onError: (err: any) => { onError: (err: any) => {
console.error('Reset password error:', err); console.error('Reset password error:', err);
showAlert(t('error'), t('loginFailed')); if (err.response && err.response.data && err.response.data.message) {
// Server returned a specific error message (likely cooldown)
showAlert(t('error'), err.response.data.message);
// Refresh cooldown status
queryClient.invalidateQueries({ queryKey: ['resetPasswordCooldown'] });
} else {
showAlert(t('error'), t('loginFailed'));
}
} }
}); });
@@ -442,9 +493,11 @@ const LoginPage: React.FC = () => {
startIcon={<Refresh />} startIcon={<Refresh />}
onClick={() => setShowResetModal(true)} onClick={() => setShowResetModal(true)}
sx={{ mb: 2 }} sx={{ mb: 2 }}
disabled={resetPasswordMutation.isPending} disabled={resetPasswordMutation.isPending || resetPasswordCooldown > 0}
> >
{t('resetPassword')} {resetPasswordCooldown > 0
? `${t('resetPassword')} (${formatWaitTime(resetPasswordCooldown)})`
: t('resetPassword')}
</Button> </Button>
)} )}
{!allowResetPassword && passwordLoginAllowed && ( {!allowResetPassword && passwordLoginAllowed && (
@@ -489,7 +542,7 @@ const LoginPage: React.FC = () => {
onClose={() => setShowResetModal(false)} onClose={() => setShowResetModal(false)}
onConfirm={handleResetPassword} onConfirm={handleResetPassword}
title={t('resetPasswordTitle')} title={t('resetPasswordTitle')}
message={t('resetPasswordMessage')} message={`${t('resetPasswordMessage')}\n\n${t('resetPasswordScriptGuide')}`}
confirmText={t('resetPasswordConfirm')} confirmText={t('resetPasswordConfirm')}
cancelText={t('cancel')} cancelText={t('cancel')}
isDanger={true} isDanger={true}

View File

@@ -315,6 +315,7 @@ export const ar = {
resetPasswordSuccess: resetPasswordSuccess:
"تم إعادة تعيين كلمة المرور. تحقق من سجلات الخادم للحصول على كلمة المرور الجديدة.", "تم إعادة تعيين كلمة المرور. تحقق من سجلات الخادم للحصول على كلمة المرور الجديدة.",
resetPasswordDisabledInfo: "تم تعطيل إعادة تعيين كلمة المرور. لإعادة تعيين كلمة المرور، قم بتشغيل الأمر التالي في دليل الخادم:\n\nnpm run reset-password\n\nأو:\n\nts-node scripts/reset-password.ts\n\nسيؤدي هذا إلى إنشاء كلمة مرور عشوائية جديدة وتمكين تسجيل الدخول بكلمة المرور.", resetPasswordDisabledInfo: "تم تعطيل إعادة تعيين كلمة المرور. لإعادة تعيين كلمة المرور، قم بتشغيل الأمر التالي في دليل الخادم:\n\nnpm run reset-password\n\nأو:\n\nts-node scripts/reset-password.ts\n\nسيؤدي هذا إلى إنشاء كلمة مرور عشوائية جديدة وتمكين تسجيل الدخول بكلمة المرور.",
resetPasswordScriptGuide: "لإعادة تعيين كلمة المرور يدوياً، قم بتشغيل الأمر التالي في دليل الخادم:\n\nnpm run reset-password\n\nأو:\n\nts-node scripts/reset-password.ts\n\nإذا لم يتم توفير كلمة مرور، سيتم إنشاء كلمة مرور عشوائية مكونة من 8 أحرف.",
waitTimeMessage: "يرجى الانتظار {time} قبل المحاولة مرة أخرى.", waitTimeMessage: "يرجى الانتظار {time} قبل المحاولة مرة أخرى.",
tooManyAttempts: "محاولات فاشلة كثيرة جداً.", tooManyAttempts: "محاولات فاشلة كثيرة جداً.",
// Passkeys // Passkeys

View File

@@ -315,6 +315,7 @@ export const de = {
resetPasswordSuccess: resetPasswordSuccess:
"Das Passwort wurde zurückgesetzt. Überprüfen Sie die Backend-Protokolle für das neue Passwort.", "Das Passwort wurde zurückgesetzt. Überprüfen Sie die Backend-Protokolle für das neue Passwort.",
resetPasswordDisabledInfo: "Die Passwort-Zurücksetzung ist deaktiviert. Um Ihr Passwort zurückzusetzen, führen Sie den folgenden Befehl im Backend-Verzeichnis aus:\n\nnpm run reset-password\n\nOder:\n\nts-node scripts/reset-password.ts\n\nDies generiert ein neues zufälliges Passwort und aktiviert die Passwort-Anmeldung.", resetPasswordDisabledInfo: "Die Passwort-Zurücksetzung ist deaktiviert. Um Ihr Passwort zurückzusetzen, führen Sie den folgenden Befehl im Backend-Verzeichnis aus:\n\nnpm run reset-password\n\nOder:\n\nts-node scripts/reset-password.ts\n\nDies generiert ein neues zufälliges Passwort und aktiviert die Passwort-Anmeldung.",
resetPasswordScriptGuide: "Um das Passwort manuell zurückzusetzen, führen Sie den folgenden Befehl im Backend-Verzeichnis aus:\n\nnpm run reset-password\n\nOder:\n\nts-node scripts/reset-password.ts\n\nWenn kein Passwort angegeben wird, wird ein zufälliges 8-stelliges Passwort generiert.",
waitTimeMessage: "Bitte warten Sie {time}, bevor Sie es erneut versuchen.", waitTimeMessage: "Bitte warten Sie {time}, bevor Sie es erneut versuchen.",
tooManyAttempts: "Zu viele fehlgeschlagene Versuche.", tooManyAttempts: "Zu viele fehlgeschlagene Versuche.",
// Passkeys // Passkeys

View File

@@ -329,6 +329,7 @@ export const en = {
resetPasswordSuccess: resetPasswordSuccess:
"Password has been reset. Check backend logs for the new password.", "Password has been reset. Check backend logs for the new password.",
resetPasswordDisabledInfo: "Password reset is disabled. To reset your password, run the following command in the backend directory:\n\nnpm run reset-password\n\nOr:\n\nts-node scripts/reset-password.ts\n\nThis will generate a new random password and enable password login.", resetPasswordDisabledInfo: "Password reset is disabled. To reset your password, run the following command in the backend directory:\n\nnpm run reset-password\n\nOr:\n\nts-node scripts/reset-password.ts\n\nThis will generate a new random password and enable password login.",
resetPasswordScriptGuide: "To reset password manually, run the following command in the backend directory:\n\nnpm run reset-password\n\nOr:\n\nts-node scripts/reset-password.ts\n\nIf no password is provided, a random 8-character password will be generated.",
waitTimeMessage: "Please wait {time} before trying again.", waitTimeMessage: "Please wait {time} before trying again.",
tooManyAttempts: "Too many failed attempts.", tooManyAttempts: "Too many failed attempts.",
// Passkeys // Passkeys

View File

@@ -338,6 +338,7 @@ export const es = {
resetPasswordSuccess: resetPasswordSuccess:
"La contraseña ha sido restablecida. Consulte los registros del backend para obtener la nueva contraseña.", "La contraseña ha sido restablecida. Consulte los registros del backend para obtener la nueva contraseña.",
resetPasswordDisabledInfo: "El restablecimiento de contraseña está deshabilitado. Para restablecer su contraseña, ejecute el siguiente comando en el directorio del backend:\n\nnpm run reset-password\n\nO:\n\nts-node scripts/reset-password.ts\n\nEsto generará una nueva contraseña aleatoria y habilitará el inicio de sesión con contraseña.", resetPasswordDisabledInfo: "El restablecimiento de contraseña está deshabilitado. Para restablecer su contraseña, ejecute el siguiente comando en el directorio del backend:\n\nnpm run reset-password\n\nO:\n\nts-node scripts/reset-password.ts\n\nEsto generará una nueva contraseña aleatoria y habilitará el inicio de sesión con contraseña.",
resetPasswordScriptGuide: "Para restablecer la contraseña manualmente, ejecute el siguiente comando en el directorio del backend:\n\nnpm run reset-password\n\nO:\n\nts-node scripts/reset-password.ts\n\nSi no se proporciona una contraseña, se generará una contraseña aleatoria de 8 caracteres.",
waitTimeMessage: "Por favor espere {time} antes de intentar nuevamente.", waitTimeMessage: "Por favor espere {time} antes de intentar nuevamente.",
tooManyAttempts: "Demasiados intentos fallidos.", tooManyAttempts: "Demasiados intentos fallidos.",
// Passkeys // Passkeys

View File

@@ -338,6 +338,7 @@ export const fr = {
resetPasswordSuccess: resetPasswordSuccess:
"Le mot de passe a été réinitialisé. Consultez les journaux du backend pour le nouveau mot de passe.", "Le mot de passe a été réinitialisé. Consultez les journaux du backend pour le nouveau mot de passe.",
resetPasswordDisabledInfo: "La réinitialisation du mot de passe est désactivée. Pour réinitialiser votre mot de passe, exécutez la commande suivante dans le répertoire backend :\n\nnpm run reset-password\n\nOu :\n\nts-node scripts/reset-password.ts\n\nCela générera un nouveau mot de passe aléatoire et activera la connexion par mot de passe.", resetPasswordDisabledInfo: "La réinitialisation du mot de passe est désactivée. Pour réinitialiser votre mot de passe, exécutez la commande suivante dans le répertoire backend :\n\nnpm run reset-password\n\nOu :\n\nts-node scripts/reset-password.ts\n\nCela générera un nouveau mot de passe aléatoire et activera la connexion par mot de passe.",
resetPasswordScriptGuide: "Pour réinitialiser le mot de passe manuellement, exécutez la commande suivante dans le répertoire backend :\n\nnpm run reset-password\n\nOu :\n\nts-node scripts/reset-password.ts\n\nSi aucun mot de passe n'est fourni, un mot de passe aléatoire de 8 caractères sera généré.",
waitTimeMessage: "Veuillez attendre {time} avant de réessayer.", waitTimeMessage: "Veuillez attendre {time} avant de réessayer.",
tooManyAttempts: "Trop de tentatives échouées.", tooManyAttempts: "Trop de tentatives échouées.",
// Passkeys // Passkeys

View File

@@ -322,6 +322,7 @@ export const ja = {
resetPasswordSuccess: resetPasswordSuccess:
"パスワードがリセットされました。新しいパスワードについては、バックエンドログを確認してください。", "パスワードがリセットされました。新しいパスワードについては、バックエンドログを確認してください。",
resetPasswordDisabledInfo: "パスワードリセットは無効になっています。パスワードをリセットするには、バックエンドディレクトリで次のコマンドを実行してください:\n\nnpm run reset-password\n\nまたは\n\nts-node scripts/reset-password.ts\n\nこれにより、新しいランダムパスワードが生成され、パスワードログインが有効になります。", resetPasswordDisabledInfo: "パスワードリセットは無効になっています。パスワードをリセットするには、バックエンドディレクトリで次のコマンドを実行してください:\n\nnpm run reset-password\n\nまたは\n\nts-node scripts/reset-password.ts\n\nこれにより、新しいランダムパスワードが生成され、パスワードログインが有効になります。",
resetPasswordScriptGuide: "パスワードを手動でリセットするには、バックエンドディレクトリで次のコマンドを実行してください:\n\nnpm run reset-password\n\nまたは\n\nts-node scripts/reset-password.ts\n\nパスワードが提供されない場合、ランダムな8文字のパスワードが生成されます。",
waitTimeMessage: "再試行する前に {time} お待ちください。", waitTimeMessage: "再試行する前に {time} お待ちください。",
tooManyAttempts: "失敗した試行が多すぎます。", tooManyAttempts: "失敗した試行が多すぎます。",
// Passkeys // Passkeys

View File

@@ -317,6 +317,7 @@ export const ko = {
resetPasswordSuccess: resetPasswordSuccess:
"비밀번호가 재설정되었습니다. 새 비밀번호는 백엔드 로그를 확인하세요.", "비밀번호가 재설정되었습니다. 새 비밀번호는 백엔드 로그를 확인하세요.",
resetPasswordDisabledInfo: "비밀번호 재설정이 비활성화되어 있습니다. 비밀번호를 재설정하려면 백엔드 디렉토리에서 다음 명령을 실행하세요:\n\nnpm run reset-password\n\n또는:\n\nts-node scripts/reset-password.ts\n\n이렇게 하면 새로운 임의의 비밀번호가 생성되고 비밀번호 로그인이 활성화됩니다.", resetPasswordDisabledInfo: "비밀번호 재설정이 비활성화되어 있습니다. 비밀번호를 재설정하려면 백엔드 디렉토리에서 다음 명령을 실행하세요:\n\nnpm run reset-password\n\n또는:\n\nts-node scripts/reset-password.ts\n\n이렇게 하면 새로운 임의의 비밀번호가 생성되고 비밀번호 로그인이 활성화됩니다.",
resetPasswordScriptGuide: "비밀번호를 수동으로 재설정하려면 백엔드 디렉토리에서 다음 명령을 실행하세요:\n\nnpm run reset-password\n\n또는:\n\nts-node scripts/reset-password.ts\n\n비밀번호가 제공되지 않으면 임의의 8자 비밀번호가 생성됩니다.",
waitTimeMessage: "다시 시도하기 전에 {time} 기다려 주세요.", waitTimeMessage: "다시 시도하기 전에 {time} 기다려 주세요.",
tooManyAttempts: "실패한 시도가 너무 많습니다.", tooManyAttempts: "실패한 시도가 너무 많습니다.",
// Passkeys // Passkeys

View File

@@ -333,6 +333,7 @@ export const pt = {
resetPasswordSuccess: resetPasswordSuccess:
"A senha foi redefinida. Verifique os logs do backend para a nova senha.", "A senha foi redefinida. Verifique os logs do backend para a nova senha.",
resetPasswordDisabledInfo: "A redefinição de senha está desabilitada. Para redefinir sua senha, execute o seguinte comando no diretório do backend:\n\nnpm run reset-password\n\nOu:\n\nts-node scripts/reset-password.ts\n\nIsso gerará uma nova senha aleatória e habilitará o login com senha.", resetPasswordDisabledInfo: "A redefinição de senha está desabilitada. Para redefinir sua senha, execute o seguinte comando no diretório do backend:\n\nnpm run reset-password\n\nOu:\n\nts-node scripts/reset-password.ts\n\nIsso gerará uma nova senha aleatória e habilitará o login com senha.",
resetPasswordScriptGuide: "Para redefinir a senha manualmente, execute o seguinte comando no diretório do backend:\n\nnpm run reset-password\n\nOu:\n\nts-node scripts/reset-password.ts\n\nSe nenhuma senha for fornecida, uma senha aleatória de 8 caracteres será gerada.",
waitTimeMessage: "Por favor, aguarde {time} antes de tentar novamente.", waitTimeMessage: "Por favor, aguarde {time} antes de tentar novamente.",
tooManyAttempts: "Muitas tentativas falharam.", tooManyAttempts: "Muitas tentativas falharam.",
// Passkeys // Passkeys

View File

@@ -333,6 +333,7 @@ export const ru = {
resetPasswordSuccess: resetPasswordSuccess:
"Пароль был сброшен. Проверьте логи бэкенда для нового пароля.", "Пароль был сброшен. Проверьте логи бэкенда для нового пароля.",
resetPasswordDisabledInfo: "Сброс пароля отключен. Чтобы сбросить пароль, выполните следующую команду в директории бэкенда:\n\nnpm run reset-password\n\nИли:\n\nts-node scripts/reset-password.ts\n\nЭто сгенерирует новый случайный пароль и включит вход по паролю.", resetPasswordDisabledInfo: "Сброс пароля отключен. Чтобы сбросить пароль, выполните следующую команду в директории бэкенда:\n\nnpm run reset-password\n\nИли:\n\nts-node scripts/reset-password.ts\n\nЭто сгенерирует новый случайный пароль и включит вход по паролю.",
resetPasswordScriptGuide: "Чтобы вручную сбросить пароль, выполните следующую команду в директории бэкенда:\n\nnpm run reset-password\n\nИли:\n\nts-node scripts/reset-password.ts\n\nЕсли пароль не указан, будет сгенерирован случайный 8-символьный пароль.",
waitTimeMessage: "Пожалуйста, подождите {time} перед повторной попыткой.", waitTimeMessage: "Пожалуйста, подождите {time} перед повторной попыткой.",
tooManyAttempts: "Слишком много неудачных попыток.", tooManyAttempts: "Слишком много неудачных попыток.",
// Passkeys // Passkeys

View File

@@ -53,9 +53,11 @@ export const zh = {
columnsCount: "{count} 列", columnsCount: "{count} 列",
enableLogin: "启用登录保护", enableLogin: "启用登录保护",
allowPasswordLogin: "允许密码登录", allowPasswordLogin: "允许密码登录",
allowPasswordLoginHelper: "禁用后,密码登录将不可用。要禁用密码登录,您必须至少有一个通行密钥。", allowPasswordLoginHelper:
"禁用后,密码登录将不可用。要禁用密码登录,您必须至少有一个通行密钥。",
allowResetPassword: "允许重置密码", allowResetPassword: "允许重置密码",
allowResetPasswordHelper: "禁用后,登录页面将不显示重置密码按钮,并且重置密码 API 将被阻止。", allowResetPasswordHelper:
"禁用后,登录页面将不显示重置密码按钮,并且重置密码 API 将被阻止。",
password: "密码", password: "密码",
enterPassword: "输入密码", enterPassword: "输入密码",
togglePasswordVisibility: "切换密码可见性", togglePasswordVisibility: "切换密码可见性",
@@ -304,7 +306,10 @@ export const zh = {
"您确定要重置密码吗当前密码将被重置为一个随机的8位字符串并显示在后端日志中。", "您确定要重置密码吗当前密码将被重置为一个随机的8位字符串并显示在后端日志中。",
resetPasswordConfirm: "重置", resetPasswordConfirm: "重置",
resetPasswordSuccess: "密码已重置。请查看后端日志以获取新密码。", resetPasswordSuccess: "密码已重置。请查看后端日志以获取新密码。",
resetPasswordDisabledInfo: "密码重置已禁用。要重置密码,请在后端目录运行以下命令:\n\nnpm run reset-password\n\n或\n\nts-node scripts/reset-password.ts\n\n这将生成一个新的随机密码并启用密码登录。", resetPasswordDisabledInfo:
"密码重置已禁用。要重置密码,请在后端目录运行以下命令:\n\nnpm run reset-password\n\n或\n\nts-node scripts/reset-password.ts\n\n这将生成一个新的随机密码并启用密码登录。",
resetPasswordScriptGuide:
"要手动重置密码,请在后端目录运行以下命令:\n\nnpm run reset-password\n\n或\n\nts-node scripts/reset-password.ts\n\n如果未提供密码将生成一个随机的8位密码。",
waitTimeMessage: "请等待 {time} 后再试。", waitTimeMessage: "请等待 {time} 后再试。",
tooManyAttempts: "失败尝试次数过多。", tooManyAttempts: "失败尝试次数过多。",
// Passkeys // Passkeys
@@ -312,8 +317,10 @@ export const zh = {
creatingPasskey: "创建中...", creatingPasskey: "创建中...",
passkeyCreated: "通行密钥创建成功", passkeyCreated: "通行密钥创建成功",
passkeyCreationFailed: "创建通行密钥失败,请重试。", passkeyCreationFailed: "创建通行密钥失败,请重试。",
passkeyWebAuthnNotSupported: "此浏览器不支持 WebAuthn。请使用支持 WebAuthn 的现代浏览器。", passkeyWebAuthnNotSupported:
passkeyRequiresHttps: "WebAuthn 需要 HTTPS 或 localhost。请通过 HTTPS 访问应用程序,或使用 localhost 而不是 IP 地址。", "此浏览器不支持 WebAuthn。请使用支持 WebAuthn 的现代浏览器。",
passkeyRequiresHttps:
"WebAuthn 需要 HTTPS 或 localhost。请通过 HTTPS 访问应用程序,或使用 localhost 而不是 IP 地址。",
removePasskeys: "删除所有通行密钥", removePasskeys: "删除所有通行密钥",
removePasskeysTitle: "删除所有通行密钥", removePasskeysTitle: "删除所有通行密钥",
removePasskeysMessage: "您确定要删除所有通行密钥吗?此操作无法撤销。", removePasskeysMessage: "您确定要删除所有通行密钥吗?此操作无法撤销。",
@@ -322,7 +329,8 @@ export const zh = {
loginWithPasskey: "使用通行密钥登录", loginWithPasskey: "使用通行密钥登录",
authenticating: "验证中...", authenticating: "验证中...",
passkeyLoginFailed: "通行密钥验证失败,请重试。", passkeyLoginFailed: "通行密钥验证失败,请重试。",
passkeyErrorPermissionDenied: "用户代理或平台在当前上下文中不允许该请求,可能是因为用户拒绝了权限。", passkeyErrorPermissionDenied:
"用户代理或平台在当前上下文中不允许该请求,可能是因为用户拒绝了权限。",
passkeyErrorAlreadyRegistered: "该认证器之前已注册。", passkeyErrorAlreadyRegistered: "该认证器之前已注册。",
linkCopied: "链接已复制到剪贴板", linkCopied: "链接已复制到剪贴板",
copyFailed: "复制链接失败", copyFailed: "复制链接失败",
@@ -480,7 +488,8 @@ export const zh = {
taskDeleted: "任务已成功删除", taskDeleted: "任务已成功删除",
clearFinishedTasks: "清除已完成任务", clearFinishedTasks: "清除已完成任务",
tasksCleared: "已成功清除已完成的任务", tasksCleared: "已成功清除已完成的任务",
confirmClearFinishedTasks: "您确定要清除所有已完成的任务(包括已完成和已取消)吗?这只会将其从列表中移除,不会删除任何已下载的文件。", confirmClearFinishedTasks:
"您确定要清除所有已完成的任务(包括已完成和已取消)吗?这只会将其从列表中移除,不会删除任何已下载的文件。",
clear: "清除", clear: "清除",
// Existing Video Detection // Existing Video Detection
@@ -636,24 +645,25 @@ export const zh = {
copyUrl: "复制链接", copyUrl: "复制链接",
new: "新", new: "新",
// Task Hooks // Task Hooks
taskHooks: '任务钩子', taskHooks: "任务钩子",
taskHooksDescription: '在任务生命周期的特定时间点执行自定义 Shell 命令。可用环境变量: MYTUBE_TASK_ID, MYTUBE_TASK_TITLE, MYTUBE_SOURCE_URL, MYTUBE_VIDEO_PATH。', taskHooksDescription:
taskHooksWarning: '警告:命令将以服务器权限运行。请谨慎使用。', "在任务生命周期的特定时间点执行自定义 Shell 命令。可用环境变量: MYTUBE_TASK_ID, MYTUBE_TASK_TITLE, MYTUBE_SOURCE_URL, MYTUBE_VIDEO_PATH。",
enterPasswordToUploadHook: '请输入密码以上传此 Hook 脚本。', taskHooksWarning: "警告:命令将以服务器权限运行。请谨慎使用。",
riskCommandDetected: '检测到危险命令:{command}。上传已拒绝。', enterPasswordToUploadHook: "请输入密码以上传此 Hook 脚本。",
hookTaskBeforeStart: '任务开始前', riskCommandDetected: "检测到危险命令:{command}。上传已拒绝。",
hookTaskBeforeStartHelper: '在下载开始前执行。', hookTaskBeforeStart: "任务开始前",
hookTaskSuccess: '任务成功', hookTaskBeforeStartHelper: "在下载开始前执行。",
hookTaskSuccessHelper: '在下载成功后,云上传/删除前执行 (等待完成)。', hookTaskSuccess: "任务成功",
hookTaskFail: '任务失败', hookTaskSuccessHelper: "在下载成功后,云上传/删除前执行 (等待完成)。",
hookTaskFailHelper: '当任务失败时执行。', hookTaskFail: "任务失败",
hookTaskCancel: '任务取消', hookTaskFailHelper: "当任务失败时执行。",
hookTaskCancelHelper: '当任务被手动取消时执行。', hookTaskCancel: "任务取消",
found: '已找到', hookTaskCancelHelper: "当任务被手动取消时执行。",
notFound: '未设置', found: "已找到",
deleteHook: '删除钩子脚本', notFound: "未设置",
confirmDeleteHook: '确定要删除钩子脚本吗?', deleteHook: "删除钩子脚本",
uploadHook: '上传 .sh', confirmDeleteHook: "确定要删除此钩子脚本吗?",
uploadHook: "上传 .sh",
disclaimerTitle: "免责声明", disclaimerTitle: "免责声明",
disclaimerText: disclaimerText: