feat: Add endpoint for retrieving reset password cooldown
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user