From b928acf5d6ea8b71ee2c4b76b68e0417ecad0c91 Mon Sep 17 00:00:00 2001 From: The Dogfather <20+gb_dogfather@noreply.git.farh.net> Date: Mon, 1 Jun 2026 00:08:19 +0000 Subject: [PATCH] =?UTF-8?q?fix(seed):=20update=20credential=20password=20o?= =?UTF-8?q?n=20existing=20accounts=20=E2=80=94=20not=20skip=20(GRO-1977)?= =?UTF-8?q?=20(#120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/seed-uat-credentials.test.ts | 48 ++++++++++++++++++- apps/api/src/db/seed.ts | 10 +++- .../0036_add_missing_coat_type_values.sql | 9 ++++ packages/db/migrations/meta/_journal.json | 4 +- 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 packages/db/migrations/0036_add_missing_coat_type_values.sql diff --git a/apps/api/src/__tests__/seed-uat-credentials.test.ts b/apps/api/src/__tests__/seed-uat-credentials.test.ts index 7f954ae..75eaffc 100644 --- a/apps/api/src/__tests__/seed-uat-credentials.test.ts +++ b/apps/api/src/__tests__/seed-uat-credentials.test.ts @@ -173,7 +173,10 @@ async function seedUatCredentials( ); 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 { // Use Better-Auth's hashPassword so test helper matches production seed.ts const { hashPassword } = await import("better-auth/crypto"); @@ -351,6 +354,49 @@ describe("seedUatCredentials — credential provisioning logic", () => { 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 ──────────────────────────────── it("AC-6: missing SEED_UAT_*_PASSWORD env var skips that account (no error)", async () => { diff --git a/apps/api/src/db/seed.ts b/apps/api/src/db/seed.ts index fc65098..e5601d1 100644 --- a/apps/api/src/db/seed.ts +++ b/apps/api/src/db/seed.ts @@ -594,7 +594,15 @@ async function seedKnownUsers() { .limit(1); 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 { // 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 diff --git a/packages/db/migrations/0036_add_missing_coat_type_values.sql b/packages/db/migrations/0036_add_missing_coat_type_values.sql new file mode 100644 index 0000000..026c5ef --- /dev/null +++ b/packages/db/migrations/0036_add_missing_coat_type_values.sql @@ -0,0 +1,9 @@ +-- Migration: 0036_add_missing_coat_type_values.sql +-- Adds missing values to coat_type enum that seed.ts requires but which were +-- omitted from the 0031_buffer_rules.sql CREATE TYPE statement (migration drift). +-- 0031 created: 'smooth', 'double', 'wire', 'curly', 'long', 'hairless' +-- Missing (from schema.ts coatTypeEnum): 'short', 'medium', 'silky' + +ALTER TYPE "coat_type" ADD VALUE IF NOT EXISTS 'short'; +ALTER TYPE "coat_type" ADD VALUE IF NOT EXISTS 'medium'; +ALTER TYPE "coat_type" ADD VALUE IF NOT EXISTS 'silky'; \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 5009d34..1c7c56a 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -248,10 +248,10 @@ "breakpoints": true }, { - "idx": 35, + "idx": 36, "version": "7", "when": 1751480000000, - "tag": "0035_add_missing_coat_type_values", + "tag": "0036_add_missing_coat_type_values", "breakpoints": true } ]