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 058b7c9..c110b19 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 (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" }, + { 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: 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, 64, { N: 16384, r: 8, p: 1 } 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()