Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13619c698c | |||
| 620b96470f | |||
| eb3f566aeb | |||
| b96b6c06fc | |||
| fa67b75b76 | |||
| 7e329ff72f |
@@ -32,7 +32,9 @@ jobs:
|
|||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: pnpm --filter @groombook/api typecheck
|
run: |
|
||||||
|
pnpm --filter @groombook/api typecheck
|
||||||
|
pnpm --filter @groombook/db typecheck
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: pnpm --filter @groombook/api lint
|
run: pnpm --filter @groombook/api lint
|
||||||
|
|||||||
@@ -50,4 +50,5 @@ CMD ["pnpm", "--filter", "@groombook/db", "seed"]
|
|||||||
|
|
||||||
# Reset stage — drops all tables, re-runs migrations, and re-seeds
|
# Reset stage — drops all tables, re-runs migrations, and re-seeds
|
||||||
FROM builder AS reset
|
FROM builder AS reset
|
||||||
|
RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
|
||||||
CMD ["pnpm", "--filter", "@groombook/db", "reset"]
|
CMD ["pnpm", "--filter", "@groombook/db", "reset"]
|
||||||
|
|||||||
@@ -159,6 +159,10 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet
|
|||||||
| TC-API-8.5 | Add waitlist entry | POST /api/portal/waitlist with pet and service | 201 Created, waitlist entry created |
|
| TC-API-8.5 | Add waitlist entry | POST /api/portal/waitlist with pet and service | 201 Created, waitlist entry created |
|
||||||
| TC-API-8.6 | View portal invoices | GET /api/portal/invoices | 200 OK, list of client's invoices returned |
|
| TC-API-8.6 | View portal invoices | GET /api/portal/invoices | 200 OK, list of client's invoices returned |
|
||||||
| TC-API-8.7 | Pay multiple invoices | POST /api/portal/invoices/pay-multiple with invoice IDs | 200 OK, payment intent created |
|
| TC-API-8.7 | Pay multiple invoices | POST /api/portal/invoices/pay-multiple with invoice IDs | 200 OK, payment intent created |
|
||||||
|
| TC-API-8.8 | SSO bridge — valid Better Auth session | POST /api/portal/session-from-auth with valid Better Auth session cookie (authenticated SSO user with matching client email) | 201 Created, `{sessionId, clientId, clientName}` returned |
|
||||||
|
| TC-API-8.9 | SSO bridge — no Better Auth session | POST /api/portal/session-from-auth without Better Auth session cookie | 401 Unauthorized |
|
||||||
|
| TC-API-8.10 | SSO bridge — no matching client | POST /api/portal/session-from-auth with valid Better Auth session for a user with no client record | 404 Not Found, error "No client record found for this user" |
|
||||||
|
| TC-API-8.11 | SSO bridge — returned session works on portal routes | After TC-API-8.8, use returned sessionId as `X-Impersonation-Session-Id` header on GET /api/portal/me | 200 OK, client profile returned |
|
||||||
|
|
||||||
### 4.9 Waitlist
|
### 4.9 Waitlist
|
||||||
|
|
||||||
|
|||||||
+114
-2
@@ -20,6 +20,7 @@ import postgres from "postgres";
|
|||||||
import { drizzle } from "drizzle-orm/postgres-js";
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
import { eq, and, sql } from "drizzle-orm";
|
import { eq, and, sql } from "drizzle-orm";
|
||||||
import * as schema from "./schema.js";
|
import * as schema from "./schema.js";
|
||||||
|
import type { MedicalAlert } from "@groombook/types";
|
||||||
|
|
||||||
// ── Seed profile configuration ─────────────────────────────────────────────
|
// ── Seed profile configuration ─────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -243,6 +244,55 @@ const groomingNotes = [
|
|||||||
"Previous clipper burn — be gentle on belly",
|
"Previous clipper burn — be gentle on belly",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ── Extended pet profile pools ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const temperamentFlagPool: string[] = [
|
||||||
|
"friendly",
|
||||||
|
"anxious-with-strangers",
|
||||||
|
"good-with-kids",
|
||||||
|
"leash-reactive",
|
||||||
|
"vocal",
|
||||||
|
"high-energy",
|
||||||
|
"calm-on-table",
|
||||||
|
"treat-motivated",
|
||||||
|
];
|
||||||
|
|
||||||
|
const medicalAlertPool: MedicalAlert[] = [
|
||||||
|
{ id: "", type: "allergies", description: "Seasonal allergies — monitor skin", severity: "low" },
|
||||||
|
{ id: "", type: "allergies", description: "Chicken allergy — avoid poultry-based treats", severity: "high" },
|
||||||
|
{ id: "", type: "joint", description: "Hip dysplasia — handle with care", severity: "medium" },
|
||||||
|
{ id: "", type: "joint", description: "Arthritis — anti-inflammatory medication on file", severity: "medium" },
|
||||||
|
{ id: "", type: "dental", description: "Dental disease — extractions in history", severity: "medium" },
|
||||||
|
{ id: "", type: "dental", description: "Baby teeth retained — vet monitor", severity: "low" },
|
||||||
|
{ id: "", type: "heart", description: "Heart murmur grade II — avoid stress", severity: "high" },
|
||||||
|
{ id: "", type: "heart", description: "Murmur cleared by vet last year", severity: "low" },
|
||||||
|
{ id: "", type: "other", description: "Eye ulcer history — be careful around face", severity: "medium" },
|
||||||
|
{ id: "", type: "other", description: "Seizure history — avoid flashing lights", severity: "high" },
|
||||||
|
{ id: "", type: "other", description: "Luxating patella — short walks only", severity: "medium" },
|
||||||
|
{ id: "", type: "other", description: "Ear infections — dry thoroughly after bath", severity: "low" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const preferredCutPool: string[] = [
|
||||||
|
"Puppy Cut",
|
||||||
|
"Teddy Bear Cut",
|
||||||
|
"Lion Cut",
|
||||||
|
"Breed Standard",
|
||||||
|
"Summer Shave",
|
||||||
|
"Kennel Cut",
|
||||||
|
"Lamb Cut",
|
||||||
|
"Continental Clip",
|
||||||
|
"Sporting Clip",
|
||||||
|
"Sanitary Trim",
|
||||||
|
"Face & Feet Trim",
|
||||||
|
"Full Groom",
|
||||||
|
];
|
||||||
|
|
||||||
|
type CoatType = (typeof schema.coatTypeEnum.enumValues)[number];
|
||||||
|
type PetSizeCategory = (typeof schema.petSizeCategoryEnum.enumValues)[number];
|
||||||
|
|
||||||
|
const coatTypePool: CoatType[] = ["short", "medium", "long", "double", "wire", "silky", "curly", "hairless"];
|
||||||
|
const petSizeCategoryPool: PetSizeCategory[] = ["small", "medium", "large", "extra_large"];
|
||||||
|
|
||||||
const appointmentNotes = [
|
const appointmentNotes = [
|
||||||
null, null, null, null,
|
null, null, null, null,
|
||||||
"Client requested extra brushing",
|
"Client requested extra brushing",
|
||||||
@@ -853,6 +903,18 @@ async function seed() {
|
|||||||
specialCareNotes: rand() < 0.1 ? "Vet clearance required before grooming" : null,
|
specialCareNotes: rand() < 0.1 ? "Vet clearance required before grooming" : null,
|
||||||
customFields: {},
|
customFields: {},
|
||||||
image: petIndex < 250 ? pick(puggleImages) : pick(demoPetImages),
|
image: petIndex < 250 ? pick(puggleImages) : pick(demoPetImages),
|
||||||
|
temperamentScore: randInt(1, 5),
|
||||||
|
temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)),
|
||||||
|
medicalAlerts: (() => {
|
||||||
|
if (rand() < 0.3) {
|
||||||
|
const count = rand() < 0.7 ? 1 : 2;
|
||||||
|
return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})(),
|
||||||
|
preferredCuts: pickN(preferredCutPool, randInt(1, 2)),
|
||||||
|
coatType: pick(coatTypePool),
|
||||||
|
petSizeCategory: pick(petSizeCategoryPool),
|
||||||
});
|
});
|
||||||
|
|
||||||
petRecords.push({ id: petId, clientId });
|
petRecords.push({ id: petId, clientId });
|
||||||
@@ -888,6 +950,12 @@ async function seed() {
|
|||||||
specialCareNotes: pet.specialCareNotes,
|
specialCareNotes: pet.specialCareNotes,
|
||||||
customFields: pet.customFields,
|
customFields: pet.customFields,
|
||||||
image: pet.image,
|
image: pet.image,
|
||||||
|
temperamentScore: pet.temperamentScore,
|
||||||
|
temperamentFlags: pet.temperamentFlags,
|
||||||
|
medicalAlerts: pet.medicalAlerts,
|
||||||
|
preferredCuts: pet.preferredCuts,
|
||||||
|
coatType: pet.coatType,
|
||||||
|
petSizeCategory: pet.petSizeCategory,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -922,8 +990,52 @@ async function seed() {
|
|||||||
.values({ id: uc.id, name: uc.name, email: uc.email, phone: uc.phone, address: uc.address })
|
.values({ id: uc.id, name: uc.name, email: uc.email, phone: uc.phone, address: uc.address })
|
||||||
.onConflictDoUpdate({ target: schema.clients.id, set: { name: uc.name, email: uc.email, phone: uc.phone, address: uc.address } });
|
.onConflictDoUpdate({ target: schema.clients.id, set: { name: uc.name, email: uc.email, phone: uc.phone, address: uc.address } });
|
||||||
await db.insert(schema.pets)
|
await db.insert(schema.pets)
|
||||||
.values({ id: uc.petId, clientId: uc.id, name: uc.petName, species: "Dog", breed: uc.petBreed, weightKg: "25.00", dateOfBirth: new Date("2021-03-15T00:00:00Z"), image: pick(demoPetImages) })
|
.values({
|
||||||
.onConflictDoUpdate({ target: schema.pets.id, set: { clientId: uc.id, name: uc.petName, species: "Dog", breed: uc.petBreed, weightKg: "25.00", dateOfBirth: new Date("2021-03-15T00:00:00Z"), image: pick(demoPetImages) } });
|
id: uc.petId,
|
||||||
|
clientId: uc.id,
|
||||||
|
name: uc.petName,
|
||||||
|
species: "Dog",
|
||||||
|
breed: uc.petBreed,
|
||||||
|
weightKg: "25.00",
|
||||||
|
dateOfBirth: new Date("2021-03-15T00:00:00Z"),
|
||||||
|
image: pick(demoPetImages),
|
||||||
|
temperamentScore: randInt(1, 5),
|
||||||
|
temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)),
|
||||||
|
medicalAlerts: (() => {
|
||||||
|
if (rand() < 0.3) {
|
||||||
|
const count = rand() < 0.7 ? 1 : 2;
|
||||||
|
return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})(),
|
||||||
|
preferredCuts: pickN(preferredCutPool, randInt(1, 2)),
|
||||||
|
coatType: pick(coatTypePool),
|
||||||
|
petSizeCategory: pick(petSizeCategoryPool),
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: schema.pets.id,
|
||||||
|
set: {
|
||||||
|
clientId: uc.id,
|
||||||
|
name: uc.petName,
|
||||||
|
species: "Dog",
|
||||||
|
breed: uc.petBreed,
|
||||||
|
weightKg: "25.00",
|
||||||
|
dateOfBirth: new Date("2021-03-15T00:00:00Z"),
|
||||||
|
image: pick(demoPetImages),
|
||||||
|
temperamentScore: randInt(1, 5),
|
||||||
|
temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)),
|
||||||
|
medicalAlerts: (() => {
|
||||||
|
if (rand() < 0.3) {
|
||||||
|
const count = rand() < 0.7 ? 1 : 2;
|
||||||
|
return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})(),
|
||||||
|
preferredCuts: pickN(preferredCutPool, randInt(1, 2)),
|
||||||
|
coatType: pick(coatTypePool),
|
||||||
|
petSizeCategory: pick(petSizeCategoryPool),
|
||||||
|
},
|
||||||
|
});
|
||||||
// Create one completed appointment for this client
|
// Create one completed appointment for this client
|
||||||
const apptId = uuid();
|
const apptId = uuid();
|
||||||
const svcIdx = 0;
|
const svcIdx = 0;
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import { getAuth } from "../lib/auth.js";
|
||||||
|
|
||||||
|
const CLIENT_ID = "550e8400-e29b-41d4-a716-446655440001";
|
||||||
|
const CLIENT_EMAIL = "alice@example.com";
|
||||||
|
const CLIENT_NAME = "Alice Smith";
|
||||||
|
|
||||||
|
const BETTER_AUTH_SESSION = {
|
||||||
|
user: {
|
||||||
|
id: "auth-user-001",
|
||||||
|
email: CLIENT_EMAIL,
|
||||||
|
name: CLIENT_NAME,
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
id: "ba-session-001",
|
||||||
|
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MOCK_CLIENT = {
|
||||||
|
id: CLIENT_ID,
|
||||||
|
email: CLIENT_EMAIL,
|
||||||
|
name: CLIENT_NAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mockGetAuth: ReturnType<typeof vi.fn>;
|
||||||
|
let mockGetSession: ReturnType<typeof vi.fn>;
|
||||||
|
let insertedSession: Record<string, unknown> | null = null;
|
||||||
|
let mockClientRow: Record<string, unknown> | null = null;
|
||||||
|
let mockStaffRow: Record<string, unknown> | null = null;
|
||||||
|
|
||||||
|
function makeChainable(data: unknown[]): unknown {
|
||||||
|
const arr = [...data];
|
||||||
|
return new Proxy(arr, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (prop === "where" || prop === "orderBy" || prop === "limit") {
|
||||||
|
return () => makeChainable(target);
|
||||||
|
}
|
||||||
|
// @ts-expect-error proxy
|
||||||
|
return target[prop];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock("@groombook/db", () => {
|
||||||
|
const impersonationSessions = new Proxy(
|
||||||
|
{ _name: "impersonationSessions" },
|
||||||
|
{ get: (t, p) => (p === "_name" ? "impersonationSessions" : { table: "impersonationSessions", column: p }) }
|
||||||
|
);
|
||||||
|
|
||||||
|
const clients = new Proxy(
|
||||||
|
{ _name: "clients" },
|
||||||
|
{ get: (t, p) => (p === "_name" ? "clients" : { table: "clients", column: p }) }
|
||||||
|
);
|
||||||
|
|
||||||
|
const staff = new Proxy(
|
||||||
|
{ _name: "staff" },
|
||||||
|
{ get: (t, p) => (p === "_name" ? "staff" : { table: "staff", column: p }) }
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getDb: () => ({
|
||||||
|
select: () => ({
|
||||||
|
from: (table: { _name: string }) => {
|
||||||
|
if (table._name === "clients") {
|
||||||
|
return makeChainable(mockClientRow ? [mockClientRow] : []);
|
||||||
|
}
|
||||||
|
if (table._name === "staff") {
|
||||||
|
return makeChainable(mockStaffRow ? [mockStaffRow] : []);
|
||||||
|
}
|
||||||
|
return makeChainable([]);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
insert: (table: { _name: string }) => ({
|
||||||
|
values: (vals: Record<string, unknown>) => ({
|
||||||
|
returning: () => {
|
||||||
|
if (table._name === "impersonationSessions") {
|
||||||
|
insertedSession = { id: "new-session-001", ...vals };
|
||||||
|
return [insertedSession];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
impersonationSessions,
|
||||||
|
clients,
|
||||||
|
staff,
|
||||||
|
eq: vi.fn(),
|
||||||
|
and: vi.fn(),
|
||||||
|
inArray: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("../lib/auth.js", () => ({
|
||||||
|
getAuth: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { portalRouter } = await import("../routes/portal.js");
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
app.route("/portal", portalRouter);
|
||||||
|
|
||||||
|
describe("POST /portal/session-from-auth", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
insertedSession = null;
|
||||||
|
mockClientRow = null;
|
||||||
|
mockStaffRow = null;
|
||||||
|
mockGetSession = vi.fn();
|
||||||
|
mockGetAuth = vi.fn(() => ({
|
||||||
|
api: {
|
||||||
|
getSession: mockGetSession,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mocked(getAuth).mockImplementation(mockGetAuth);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 401 when no Better Auth session", async () => {
|
||||||
|
mockGetSession.mockResolvedValue(null);
|
||||||
|
const res = await app.request("/portal/session-from-auth", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toBe("Unauthorized");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 404 when authenticated user has no client record", async () => {
|
||||||
|
mockGetSession.mockResolvedValue(BETTER_AUTH_SESSION);
|
||||||
|
mockClientRow = null;
|
||||||
|
const res = await app.request("/portal/session-from-auth", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toBe("No client record found for this user");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a portal session with sessionId, clientId, clientName when client is found", async () => {
|
||||||
|
mockGetSession.mockResolvedValue(BETTER_AUTH_SESSION);
|
||||||
|
mockClientRow = MOCK_CLIENT;
|
||||||
|
mockStaffRow = { id: "00000000-0000-0000-0000-000000000001" };
|
||||||
|
const res = await app.request("/portal/session-from-auth", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body).toHaveProperty("sessionId");
|
||||||
|
expect(body).toHaveProperty("clientId", CLIENT_ID);
|
||||||
|
expect(body).toHaveProperty("clientName", CLIENT_NAME);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a portal session with reason sso-bridge", async () => {
|
||||||
|
mockGetSession.mockResolvedValue(BETTER_AUTH_SESSION);
|
||||||
|
mockClientRow = MOCK_CLIENT;
|
||||||
|
mockStaffRow = { id: "00000000-0000-0000-0000-000000000001" };
|
||||||
|
const res = await app.request("/portal/session-from-auth", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
expect(insertedSession).not.toBeNull();
|
||||||
|
expect((insertedSession as Record<string, unknown>).reason).toBe("sso-bridge");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 503 when auth is not configured", async () => {
|
||||||
|
mockGetAuth.mockImplementation(() => {
|
||||||
|
throw new Error("Auth not initialized");
|
||||||
|
});
|
||||||
|
const res = await app.request("/portal/session-from-auth", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(503);
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-1
@@ -172,7 +172,7 @@ export async function initAuth(): Promise<void> {
|
|||||||
clientSecret: oidcClientSecret,
|
clientSecret: oidcClientSecret,
|
||||||
issuerUrl: oidcIssuer,
|
issuerUrl: oidcIssuer,
|
||||||
internalBaseUrl: process.env.OIDC_INTERNAL_BASE,
|
internalBaseUrl: process.env.OIDC_INTERNAL_BASE,
|
||||||
scopes: "openid profile email",
|
scopes: "openid profile email role",
|
||||||
};
|
};
|
||||||
console.log("[auth] Using env var config (no DB config found)");
|
console.log("[auth] Using env var config (no DB config found)");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,78 @@ portalRouter.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Bridge Better Auth session → portal session for real SSO customers (GRO-1866).
|
||||||
|
// Registered BEFORE the /* middleware so it is NOT subject to validatePortalSession.
|
||||||
|
import { getAuth } from "../lib/auth.js";
|
||||||
|
|
||||||
|
portalRouter.post("/session-from-auth", async (c) => {
|
||||||
|
let auth;
|
||||||
|
try {
|
||||||
|
auth = getAuth();
|
||||||
|
} catch {
|
||||||
|
return c.json({ error: "Authentication not configured" }, 503);
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: c.req.raw.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return c.json({ error: "Unauthorized" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = getDb();
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.email, session.user.email))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return c.json({ error: "No client record found for this user" }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEMO_STAFF_ID = "00000000-0000-0000-0000-000000000001";
|
||||||
|
|
||||||
|
let staffId = DEMO_STAFF_ID;
|
||||||
|
const [demoStaff] = await db
|
||||||
|
.select({ id: staff.id })
|
||||||
|
.from(staff)
|
||||||
|
.where(eq(staff.id, DEMO_STAFF_ID))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!demoStaff) {
|
||||||
|
const [firstStaff] = await db
|
||||||
|
.select({ id: staff.id })
|
||||||
|
.from(staff)
|
||||||
|
.where(eq(staff.active, true))
|
||||||
|
.limit(1);
|
||||||
|
if (!firstStaff) {
|
||||||
|
return c.json({ error: "No staff records found" }, 500);
|
||||||
|
}
|
||||||
|
staffId = firstStaff.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [portalSession] = await db
|
||||||
|
.insert(impersonationSessions)
|
||||||
|
.values({
|
||||||
|
staffId,
|
||||||
|
clientId: client.id,
|
||||||
|
reason: "sso-bridge",
|
||||||
|
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
sessionId: portalSession.id,
|
||||||
|
clientId: client.id,
|
||||||
|
clientName: client.name,
|
||||||
|
},
|
||||||
|
201
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Apply middleware to all portal routes
|
// Apply middleware to all portal routes
|
||||||
portalRouter.use("/*", validatePortalSession, portalAudit);
|
portalRouter.use("/*", validatePortalSession, portalAudit);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user