feat: Add script to reset password securely
This commit is contained in:
@@ -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": [],
|
||||
|
||||
155
backend/scripts/reset-password.ts
Normal file
155
backend/scripts/reset-password.ts
Normal 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);
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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%' }}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user