From 4ec2885b097f33e77f28b837b201ae182c7797a4 Mon Sep 17 00:00:00 2001 From: Barcode Betty Date: Sat, 23 May 2026 20:23:35 +0000 Subject: [PATCH 1/2] GRO-1636: seed.ts creates Better Auth credential accounts for UAT personas After creating staff table records for UAT personas, seedKnownUsers() now reads SEED_UAT_*_PASSWORD env vars and creates Better Auth user + account rows so personas can email+password login. Uses the same scrypt hash format (N=16384, r=8, p=1, dkLen=64) as better-auth. For uat-super and uat-groomer, the staff record is linked to the Better Auth user via userId field. Co-Authored-By: Paperclip --- packages/db/src/seed.ts | 76 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 058b7c9..0c2bd00 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -20,6 +20,7 @@ import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; import { eq, sql } from "drizzle-orm"; import * as schema from "./schema.js"; +import { randomBytes, scrypt } from "node:crypto"; // ── Seed profile configuration ───────────────────────────────────────────── @@ -509,6 +510,81 @@ async function seedKnownUsers() { } console.log(`✓ Seeded ${demoSvcs.length} services`); + // ── Better Auth credential accounts for UAT personas ───────────────────── + // Creates user + account rows so UAT personas can email+password login. + // Uses the same scrypt config as better-auth (N=16384, r=8, p=1, dkLen=64). + const uatCredAccounts: Array<{ email: string; passwordEnvKey: string; staffId: string }> = [ + { email: "uat-super@groombook.dev", passwordEnvKey: "SEED_UAT_SUPER_PASSWORD", staffId: "00000000-0000-0000-0000-000000000003" }, + { email: "uat-groomer@groombook.dev", passwordEnvKey: "SEED_UAT_GROOMER_PASSWORD", staffId: "00000000-0000-0000-0000-000000000004" }, + { email: "uat-customer@groombook.dev", passwordEnvKey: "SEED_UAT_CUSTOMER_PASSWORD", staffId: "" }, + { email: "uat-tester@groombook.dev", passwordEnvKey: "SEED_UAT_TESTER_PASSWORD", staffId: "" }, + ]; + + for (const acct of uatCredAccounts) { + const password = process.env[acct.passwordEnvKey]; + if (!password) { + console.log(`⊘ No ${acct.passwordEnvKey} set — skipping Better Auth account for ${acct.email}`); + continue; + } + + // Check if user already exists + 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.email}' already exists — skipping`); + } else { + // Hash with same scrypt params as better-auth: N=16384, r=8, p=1, dkLen=64 + // Use Promise-based scrypt API (callback pattern, wrapped in Promise) + const salt = randomBytes(16); + const key = await new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + scrypt(password.normalize("NFKC"), salt, 16384, { r: 8, p: 1, dkLen: 64 } as any, (err: Error | null, derivedKey: Buffer) => { + if (err) reject(err); + else resolve(derivedKey); + }); + }); + const passwordHash = `${salt.toString("hex")}:${key.toString("hex")}`; + + const [newUser] = await db.insert(schema.user).values({ + id: uuid(), + name: acct.email.split("@")[0]!, + email: acct.email, + emailVerified: true, + }).returning(); + userId = newUser!.id; + + await db.insert(schema.account).values({ + id: uuid(), + accountId: userId, + providerId: "credential", + userId, + password: passwordHash, + }); + console.log(`✓ Created Better Auth credential account for '${acct.email}'`); + } + + // Link staff record to Better Auth user if staff exists and has no userId yet + if (acct.staffId) { + const [existingStaff] = await db + .select() + .from(schema.staff) + .where(eq(schema.staff.id, acct.staffId)) + .limit(1); + if (existingStaff && !existingStaff.userId) { + await db.update(schema.staff) + .set({ userId }) + .where(eq(schema.staff.id, acct.staffId)); + console.log(` ↳ Linked staff '${acct.email}' to Better Auth user`); + } + } + } + // ── Client: Demo Client ── const [existingClient] = await db .select() -- 2.52.0 From 964c63bbdff5412f7da0e92bc33468f7bc780789 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Sat, 23 May 2026 20:41:30 +0000 Subject: [PATCH 2/2] GRO-1636: fix scrypt keylen=64 and add email+password UAT test cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Fix scrypt keylen: positional arg is output key length, not N cost. Correct call: scrypt(pass, salt, 64, {N:16384, r:8, p:1}) This produces a 64-byte key matching Better Auth's expected format. 2. Update UAT_PLAYBOOK.md §4.1 with 6 new email+password login test cases covering all 4 UAT personas (super, groomer, customer, tester), renumbered session/logout/RBAC tests, and a reset-cycle survival test. Co-Authored-By: Paperclip --- UAT_PLAYBOOK.md | 17 +++++++++++------ packages/db/src/seed.ts | 6 +++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/UAT_PLAYBOOK.md b/UAT_PLAYBOOK.md index f4d1cd8..fee9408 100644 --- a/UAT_PLAYBOOK.md +++ b/UAT_PLAYBOOK.md @@ -35,12 +35,17 @@ GroomBook is an open-source, self-hostable pet grooming business management & CR | # | Scenario | Steps | Expected | |---|----------|-------|----------| -| TC-APP-4.1.1 | OIDC login | 1. Navigate to UAT environment
2. Click "Login with Authentik"
3. Enter test credentials
4. Authorize the application | User is redirected to app dashboard, session is established | -| TC-APP-4.1.2 | Session persistence | 1. Log in as any user
2. Close browser tab
3. Reopen browser and navigate to UAT | User remains logged in, no re-authentication required | -| TC-APP-4.1.3 | Logout | 1. Log in as any user
2. Click logout button
3. Attempt to access protected route | User is logged out and redirected to login page | -| TC-APP-4.1.4 | RBAC - Manager access | 1. Log in as Manager
2. Navigate to Settings, Staff Management, Reports | All administrative features are accessible | -| TC-APP-4.1.5 | RBAC - Staff access | 1. Log in as Staff
2. Attempt to access Settings, Staff Management | Access denied or limited view, staff can only see assigned appointments | -| TC-APP-4.1.6 | RBAC - Client access | 1. Log in as Client
2. Navigate to portal
3. Attempt to access admin areas | Client can only view their own appointments, pets, and profile | +| TC-APP-4.1.1 | OIDC login (Authentik) | 1. Navigate to UAT environment
2. Click "Login with Authentik"
3. Enter test credentials
4. Authorize the application | User is redirected to app dashboard, session is established | +| TC-APP-4.1.2 | Email + password login (UAT Super) | 1. Navigate to UAT environment sign-in page
2. Select email+password flow
3. Enter `uat-super@groombook.dev` and UAT super password
4. Submit | User is logged in and redirected to dashboard with manager access | +| TC-APP-4.1.3 | Email + password login (UAT Groomer) | 1. Navigate to UAT environment sign-in page
2. Select email+password flow
3. Enter `uat-groomer@groombook.dev` and UAT groomer password
4. Submit | User is logged in and redirected to dashboard with staff/groomer access | +| TC-APP-4.1.4 | Email + password login (UAT Customer) | 1. Navigate to UAT environment sign-in page
2. Select email+password flow
3. Enter `uat-customer@groombook.dev` and UAT customer password
4. Submit | User is logged in with client portal access | +| TC-APP-4.1.5 | Email + password login (UAT Tester) | 1. Navigate to UAT environment sign-in page
2. Select email+password flow
3. Enter `uat-tester@groombook.dev` and UAT tester password
4. Submit | User is logged in with staff/tester access | +| TC-APP-4.1.6 | Session persistence | 1. Log in as any user
2. Close browser tab
3. Reopen browser and navigate to UAT | User remains logged in, no re-authentication required | +| TC-APP-4.1.7 | Logout | 1. Log in as any user
2. Click logout button
3. Attempt to access protected route | User is logged out and redirected to login page | +| TC-APP-4.1.8 | RBAC - Manager access | 1. Log in as Manager (OIDC or email+password)
2. Navigate to Settings, Staff Management, Reports | All administrative features are accessible | +| TC-APP-4.1.9 | RBAC - Staff access | 1. Log in as Staff (OIDC or email+password)
2. Attempt to access Settings, Staff Management | Access denied or limited view, staff can only see assigned appointments | +| TC-APP-4.1.10 | RBAC - Client access | 1. Log in as Client (email+password)
2. Navigate to portal
3. Attempt to access admin areas | Client can only view their own appointments, pets, and profile | +| TC-APP-4.1.11 | Login after hourly reset | 1. Wait for or trigger `reset-demo-data` CronJob to run
2. Attempt email+password login as any UAT persona | Login succeeds — Better Auth credential accounts survive the reset cycle | ### 4.2 Setup Wizard / OOBE diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 0c2bd00..c110b19 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -512,7 +512,7 @@ async function seedKnownUsers() { // ── Better Auth credential accounts for UAT personas ───────────────────── // Creates user + account rows so UAT personas can email+password login. - // Uses the same scrypt config as better-auth (N=16384, r=8, p=1, dkLen=64). + // Uses the same scrypt config as better-auth (keylen=64, N=16384, r=8, p=1). const uatCredAccounts: Array<{ email: string; passwordEnvKey: string; staffId: string }> = [ { email: "uat-super@groombook.dev", passwordEnvKey: "SEED_UAT_SUPER_PASSWORD", staffId: "00000000-0000-0000-0000-000000000003" }, { email: "uat-groomer@groombook.dev", passwordEnvKey: "SEED_UAT_GROOMER_PASSWORD", staffId: "00000000-0000-0000-0000-000000000004" }, @@ -539,12 +539,12 @@ async function seedKnownUsers() { userId = existingUser.id; console.log(`✓ Better Auth user '${acct.email}' already exists — skipping`); } else { - // Hash with same scrypt params as better-auth: N=16384, r=8, p=1, dkLen=64 + // Hash with same scrypt params as better-auth: keylen=64, N=16384, r=8, p=1 // Use Promise-based scrypt API (callback pattern, wrapped in Promise) const salt = randomBytes(16); const key = await new Promise((resolve, reject) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - scrypt(password.normalize("NFKC"), salt, 16384, { r: 8, p: 1, dkLen: 64 } as any, (err: Error | null, derivedKey: Buffer) => { + scrypt(password.normalize("NFKC"), salt, 64, { N: 16384, r: 8, p: 1 } as any, (err: Error | null, derivedKey: Buffer) => { if (err) reject(err); else resolve(derivedKey); }); -- 2.52.0