/** * Notfall-Reset: setzt das Passwort eines Mitarbeiter-Users direkt in der DB. * Wird vom scripts/admin-rescue.sh-Wrapper im Container ausgeführt, wenn ein * Admin sich ausgesperrt hat (z.B. weil admin@admin.com keine echte * E-Mail-Adresse ist und der Passwort-vergessen-Flow daher nicht greift). * * Aufruf: * npx tsx prisma/reset-admin-password.ts # generiert PW * npx tsx prisma/reset-admin-password.ts # eigenes PW * * Setzt zusätzlich `tokenInvalidatedAt = now()` → alle bestehenden Sessions * dieses Users werden sofort ausgeloggt (Defense gegen Wiederverwendung * gestohlener Tokens). */ import bcrypt from 'bcryptjs'; import prisma from '../src/lib/prisma.js'; import { validatePasswordComplexity, STAFF_MIN_PASSWORD_LENGTH } from '../src/utils/passwordGenerator.js'; const BCRYPT_COST = 12; function generateRescuePassword(): string { const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; const lower = 'abcdefghijkmnopqrstuvwxyz'; const digits = '23456789'; const special = '!@#$%&*+=?'; const all = upper + lower + digits + special; const pick = (s: string) => s[Math.floor(Math.random() * s.length)]; const chars = [pick(upper), pick(lower), pick(digits), pick(special)]; for (let i = chars.length; i < 28; i++) chars.push(pick(all)); for (let i = chars.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [chars[i], chars[j]] = [chars[j], chars[i]]; } return chars.join(''); } async function main() { const email = process.argv[2]; const providedPw = process.argv[3]; if (!email) { console.error('Aufruf: npx tsx prisma/reset-admin-password.ts [passwort]'); process.exit(1); } const user = await prisma.user.findUnique({ where: { email }, select: { id: true, email: true, firstName: true, lastName: true }, }); if (!user) { console.error(`User "${email}" nicht gefunden.`); process.exit(2); } let plain: string; if (providedPw) { const c = validatePasswordComplexity(providedPw, { minLength: STAFF_MIN_PASSWORD_LENGTH }); if (!c.ok) { console.error('Übergebenes Passwort erfüllt Mitarbeiter-Komplexität nicht:'); for (const e of c.errors) console.error(' - ' + e); process.exit(3); } plain = providedPw; } else { plain = generateRescuePassword(); } const hash = await bcrypt.hash(plain, BCRYPT_COST); await prisma.user.update({ where: { id: user.id }, data: { password: hash, passwordResetToken: null, passwordResetExpiresAt: null, tokenInvalidatedAt: new Date(), }, }); console.log('========================================================'); console.log(` User: ${user.email} (${user.firstName} ${user.lastName})`); console.log(` Neues Passwort: ${plain}`); console.log(' ⚠️ Wird hier EINMAL ausgegeben – sofort kopieren!'); console.log(' Alle bestehenden Sessions wurden invalidiert.'); console.log('========================================================'); } main() .catch((e) => { console.error('Reset fehlgeschlagen:', e); process.exit(99); }) .finally(async () => { await prisma.$disconnect(); });