3dda83314a
Use Case: Admin sperrt sich aus (admin@admin.com ist keine echte Mailadresse, Passwort-vergessen-Flow kann keine Mail liefern) oder Brute-Force-Lockout will sich nicht von selbst auflösen. backend/prisma/reset-admin-password.ts: - Findet User per Email, hasht neues PW mit bcrypt cost 12 - Schreibt direkt in user.password, setzt tokenInvalidatedAt=now() (kickt alle bestehenden Sessions), löscht Reset-Tokens - Eigenes PW: Komplexitäts-Check 25 Zeichen - Kein PW-Argument: 28-char Zufallspasswort (alle 4 Klassen garantiert), wird einmal in stdout ausgegeben scripts/admin-rescue.sh: - password <email> [pw] → docker exec npx tsx … reset-admin-password - unlock → docker restart opencrm-app (leert In-Memory-Rate-Limit-Store) - all <email> [pw] → beides Live-verifiziert: random-Modus, schwaches PW → klare Fehlerliste, langes eigenes PW → akzeptiert, unbekannter User → exit 2, bash -n syntax-check ok. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
/**
|
||
* 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 <email> # generiert PW
|
||
* npx tsx prisma/reset-admin-password.ts <email> <passwort> # 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 <email> [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();
|
||
});
|