Files
app/scripts/rotate-totp-key.ts
BOHA 63192dc781 chore(deps): upgrade Prisma 6.19 -> 7.8.0 (Rust-free client + mariadb adapter)
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>
2026-06-09 07:06:02 +02:00

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);
});