fix(seed): update credential password on existing accounts — not skip (GRO-1977) (#120)
This commit was merged in pull request #120.
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
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -248,10 +248,10 @@
|
|||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 35,
|
"idx": 36,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1751480000000,
|
"when": 1751480000000,
|
||||||
"tag": "0035_add_missing_coat_type_values",
|
"tag": "0036_add_missing_coat_type_values",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user