prisma / @prisma/client 6.19.2 -> 7.8.0; added @prisma/adapter-mariadb 7.8.0 and mariadb 3.5.2 (the runtime driver). Prisma 7 breaking changes handled (verified via Prisma's own tooling — the official upgrade-guide summary had hallucinated package names, so I confirmed everything against `prisma validate`/`generate` and the installed type defs): - The Rust query engine is removed; the runtime connection is now made through a driver adapter. src/config/database.ts wires PrismaMariaDb, parsing DATABASE_URL into an explicit mariadb pool config so the literal `@` in our password (which a naive connection-string parser mishandles) is correct. - datasource `url` is no longer allowed in schema.prisma -> moved to a new prisma.config.ts (schema + datasource.url + migrations.seed). Prisma 7 no longer auto-loads .env, so the config does `import "dotenv/config"`. - The deprecated package.json#prisma seed config was removed (now in the config). - seed.ts and the two maintenance scripts now use the shared adapter-wired client. The legacy `prisma-client-js` generator STILL works under v7 (generates to @prisma/client as before), so NO import-path changes were needed across the 16 @prisma/client import sites — switching to the new prisma-client/output generator is not required for us. No prisma.$use() middleware exists. Node 24 / TS 5.9 exceed the v7 minimums (20.19 / 5.4). No migrations were run (your DBs are untouched); removing datasource.url is a config change, not DDL, so this phase needs no new migration. Gates: tsc 0 | build 0 | vitest 247/247 against app_test (proves the mariadb adapter works at runtime, incl. raw SELECT...FOR UPDATE locking and Decimal/VAT) | eslint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
2.6 KiB
TypeScript
74 lines
2.6 KiB
TypeScript
import "dotenv/config";
|
|
import { decryptWithKey, encryptWithKey } from "../src/utils/encryption";
|
|
// Shared adapter-wired client (Prisma 7 needs a driver adapter).
|
|
import prisma from "../src/config/database";
|
|
|
|
// Reuse the dual-format decrypt/encrypt from src/utils/encryption.ts so this
|
|
// script handles BOTH wire formats: the TS `hex:hex:hex` form and the
|
|
// PHP-legacy single-base64 blob. The previous hand-rolled `decrypt` only parsed
|
|
// the TS form and threw `Buffer.from(undefined, 'hex')` on any legacy secret —
|
|
// inside the $transaction that aborted/rolled back the ENTIRE batch on the first
|
|
// legacy user. These key-parameterized helpers accept the old/new keys passed on
|
|
// the command line (the config-bound `decrypt`/`encrypt` use the env key only).
|
|
const decrypt = (ciphertext: string, keyHex: string): string =>
|
|
decryptWithKey(ciphertext, keyHex);
|
|
const encrypt = (plaintext: string, keyHex: string): string =>
|
|
encryptWithKey(plaintext, keyHex);
|
|
|
|
async function main() {
|
|
const oldKey = process.argv[2];
|
|
const newKey = process.argv[3];
|
|
const dryRun = process.argv.includes("--dry-run");
|
|
|
|
if (!oldKey || !newKey) {
|
|
console.error(
|
|
"Usage: tsx scripts/rotate-totp-key.ts <old-key-hex> <new-key-hex> [--dry-run]",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const users = await prisma.users.findMany({
|
|
where: { totp_enabled: true, totp_secret: { not: null } },
|
|
select: { id: true, username: true, totp_secret: true },
|
|
});
|
|
|
|
console.log(`Found ${users.length} users with TOTP enabled`);
|
|
|
|
if (dryRun) {
|
|
for (const user of users) {
|
|
try {
|
|
const decrypted = decrypt(user.totp_secret!, oldKey);
|
|
const reEncrypted = encrypt(decrypted, newKey);
|
|
decrypt(reEncrypted, newKey); // verify round-trip
|
|
console.log(
|
|
` [OK] ${user.username} (id=${user.id}) — decryption and re-encryption verified`,
|
|
);
|
|
} catch (e) {
|
|
console.error(` [FAIL] ${user.username} (id=${user.id}) — ${e}`);
|
|
}
|
|
}
|
|
console.log("\nDry run complete. No changes made.");
|
|
return;
|
|
}
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
for (const user of users) {
|
|
const decrypted = decrypt(user.totp_secret!, oldKey);
|
|
const reEncrypted = encrypt(decrypted, newKey);
|
|
await tx.users.update({
|
|
where: { id: user.id },
|
|
data: { totp_secret: reEncrypted },
|
|
});
|
|
console.log(` Re-encrypted TOTP for ${user.username} (id=${user.id})`);
|
|
}
|
|
});
|
|
|
|
console.log("\nAll TOTP secrets re-encrypted successfully.");
|
|
await prisma.$disconnect();
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|