diff --git a/packages/db/package.json b/packages/db/package.json index d4adc58..4cdd0d9 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -25,6 +25,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "better-auth": "^1.5.6", "drizzle-orm": "^0.38.4", "postgres": "^3.4.5" }, diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 4563ce7..79164fa 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -18,7 +18,7 @@ import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; -import { eq, sql } from "drizzle-orm"; +import { eq, and, sql } from "drizzle-orm"; import * as schema from "./schema.js"; // ── Seed profile configuration ───────────────────────────────────────────── @@ -490,6 +490,90 @@ async function seedKnownUsers() { } } + // ── Better-Auth email+password credentials for UAT accounts ────────────────── + // Provisions Better-Auth user + account records so UAT testers can log in + // via email+password (POST /api/auth/sign-in/email) instead of Authentik SSO. + const uatPasswordAccounts = [ + { email: "uat-super@groombook.dev", name: "UAT Super User", passwordEnv: "SEED_UAT_SUPER_PASSWORD", staffEmail: "uat-super@groombook.dev" }, + { email: "uat-groomer@groombook.dev", name: "UAT Staff Groomer", passwordEnv: "SEED_UAT_GROOMER_PASSWORD", staffEmail: "uat-groomer@groombook.dev" }, + { email: "uat-customer@groombook.dev", name: "UAT Customer", passwordEnv: "SEED_UAT_CUSTOMER_PASSWORD", staffEmail: null }, + { email: "uat-tester@groombook.dev", name: "UAT Tester", passwordEnv: "SEED_UAT_TESTER_PASSWORD", staffEmail: "uat-tester@groombook.dev" }, + ]; + + for (const acct of uatPasswordAccounts) { + const password = process.env[acct.passwordEnv]; + if (!password) { + console.warn(`⚠ Skipping ${acct.email} — ${acct.passwordEnv} not set`); + continue; + } + + // 1. Find or create the Better-Auth user + const [existingUser] = await db + .select() + .from(schema.user) + .where(eq(schema.user.email, acct.email)) + .limit(1); + + let userId: string; + if (existingUser) { + userId = existingUser.id; + console.log(`✓ Better-Auth user '${acct.name}' already exists — skipping user creation`); + } else { + userId = uuid(); + await db.insert(schema.user).values({ + id: userId, + name: acct.name, + email: acct.email, + emailVerified: true, + }); + console.log(`✓ Created Better-Auth user '${acct.name}' (${acct.email})`); + } + + // 2. Check if credential account already exists + const [existingAccount] = await db + .select() + .from(schema.account) + .where(and( + eq(schema.account.userId, userId), + eq(schema.account.providerId, "credential") + )) + .limit(1); + + if (existingAccount) { + console.log(`✓ Credential account for '${acct.email}' already exists — skipping`); + } else { + // Use Better-Auth's own hashPassword so the hash format matches what + // better-auth validates at sign-in time. + // better-auth/crypto uses: N=16384, r=16, p=1, dkLen=64. + const { hashPassword } = await import("better-auth/crypto"); + const passwordHash = await hashPassword(password); + + await db.insert(schema.account).values({ + id: uuid(), + accountId: userId, + providerId: "credential", + userId, + password: passwordHash, + }); + console.log(`✓ Created credential account for '${acct.email}'`); + } + + // 3. Link staff record to Better-Auth user (for accounts that have staff records) + if (acct.staffEmail) { + const [existingStaff] = await db + .select() + .from(schema.staff) + .where(eq(schema.staff.email, acct.staffEmail)) + .limit(1); + if (existingStaff && !existingStaff.userId) { + await db.update(schema.staff) + .set({ userId }) + .where(eq(schema.staff.id, existingStaff.id)); + console.log(`✓ Linked staff '${acct.staffEmail}' → Better-Auth user`); + } + } + } + // ── Services: idempotent upsert using name as unique key ───────────────────── // UNIQUE constraint on services.name (migration 0020) must exist first. // Uses b0000001-... IDs to match main seed servicesDef for same-named services.