fix(seed): update credential password on existing accounts — not skip (GRO-1977)
Previously, seed.ts skip-inserted a credential account if it already existed, freezing the stored hash at first-seed. Now it re-hashes the current env var value and UPDATE the existing row, enabling password rotation without a full DB wipe. - AC-8: re-seeding with a changed SEED_UAT_*_PASSWORD updates the stored hash - AC-5 still passes: re-seeding with the same password is idempotent (no new rows) - All 540 tests pass Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -173,7 +173,10 @@ async function seedUatCredentials(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (existingAccount) {
|
if (existingAccount) {
|
||||||
// skip — already has credential account
|
// Re-hash and update the password (mirrors seed.ts behavior)
|
||||||
|
const { hashPassword } = await import("better-auth/crypto");
|
||||||
|
const passwordHash = await hashPassword(password);
|
||||||
|
existingAccount.password = passwordHash;
|
||||||
} else {
|
} else {
|
||||||
// Use Better-Auth's hashPassword so test helper matches production seed.ts
|
// Use Better-Auth's hashPassword so test helper matches production seed.ts
|
||||||
const { hashPassword } = await import("better-auth/crypto");
|
const { hashPassword } = await import("better-auth/crypto");
|
||||||
@@ -351,6 +354,49 @@ describe("seedUatCredentials — credential provisioning logic", () => {
|
|||||||
expect(insertedAccounts).toHaveLength(0);
|
expect(insertedAccounts).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── AC-8: existing account password IS updated (not frozen at first-seed) ──
|
||||||
|
|
||||||
|
it("AC-8: re-seeding with a changed password env var updates the stored hash", async () => {
|
||||||
|
const ORIGINAL_PASSWORD = "original-password";
|
||||||
|
const ROTATED_PASSWORD = "rotated-password-456";
|
||||||
|
|
||||||
|
process.env.SEED_UAT_CUSTOMER_PASSWORD = ROTATED_PASSWORD;
|
||||||
|
|
||||||
|
const preExistingUsers: UserRow[] = [
|
||||||
|
{ id: "pre-existing-user", email: "uat-customer@groombook.dev", name: "UAT Customer", emailVerified: true },
|
||||||
|
];
|
||||||
|
// Account was created with the original password on first seed
|
||||||
|
const originalHash = await hashPassword(ORIGINAL_PASSWORD);
|
||||||
|
const preExistingAccounts: AccountRow[] = [
|
||||||
|
{
|
||||||
|
id: "pre-existing-acct",
|
||||||
|
accountId: "pre-existing-user",
|
||||||
|
providerId: "credential",
|
||||||
|
userId: "pre-existing-user",
|
||||||
|
password: originalHash,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Re-seed with the rotated password env var
|
||||||
|
await seedUatCredentials([UAT_ACCOUNTS[2]!], {
|
||||||
|
users: preExistingUsers,
|
||||||
|
accounts: preExistingAccounts,
|
||||||
|
staff: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// No new user or account created
|
||||||
|
expect(insertedUsers).toHaveLength(0);
|
||||||
|
expect(insertedAccounts).toHaveLength(0);
|
||||||
|
|
||||||
|
// The pre-existing account's password WAS updated (not frozen at first-seed).
|
||||||
|
// hashPassword uses a random salt so we verify by format + that it is a new,
|
||||||
|
// different valid hash from the original.
|
||||||
|
const updatedAcct = preExistingAccounts[0]!;
|
||||||
|
expect(updatedAcct.password).toBeDefined();
|
||||||
|
expect(updatedAcct.password).toMatch(/^[a-f0-9]{32}:[a-f0-9]{128}$/);
|
||||||
|
expect(updatedAcct.password).not.toBe(originalHash); // it actually changed
|
||||||
|
});
|
||||||
|
|
||||||
// ── AC-6: missing env var skips with warning ────────────────────────────────
|
// ── AC-6: missing env var skips with warning ────────────────────────────────
|
||||||
|
|
||||||
it("AC-6: missing SEED_UAT_*_PASSWORD env var skips that account (no error)", async () => {
|
it("AC-6: missing SEED_UAT_*_PASSWORD env var skips that account (no error)", async () => {
|
||||||
|
|||||||
@@ -594,7 +594,15 @@ async function seedKnownUsers() {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existingAccount) {
|
if (existingAccount) {
|
||||||
console.log(`✓ Credential account for '${acct.email}' already exists — skipping`);
|
// Re-hash and update the password so that re-seeding rotates credentials
|
||||||
|
// when the env var changes (e.g. after a password rotation). Previously
|
||||||
|
// this branch skipped entirely, freezing the hash at first-seed.
|
||||||
|
const { hashPassword } = await import("better-auth/crypto");
|
||||||
|
const passwordHash = await hashPassword(password);
|
||||||
|
await db.update(schema.account)
|
||||||
|
.set({ password: passwordHash })
|
||||||
|
.where(eq(schema.account.id, existingAccount.id));
|
||||||
|
console.log(`✓ Credential account for '${acct.email}' already exists — password updated`);
|
||||||
} else {
|
} else {
|
||||||
// Use Better-Auth's own hashPassword to guarantee parameter/encoding match.
|
// Use Better-Auth's own hashPassword to guarantee parameter/encoding match.
|
||||||
// better-auth/crypto uses: N=16384, r=16, p=1, dkLen=64, salt as 16-byte random
|
// better-auth/crypto uses: N=16384, r=16, p=1, dkLen=64, salt as 16-byte random
|
||||||
|
|||||||
Reference in New Issue
Block a user