Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9ba6045ad | |||
| b83a793de4 | |||
| a610ef9d39 |
@@ -0,0 +1 @@
|
|||||||
|
GRO-1757 direct push CI trigger - 2026-05-26T00:15:41Z
|
||||||
@@ -32,9 +32,7 @@ jobs:
|
|||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: |
|
run: pnpm --filter @groombook/api typecheck
|
||||||
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,5 +50,4 @@ 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,10 +159,6 @@ 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
|
||||||
|
|
||||||
|
|||||||
+2
-114
@@ -20,7 +20,6 @@ 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 ─────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -244,55 +243,6 @@ 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",
|
||||||
@@ -903,18 +853,6 @@ 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 });
|
||||||
@@ -950,12 +888,6 @@ 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,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -990,52 +922,8 @@ 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({
|
.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) })
|
||||||
id: uc.petId,
|
.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) } });
|
||||||
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;
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
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 role",
|
scopes: "openid profile email",
|
||||||
};
|
};
|
||||||
console.log("[auth] Using env var config (no DB config found)");
|
console.log("[auth] Using env var config (no DB config found)");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,78 +71,6 @@ 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