Compare commits

..

3 Commits

Author SHA1 Message Date
Barcode Betty 3bc8c634ab Merge origin/dev into fix/gro-1749-uat-seed-sync
Resolve conflict in apps/api/src/routes/admin/seed.ts:
- Keep UAT_CLIENT with address (not stripped by GRO-1743)
- Keep weightKg in UAT_PETS (from both branches)
- Use origin/dev coatType as const (typed properly)
- Use existingPet naming (from origin/dev)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-25 17:45:25 +00:00
Barcode Betty a848a3f35f fix(gro-1749): sync UAT seed data to root src and fix route path
CI / Lint & Typecheck (pull_request) Successful in 12s
CI / Test (pull_request) Successful in 12s
CI / Build & Push Docker Images (pull_request) Successful in 1m11s
- Add UAT_CLIENT (name: "UAT Customer", email: uat-customer@groombook.dev)
- Add UAT_PETS: Bella (Poodle, curly) and Max (Labrador Retriever, short)
- Add UAT Client upsert block and UAT Pets loop after Demo Dog
- Change adminSeedRouter.post("/seed") → adminSeedRouter.post("/")
  in both src/routes/admin/seed.ts (Docker build) and apps/api/src/

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:41:27 +00:00
Flea Flicker d17915907c fix(ci): add provenance: false to fix registry push failures
CI / Test (pull_request) Successful in 1m6s
CI / Lint & Typecheck (pull_request) Successful in 1m10s
CI / Build & Push Docker Images (pull_request) Successful in 37s
OCI attestation manifests fail to push on Gitea registry when image
layers already exist (cross-repo blob references). Disabling provenance
generation on all four build-push-action steps ensures a simple single
manifest push that Gitea handles reliably.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-23 01:25:07 +00:00
7 changed files with 3 additions and 75 deletions
-1
View File
@@ -1 +0,0 @@
GRO-1757 direct push CI trigger - 2026-05-26T00:15:41Z
-20
View File
@@ -48,26 +48,6 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet
| TC-API-1.15 | Name fallback — no name, no email | Auto-provision where Better-Auth user has name = null, email = null | Staff name = "Unknown" |
| TC-API-1.16 | OIDC login — Terraform-provisioned user | Initiate OIDC login as any UAT persona (uat-super, uat-groomer, uat-customer, uat-tester), complete authentik callback | 200 OK, session created — no account_not_linked error |
#### SSO Login Journey (Authentik OIDC end-to-end)
| # | Scenario | Steps | Pass Criteria | Fail Criteria |
|---|----------|-------|---------------|---------------|
| TC-API-1.17 | SSO redirect to Authentik | Navigate to app → sign-in page shown → click "Sign in with SSO" | Redirected to Authentik at auth.farh.net | 403 error, redirect loop, no SSO button |
| TC-API-1.18 | Authenticate with valid OIDC credentials | At Authentik login page, enter valid credentials and authenticate | Redirected back to app with valid session | Redirect loop, 403, missing session cookie |
| TC-API-1.19 | SSO user auto-provisioned as groomer | Complete SSO login as a user with no pre-existing staff record | 200 response; groomer staff record auto-created; session active | 403 Forbidden, staff record not created |
| TC-API-1.20 | Existing staff record resolves correctly | Complete SSO login as uat-groomer (pre-existing staff) | 200 OK, correct staff identity resolved, no duplicate record created | 403, duplicate record, wrong staff data |
| TC-API-1.21 | SSO session grants dashboard access | After TC-API-1.18 SSO login, GET /api/staff/me | 200 OK, valid staff record returned, correct role displayed | 401/403, missing session, wrong identity |
#### OOBE Flow Post-Login
| # | Scenario | Steps | Pass Criteria | Fail Criteria |
|---|----------|-------|---------------|---------------|
| TC-API-1.22 | Fresh DB reports needsSetup | On a fresh DB (no super user), GET /api/setup/status | needsSetup: true returned | needsSetup: false when it should be true |
| TC-API-1.23 | Configure OIDC via auth-provider endpoint | POST /api/setup/auth-provider with valid OIDC config | 200 OK, auth provider configured, no 403 | 403, setup blocked, invalid config rejected |
| TC-API-1.24 | Complete setup creates super user | POST /api/setup with business name (after TC-API-1.23) | First user becomes super user, setup completes | Setup errors, 403 on admin endpoints |
| TC-API-1.25 | Super user accesses admin features | After TC-API-1.24, GET /api/staff/me and verify isSuperUser: true | isSuperUser: true, admin endpoints accessible | 403 on admin, isSuperUser: false |
| TC-API-1.26 | Auto-provision skipped during OOBE | During fresh setup (needsSetup: true), complete OIDC login — verify no duplicate staff record created before setup completes | No duplicate staff, OOBE completes successfully | Duplicate staff record, 403 before setup, auto-provision interferes with OOBE |
### 4.2 Client Management
| # | Scenario | Steps | Expected |
+1 -1
View File
@@ -46,7 +46,7 @@ const UAT_CLIENT = {
const UAT_PETS = [
{ name: "Bella", species: "Dog", breed: "Poodle", coatType: "curly" as const, weightKg: "20.00" },
{ name: "Max", species: "Dog", breed: "Labrador Retriever", coatType: "smooth" as const, weightKg: "30.00" },
{ name: "Max", species: "Dog", breed: "Labrador Retriever", coatType: "short" as const, weightKg: "30.00" },
];
const DEMO_SERVICES = [
-4
View File
@@ -105,10 +105,6 @@ export function buildPet(overrides: Partial<PetRow> & { clientId: string }): Pet
photoKey: null,
photoUploadedAt: null,
image: null,
temperamentScore: null,
temperamentFlags: [],
medicalAlerts: [],
preferredCuts: [],
createdAt: new Date("2025-01-01T00:00:00Z"),
updatedAt: new Date("2025-01-01T00:00:00Z"),
};
-5
View File
@@ -11,7 +11,6 @@ import {
unique,
uuid,
} from "drizzle-orm/pg-core";
import type { MedicalAlert } from "@groombook/types";
// ─── Enums ────────────────────────────────────────────────────────────────────
@@ -165,10 +164,6 @@ export const pets = pgTable(
specialCareNotes: text("special_care_notes"),
coatType: coatTypeEnum("coat_type"),
petSizeCategory: petSizeCategoryEnum("pet_size_category"),
temperamentScore: integer("temperament_score"),
temperamentFlags: jsonb("temperament_flags").$type<string[]>().default([]),
medicalAlerts: jsonb("medical_alerts").$type<MedicalAlert[]>().default([]),
preferredCuts: jsonb("preferred_cuts").$type<string[]>().default([]),
customFields: jsonb("custom_fields").$type<Record<string, string>>().notNull().default({}),
photoKey: text("photo_key"),
photoUploadedAt: timestamp("photo_uploaded_at"),
+1 -43
View File
@@ -1,5 +1,5 @@
import type { MiddlewareHandler } from "hono";
import { and, eq, getDb, sql, staff, account } from "@groombook/db";
import { and, eq, getDb, sql, staff } from "@groombook/db";
export type StaffRole = "groomer" | "receptionist" | "manager";
export type StaffRow = typeof staff.$inferSelect;
@@ -110,48 +110,6 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
return;
}
}
// Auto-provision for OIDC users: check if jwt.sub has an OAuth/OIDC account
// (e.g. authentik). If so, create a groomer staff record on the fly.
if (jwt.email) {
const [oidcAccount] = await db
.select({ id: account.id })
.from(account)
.where(
and(
eq(account.userId, jwt.sub),
sql`${account.providerId} IN ('authentik', 'google', 'github')`
)
)
.limit(1);
if (oidcAccount) {
// Derive name: prefer jwt.name, fall back to email prefix, then "Unknown"
const name =
jwt.name?.trim() ||
(jwt.email ? jwt.email.split("@")[0] : "Unknown");
const [newStaff] = await db
.insert(staff)
.values({
userId: jwt.sub,
email: jwt.email ?? "",
name,
role: "groomer",
isSuperUser: false,
active: true,
})
.returning();
console.log(
`[rbac] auto-provisioned staff record for OIDC user: ${jwt.sub} -> staff:${newStaff.id} (${name})`
);
c.set("staff", newStaff);
await next();
return;
}
}
return c.json(
{ error: "Forbidden: no staff record found for authenticated user" },
403
+1 -1
View File
@@ -46,7 +46,7 @@ const UAT_CLIENT = {
const UAT_PETS = [
{ name: "Bella", species: "Dog", breed: "Poodle", coatType: "curly", weightKg: "20.00" },
{ name: "Max", species: "Dog", breed: "Labrador Retriever", coatType: "smooth", weightKg: "30.00" },
{ name: "Max", species: "Dog", breed: "Labrador Retriever", coatType: "short", weightKg: "30.00" },
];
const DEMO_SERVICES = [