feat: Add script to reset password securely

This commit is contained in:
Peifan Li
2026-01-03 11:38:31 -05:00
parent 9968268975
commit 1b9451bffa
14 changed files with 205 additions and 16 deletions

View File

@@ -9,6 +9,7 @@
"generate": "drizzle-kit generate",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"reset-password": "ts-node scripts/reset-password.ts",
"postinstall": "node -e \"const fs = require('fs'); const cp = require('child_process'); const p = 'bgutil-ytdlp-pot-provider/server'; if (fs.existsSync(p)) { console.log('Building provider...'); cp.execSync('npm install && npx tsc', { cwd: p, stdio: 'inherit' }); } else { console.log('Skipping provider build: ' + p + ' not found'); }\""
},
"keywords": [],

View File

@@ -0,0 +1,155 @@
#!/usr/bin/env ts-node
/**
* Script to directly reset password and enable password login in the database
*
* Usage:
* npm run reset-password [new-password]
* or
* ts-node scripts/reset-password.ts [new-password]
*
* If no password is provided, a random 8-character password will be generated.
* The script will:
* 1. Hash the password using bcrypt
* 2. Update the password in the settings table
* 3. Set passwordLoginAllowed to true
* 4. Set loginEnabled to true
* 5. Display the new password (if generated)
*
* Examples:
* npm run reset-password # Generate random password
* npm run reset-password mynewpassword123 # Set specific password
*/
import Database from "better-sqlite3";
import bcrypt from "bcryptjs";
import crypto from "crypto";
import fs from "fs-extra";
import path from "path";
import dotenv from "dotenv";
// Load environment variables
dotenv.config();
// Determine database path
const ROOT_DIR = process.cwd();
const DATA_DIR = process.env.DATA_DIR || path.join(ROOT_DIR, "data");
// Normalize and resolve paths to prevent path traversal
const normalizedDataDir = path.normalize(path.resolve(DATA_DIR));
const dbPath = path.normalize(path.resolve(normalizedDataDir, "mytube.db"));
// Validate that the database path is within the expected directory
// This prevents path traversal attacks via environment variables
const resolvedDataDir = path.resolve(normalizedDataDir);
const resolvedDbPath = path.resolve(dbPath);
if (!resolvedDbPath.startsWith(resolvedDataDir + path.sep) && resolvedDbPath !== resolvedDataDir) {
console.error("Error: Invalid database path detected (path traversal attempt)");
process.exit(1);
}
/**
* Configure SQLite database for compatibility
*/
function configureDatabase(db: Database.Database): void {
db.pragma("journal_mode = DELETE");
db.pragma("synchronous = NORMAL");
db.pragma("busy_timeout = 5000");
db.pragma("foreign_keys = ON");
}
/**
* Generate a random password
*/
function generateRandomPassword(length: number = 8): string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const randomBytes = crypto.randomBytes(length);
return Array.from(randomBytes, (byte) => chars.charAt(byte % chars.length)).join("");
}
/**
* Hash a password using bcrypt
*/
async function hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt(10);
return await bcrypt.hash(password, salt);
}
/**
* Main function to reset password and enable password login
*/
async function resetPassword(newPassword?: string): Promise<void> {
// Check if database exists
if (!fs.existsSync(dbPath)) {
console.error(`Error: Database not found at ${dbPath}`);
console.error("Please ensure the MyTube backend has been started at least once.");
process.exit(1);
}
// Generate password if not provided
const password = newPassword || generateRandomPassword(8);
const isGenerated = !newPassword;
// Hash the password
console.log("Hashing password...");
const hashedPassword = await hashPassword(password);
// Connect to database
console.log(`Connecting to database at ${dbPath}...`);
const db = new Database(dbPath);
configureDatabase(db);
try {
// Start transaction
db.transaction(() => {
// Update password
db.prepare(`
INSERT INTO settings (key, value)
VALUES ('password', ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value
`).run(JSON.stringify(hashedPassword));
// Set passwordLoginAllowed to true
db.prepare(`
INSERT INTO settings (key, value)
VALUES ('passwordLoginAllowed', ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value
`).run(JSON.stringify(true));
// Set loginEnabled to true
db.prepare(`
INSERT INTO settings (key, value)
VALUES ('loginEnabled', ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value
`).run(JSON.stringify(true));
})();
console.log("✓ Password reset successfully");
console.log("✓ Password login enabled");
console.log("✓ Login enabled");
if (isGenerated) {
console.log("\n" + "=".repeat(50));
console.log("NEW PASSWORD (save this securely):");
console.log(password);
console.log("=".repeat(50));
console.log("\n⚠ This password will not be shown again!");
} else {
console.log("\n✓ Password has been set to the provided value");
}
} catch (error) {
console.error("Error updating database:", error);
process.exit(1);
} finally {
db.close();
}
}
// Parse command line arguments
const args = process.argv.slice(2);
const providedPassword = args[0];
// Run the script
resetPassword(providedPassword).catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});

View File

@@ -146,6 +146,23 @@ const SecuritySettings: React.FC<SecuritySettingsProps> = ({ settings, onChange
{settings.loginEnabled && (
<Box sx={{ mt: 2, maxWidth: 400 }}>
{settings.passwordLoginAllowed !== false && (
<TextField
fullWidth
sx={{ mb: 2 }}
label={t('password')}
type="password"
value={settings.password || ''}
onChange={(e) => onChange('password', e.target.value)}
helperText={
settings.isPasswordSet
? t('passwordHelper')
: t('passwordSetHelper')
}
/>
)}
<FormControlLabel
control={
<Switch
@@ -178,21 +195,6 @@ const SecuritySettings: React.FC<SecuritySettingsProps> = ({ settings, onChange
</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={{ mb: 2 }}>
<Button

View File

@@ -1,4 +1,4 @@
import { ErrorOutline, Fingerprint, LockOutlined, Refresh, Visibility, VisibilityOff } from '@mui/icons-material';
import { ErrorOutline, Fingerprint, InfoOutlined, LockOutlined, Refresh, Visibility, VisibilityOff } from '@mui/icons-material';
import {
Alert,
Avatar,
@@ -12,6 +12,7 @@ import {
InputAdornment,
TextField,
ThemeProvider,
Tooltip,
Typography
} from '@mui/material';
import { startAuthentication } from '@simplewebauthn/browser';
@@ -439,6 +440,26 @@ const LoginPage: React.FC = () => {
{t('resetPassword')}
</Button>
)}
{!allowResetPassword && passwordLoginAllowed && (
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', mb: 2 }}>
<Tooltip title={t('resetPasswordDisabledInfo') || 'Click for information about resetting password'}>
<IconButton
onClick={() => showAlert(
t('resetPassword') || 'Reset Password',
t('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.'
)}
color="primary"
sx={{
'&:hover': {
backgroundColor: 'action.hover'
}
}}
>
<InfoOutlined />
</IconButton>
</Tooltip>
</Box>
)}
<Box sx={{ minHeight: waitTime > 0 || (error && waitTime === 0) ? 'auto' : 0, mt: 2 }}>
{waitTime > 0 && (
<Alert severity="warning" sx={{ width: '100%' }}>

View File

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

View File

@@ -314,6 +314,7 @@ export const de = {
resetPasswordConfirm: "Zurücksetzen",
resetPasswordSuccess:
"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.",
waitTimeMessage: "Bitte warten Sie {time}, bevor Sie es erneut versuchen.",
tooManyAttempts: "Zu viele fehlgeschlagene Versuche.",
// Passkeys

View File

@@ -328,6 +328,7 @@ export const en = {
resetPasswordConfirm: "Reset",
resetPasswordSuccess:
"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.",
waitTimeMessage: "Please wait {time} before trying again.",
tooManyAttempts: "Too many failed attempts.",
// Passkeys

View File

@@ -337,6 +337,7 @@ export const es = {
resetPasswordConfirm: "Restablecer",
resetPasswordSuccess:
"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.",
waitTimeMessage: "Por favor espere {time} antes de intentar nuevamente.",
tooManyAttempts: "Demasiados intentos fallidos.",
// Passkeys

View File

@@ -337,6 +337,7 @@ export const fr = {
resetPasswordConfirm: "Réinitialiser",
resetPasswordSuccess:
"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.",
waitTimeMessage: "Veuillez attendre {time} avant de réessayer.",
tooManyAttempts: "Trop de tentatives échouées.",
// Passkeys

View File

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

View File

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

View File

@@ -332,6 +332,7 @@ export const pt = {
resetPasswordConfirm: "Redefinir",
resetPasswordSuccess:
"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.",
waitTimeMessage: "Por favor, aguarde {time} antes de tentar novamente.",
tooManyAttempts: "Muitas tentativas falharam.",
// Passkeys

View File

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

View File

@@ -304,6 +304,7 @@ export const zh = {
"您确定要重置密码吗当前密码将被重置为一个随机的8位字符串并显示在后端日志中。",
resetPasswordConfirm: "重置",
resetPasswordSuccess: "密码已重置。请查看后端日志以获取新密码。",
resetPasswordDisabledInfo: "密码重置已禁用。要重置密码,请在后端目录运行以下命令:\n\nnpm run reset-password\n\n或\n\nts-node scripts/reset-password.ts\n\n这将生成一个新的随机密码并启用密码登录。",
waitTimeMessage: "请等待 {time} 后再试。",
tooManyAttempts: "失败尝试次数过多。",
// Passkeys