feat(seed): provision Better-Auth email+password credentials for UAT accounts
Adds a seeding step after UAT staff creation that: - Creates Better-Auth user records (emailVerified: true) for 4 UAT accounts - Creates account records with providerId="credential" and scrypt-hashed passwords - Links staff.userId for accounts with existing staff records (super, groomer, tester) - Reads passwords from SEED_UAT_*_PASSWORD env vars (guard clause skips if unset) - Is fully idempotent (upsert-safe) Bypasses Authentik SSO for UAT login; Shedward can authenticate via POST /api/auth/sign-in/email using the same UAT password secrets. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -28,6 +28,12 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet
|
||||
| TC-API-1.1 | Login via OIDC | POST to OIDC provider callback, verify JWT token issued | 200 OK, JWT returned with valid claims |
|
||||
| TC-API-1.2 | Session persistence | Make authenticated request, verify session token valid | 200 OK, request succeeds |
|
||||
| TC-API-1.3 | Logout | Call logout endpoint, verify token invalidated | 200 OK, subsequent requests return 401 |
|
||||
| TC-API-1.4 | Email+password login (UAT) | POST /api/auth/sign-in/email with uat-super@groombook.dev + SEED_UAT_SUPER_PASSWORD | 200 OK, session cookie returned |
|
||||
| TC-API-1.5 | Email+password login — groomer | POST /api/auth/sign-in/email with uat-groomer@groombook.dev + SEED_UAT_GROOMER_PASSWORD | 200 OK, session cookie returned |
|
||||
| TC-API-1.6 | Email+password login — customer | POST /api/auth/sign-in/email with uat-customer@groombook.dev + SEED_UAT_CUSTOMER_PASSWORD | 200 OK, session cookie returned |
|
||||
| TC-API-1.7 | Email+password login — tester | POST /api/auth/sign-in/email with uat-tester@groombook.dev + SEED_UAT_TESTER_PASSWORD | 200 OK, session cookie returned |
|
||||
| TC-API-1.8 | Email+password — invalid password | POST /api/auth/sign-in/email with wrong password | 400 Bad Request, error returned |
|
||||
| TC-API-1.9 | Email+password — unknown user | POST /api/auth/sign-in/email with non-existent email | 400 Bad Request, error returned |
|
||||
|
||||
### 4.2 Client Management
|
||||
|
||||
|
||||
+87
-1
@@ -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 ─────────────────────────────────────────────
|
||||
@@ -511,6 +511,92 @@ 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 {
|
||||
// Hash password using the same scrypt derivation as crypto.ts (AES-256-GCM key derivation).
|
||||
// Better-Auth defaults to scrypt for password hashing; match those parameters here.
|
||||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||||
const salt = randomBytes(16);
|
||||
// scryptSync(password, salt, keylen=64, N=32768, r=8, p=1) — matches common better-auth scrypt params
|
||||
const hashed = scryptSync(password, salt, 64, { N: 32768, r: 8, p: 1 }).toString("base64");
|
||||
const passwordHash = `${salt.toString("base64")}:${hashed}`;
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user