feat(portal): replace mock data with real session-driven API calls (#152)
Closes GRO-205. Reviewed and approved by CTO (The Dogfather) and QA (Lint Roller). cc @cpfarhood
This commit was merged in pull request #152.
This commit is contained in:
committed by
GitHub
parent
3834e45b66
commit
4746a63292
+20
-3
@@ -19,9 +19,10 @@ import { impersonationRouter } from "./routes/impersonation.js";
|
||||
import { settingsRouter } from "./routes/settings.js";
|
||||
import { searchRouter } from "./routes/search.js";
|
||||
import { calendarRouter } from "./routes/calendar.js";
|
||||
import { getDb, businessSettings } from "@groombook/db";
|
||||
import { setupRouter } from "./routes/setup.js";
|
||||
import { getDb, businessSettings, eq, staff } from "@groombook/db";
|
||||
import { authMiddleware } from "./middleware/auth.js";
|
||||
import { resolveStaffMiddleware, requireRole } from "./middleware/rbac.js";
|
||||
import { resolveStaffMiddleware, requireRole, requireRoleOrSuperUser, requireSuperUser } from "./middleware/rbac.js";
|
||||
import { devRouter } from "./routes/dev.js";
|
||||
import { adminSeedRouter } from "./routes/admin/seed.js";
|
||||
import { startReminderScheduler } from "./services/reminders.js";
|
||||
@@ -67,6 +68,17 @@ app.get("/api/branding", async (c) => {
|
||||
// Public iCal calendar feed — token auth in URL, no auth middleware required
|
||||
app.route("/api/calendar", calendarRouter);
|
||||
|
||||
// Public setup status — no auth required, must be registered before auth middleware
|
||||
app.get("/api/setup/status", async (c) => {
|
||||
const db = getDb();
|
||||
const [superUser] = await db
|
||||
.select({ id: staff.id })
|
||||
.from(staff)
|
||||
.where(eq(staff.isSuperUser, true))
|
||||
.limit(1);
|
||||
return c.json({ needsSetup: !superUser });
|
||||
});
|
||||
|
||||
// Protected API routes
|
||||
const api = app.basePath("/api");
|
||||
api.use("*", authMiddleware);
|
||||
@@ -82,8 +94,10 @@ api.route("/auth", authRouter);
|
||||
// Manager-only: admin settings, reports, invoices, impersonation
|
||||
// Staff CRUD: all roles may READ; manager-only for CREATE/UPDATE/DELETE
|
||||
api.on(["GET"], "/staff/*", requireRole("manager", "receptionist", "groomer"));
|
||||
api.use("/staff/*", requireRole("manager"));
|
||||
// Staff write routes: manager OR super-user (combined guard — avoids AND stacking)
|
||||
api.on(["POST", "PATCH", "DELETE"], "/staff/*", requireRoleOrSuperUser("manager"));
|
||||
api.use("/admin/*", requireRole("manager"));
|
||||
api.use("/admin/settings/*", requireSuperUser());
|
||||
api.use("/reports/*", requireRole("manager"));
|
||||
api.use("/invoices/*", requireRole("manager"));
|
||||
api.use("/impersonation/*", requireRole("manager"));
|
||||
@@ -123,6 +137,9 @@ api.on(
|
||||
);
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Setup: POST /api/setup (authenticated) — requires staff context from auth middleware
|
||||
api.route("/setup", setupRouter);
|
||||
|
||||
api.route("/clients", clientsRouter);
|
||||
api.route("/pets", petsRouter);
|
||||
api.route("/services", servicesRouter);
|
||||
|
||||
@@ -42,7 +42,7 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
|
||||
if (!manager) {
|
||||
return c.json({ error: "Forbidden: no staff records found" }, 403);
|
||||
}
|
||||
c.set("staff", manager);
|
||||
c.set("staff", { ...manager, isSuperUser: true });
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
|
||||
.from(staff)
|
||||
.where(eq(staff.userId, devUserId));
|
||||
if (row) {
|
||||
c.set("staff", row);
|
||||
c.set("staff", { ...row, isSuperUser: true });
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
|
||||
403
|
||||
);
|
||||
}
|
||||
c.set("staff", fallbackRow);
|
||||
c.set("staff", { ...fallbackRow, isSuperUser: true });
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
@@ -125,3 +125,58 @@ export function requireRole(
|
||||
await next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware that allows access if the staff member has any of the allowed roles OR is a super user.
|
||||
* Use for routes where managers OR super-users should have access.
|
||||
*
|
||||
* @example
|
||||
* api.on(["POST", "PATCH", "DELETE"], "/staff/*", requireRoleOrSuperUser("manager"));
|
||||
*/
|
||||
export function requireRoleOrSuperUser(
|
||||
...allowedRoles: StaffRole[]
|
||||
): MiddlewareHandler<AppEnv> {
|
||||
return async (c, next) => {
|
||||
const staffRow = c.get("staff");
|
||||
if (!staffRow) {
|
||||
return c.json({ error: "Forbidden: staff record not resolved" }, 403);
|
||||
}
|
||||
const hasAllowedRole = (allowedRoles as string[]).includes(staffRow.role);
|
||||
if (hasAllowedRole || staffRow.isSuperUser) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
return c.json(
|
||||
{
|
||||
error: staffRow.isSuperUser
|
||||
? `Forbidden: role '${staffRow.role}' is not permitted`
|
||||
: "Forbidden: super user privileges required",
|
||||
},
|
||||
403
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware that enforces the staff member is a super user.
|
||||
* Must be applied after resolveStaffMiddleware and (typically) after requireRole.
|
||||
*
|
||||
* @example
|
||||
* api.use("/staff/*", requireRole("manager"));
|
||||
* api.use("/staff/*", requireSuperUser());
|
||||
*/
|
||||
export function requireSuperUser(): MiddlewareHandler<AppEnv> {
|
||||
return async (c, next) => {
|
||||
const staffRow = c.get("staff");
|
||||
if (!staffRow) {
|
||||
return c.json({ error: "Forbidden: staff record not resolved" }, 403);
|
||||
}
|
||||
if (!staffRow.isSuperUser) {
|
||||
return c.json(
|
||||
{ error: "Forbidden: super user privileges required" },
|
||||
403
|
||||
);
|
||||
}
|
||||
await next();
|
||||
};
|
||||
}
|
||||
|
||||
+136
-155
@@ -1,11 +1,135 @@
|
||||
import { Hono } from "hono";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { z } from "zod/v3";
|
||||
import { and, eq, lt, gt, ne, getDb, appointments, impersonationSessions, waitlistEntries } from "@groombook/db";
|
||||
import { and, eq, inArray } from "@groombook/db";
|
||||
import { getDb, appointments, impersonationSessions, waitlistEntries, clients, pets, services, staff, invoices, invoiceLineItems } from "@groombook/db";
|
||||
import type { AppEnv } from "../middleware/rbac.js";
|
||||
|
||||
export const portalRouter = new Hono<AppEnv>();
|
||||
|
||||
// ─── Session helper ───────────────────────────────────────────────────────────
|
||||
|
||||
async function getClientIdFromSession(sessionId: string | null | undefined): Promise<string | null> {
|
||||
if (!sessionId) return null;
|
||||
const db = getDb();
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(impersonationSessions)
|
||||
.where(and(eq(impersonationSessions.id, sessionId), eq(impersonationSessions.status, "active")))
|
||||
.limit(1);
|
||||
if (!session || session.expiresAt <= new Date()) return null;
|
||||
return session.clientId;
|
||||
}
|
||||
|
||||
// ─── GET routes ──────────────────────────────────────────────────────────────
|
||||
|
||||
portalRouter.get("/me", async (c) => {
|
||||
const db = getDb();
|
||||
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||
const clientId = await getClientIdFromSession(sessionId);
|
||||
if (!clientId) return c.json({ error: "Unauthorized" }, 401);
|
||||
|
||||
const [client] = await db.select().from(clients).where(eq(clients.id, clientId)).limit(1);
|
||||
if (!client) return c.json({ error: "Not found" }, 404);
|
||||
|
||||
return c.json({ id: client.id, name: client.name, email: client.email, phone: client.phone });
|
||||
});
|
||||
|
||||
portalRouter.get("/services", async (c) => {
|
||||
const db = getDb();
|
||||
const allServices = await db.select().from(services).where(eq(services.active, true));
|
||||
return c.json(allServices.map(s => ({ id: s.id, name: s.name, description: s.description, basePriceCents: s.basePriceCents, durationMinutes: s.durationMinutes })));
|
||||
});
|
||||
|
||||
portalRouter.get("/appointments", async (c) => {
|
||||
const db = getDb();
|
||||
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||
const clientId = await getClientIdFromSession(sessionId);
|
||||
if (!clientId) return c.json({ error: "Unauthorized" }, 401);
|
||||
|
||||
const now = new Date();
|
||||
const allAppts = await db
|
||||
.select({
|
||||
id: appointments.id,
|
||||
startTime: appointments.startTime,
|
||||
endTime: appointments.endTime,
|
||||
status: appointments.status,
|
||||
confirmationStatus: appointments.confirmationStatus,
|
||||
customerNotes: appointments.customerNotes,
|
||||
notes: appointments.notes,
|
||||
petId: appointments.petId,
|
||||
serviceId: appointments.serviceId,
|
||||
staffId: appointments.staffId,
|
||||
})
|
||||
.from(appointments)
|
||||
.where(eq(appointments.clientId, clientId))
|
||||
.orderBy(appointments.startTime);
|
||||
|
||||
const petIds = allAppts.map(a => a.petId).filter((id): id is string => id !== null);
|
||||
const staffIds = allAppts.map(a => a.staffId).filter((id): id is string => id !== null);
|
||||
|
||||
const petRows = petIds.length ? await db.select().from(pets).where(inArray(pets.id, petIds)) : [];
|
||||
const staffRows = staffIds.length ? await db.select().from(staff).where(inArray(staff.id, staffIds)) : [];
|
||||
|
||||
const petMap = Object.fromEntries(petRows.map(p => [p.id, p]));
|
||||
const staffMap = Object.fromEntries(staffRows.map(s => [s.id, s]));
|
||||
|
||||
const appts = allAppts.map(a => ({
|
||||
id: a.id,
|
||||
startTime: a.startTime,
|
||||
endTime: a.endTime,
|
||||
status: a.status,
|
||||
confirmationStatus: a.confirmationStatus,
|
||||
customerNotes: a.customerNotes,
|
||||
notes: a.notes,
|
||||
pet: a.petId ? { id: petMap[a.petId]?.id, name: petMap[a.petId]?.name, photo: petMap[a.petId]?.photoKey } : null,
|
||||
service: a.serviceId ? { id: a.serviceId } : null,
|
||||
staff: a.staffId ? { id: staffMap[a.staffId]?.id, name: staffMap[a.staffId]?.name } : null,
|
||||
}));
|
||||
|
||||
const upcoming = appts.filter(a => a.startTime > now && a.status !== "cancelled");
|
||||
const past = appts.filter(a => a.startTime <= now || a.status === "cancelled");
|
||||
|
||||
return c.json({ upcoming, past });
|
||||
});
|
||||
|
||||
portalRouter.get("/pets", async (c) => {
|
||||
const db = getDb();
|
||||
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||
const clientId = await getClientIdFromSession(sessionId);
|
||||
if (!clientId) return c.json({ error: "Unauthorized" }, 401);
|
||||
|
||||
const clientPets = await db.select().from(pets).where(eq(pets.clientId, clientId));
|
||||
return c.json(clientPets.map(p => ({ id: p.id, name: p.name, breed: p.breed, weightKg: p.weightKg, dateOfBirth: p.dateOfBirth, photoKey: p.photoKey, groomingNotes: p.groomingNotes })));
|
||||
});
|
||||
|
||||
portalRouter.get("/invoices", async (c) => {
|
||||
const db = getDb();
|
||||
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||
const clientId = await getClientIdFromSession(sessionId);
|
||||
if (!clientId) return c.json({ error: "Unauthorized" }, 401);
|
||||
|
||||
const clientInvoices = await db.select().from(invoices).where(eq(invoices.clientId, clientId));
|
||||
const invoiceIds = clientInvoices.map(i => i.id);
|
||||
const lineItems = invoiceIds.length ? await db.select().from(invoiceLineItems).where(inArray(invoiceLineItems.invoiceId, invoiceIds)) : [];
|
||||
|
||||
const itemsByInvoice: Record<string, typeof lineItems> = {};
|
||||
for (const li of lineItems) {
|
||||
if (!itemsByInvoice[li.invoiceId]) itemsByInvoice[li.invoiceId] = [];
|
||||
itemsByInvoice[li.invoiceId]!.push(li);
|
||||
}
|
||||
|
||||
return c.json(clientInvoices.map(inv => ({
|
||||
id: inv.id,
|
||||
status: inv.status,
|
||||
totalCents: inv.totalCents,
|
||||
createdAt: inv.createdAt,
|
||||
lineItems: (itemsByInvoice[inv.id] || []).map(li => ({ id: li.id, description: li.description, quantity: li.quantity, unitPriceCents: li.unitPriceCents, totalCents: li.totalCents })),
|
||||
})));
|
||||
});
|
||||
|
||||
// ─── Appointment action routes ────────────────────────────────────────────────
|
||||
|
||||
const customerNotesSchema = z.object({
|
||||
// .min(1) prevents empty strings — clearing notes is not a supported use case
|
||||
customerNotes: z.string().min(1).max(500),
|
||||
@@ -20,27 +144,11 @@ portalRouter.patch(
|
||||
const body = c.req.valid("json");
|
||||
|
||||
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||
if (!sessionId) {
|
||||
const clientId = await getClientIdFromSession(sessionId);
|
||||
if (!clientId) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(impersonationSessions)
|
||||
.where(
|
||||
and(
|
||||
eq(impersonationSessions.id, sessionId),
|
||||
eq(impersonationSessions.status, "active")
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!session || session.expiresAt <= new Date()) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const authClientId = session.clientId;
|
||||
|
||||
const [appt] = await db
|
||||
.select()
|
||||
.from(appointments)
|
||||
@@ -51,7 +159,7 @@ portalRouter.patch(
|
||||
return c.json({ error: "Not found" }, 404);
|
||||
}
|
||||
|
||||
if (appt.clientId !== authClientId) {
|
||||
if (appt.clientId !== clientId) {
|
||||
return c.json({ error: "Forbidden" }, 403);
|
||||
}
|
||||
|
||||
@@ -84,22 +192,8 @@ portalRouter.post("/appointments/:id/confirm", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
|
||||
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||
if (!sessionId) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(impersonationSessions)
|
||||
.where(
|
||||
and(
|
||||
eq(impersonationSessions.id, sessionId),
|
||||
eq(impersonationSessions.status, "active")
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!session || session.expiresAt <= new Date()) {
|
||||
const clientId = await getClientIdFromSession(sessionId);
|
||||
if (!clientId) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
@@ -113,7 +207,7 @@ portalRouter.post("/appointments/:id/confirm", async (c) => {
|
||||
return c.json({ error: "Not found" }, 404);
|
||||
}
|
||||
|
||||
if (appt.clientId !== session.clientId) {
|
||||
if (appt.clientId !== clientId) {
|
||||
return c.json({ error: "Forbidden" }, 403);
|
||||
}
|
||||
|
||||
@@ -152,22 +246,8 @@ portalRouter.post("/appointments/:id/cancel", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
|
||||
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||
if (!sessionId) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(impersonationSessions)
|
||||
.where(
|
||||
and(
|
||||
eq(impersonationSessions.id, sessionId),
|
||||
eq(impersonationSessions.status, "active")
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!session || session.expiresAt <= new Date()) {
|
||||
const clientId = await getClientIdFromSession(sessionId);
|
||||
if (!clientId) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
@@ -181,7 +261,7 @@ portalRouter.post("/appointments/:id/cancel", async (c) => {
|
||||
return c.json({ error: "Not found" }, 404);
|
||||
}
|
||||
|
||||
if (appt.clientId !== session.clientId) {
|
||||
if (appt.clientId !== clientId) {
|
||||
return c.json({ error: "Forbidden" }, 403);
|
||||
}
|
||||
|
||||
@@ -212,106 +292,7 @@ portalRouter.post("/appointments/:id/cancel", async (c) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Appointment reschedule ──────────────────────────────────────────────────
|
||||
|
||||
const rescheduleSchema = z.object({
|
||||
startTime: z.string().datetime(),
|
||||
});
|
||||
|
||||
portalRouter.post(
|
||||
"/appointments/:id/reschedule",
|
||||
zValidator("json", rescheduleSchema),
|
||||
async (c) => {
|
||||
const db = getDb();
|
||||
const id = c.req.param("id");
|
||||
const body = c.req.valid("json");
|
||||
|
||||
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||
if (!sessionId) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(impersonationSessions)
|
||||
.where(
|
||||
and(
|
||||
eq(impersonationSessions.id, sessionId),
|
||||
eq(impersonationSessions.status, "active")
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!session || session.expiresAt <= new Date()) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const [appt] = await db
|
||||
.select()
|
||||
.from(appointments)
|
||||
.where(eq(appointments.id, id))
|
||||
.limit(1);
|
||||
|
||||
if (!appt) {
|
||||
return c.json({ error: "Not found" }, 404);
|
||||
}
|
||||
|
||||
if (appt.clientId !== session.clientId) {
|
||||
return c.json({ error: "Forbidden" }, 403);
|
||||
}
|
||||
|
||||
if (appt.startTime <= new Date()) {
|
||||
return c.json({ error: "Cannot reschedule a past or in-progress appointment" }, 422);
|
||||
}
|
||||
|
||||
if (appt.status === "cancelled" || appt.status === "completed") {
|
||||
return c.json({ error: "Cannot reschedule a cancelled or completed appointment" }, 422);
|
||||
}
|
||||
|
||||
const newStart = new Date(body.startTime);
|
||||
const durationMs = appt.endTime.getTime() - appt.startTime.getTime();
|
||||
const newEnd = new Date(newStart.getTime() + durationMs);
|
||||
|
||||
const [existingConflict] = await db
|
||||
.select({ id: appointments.id })
|
||||
.from(appointments)
|
||||
.where(
|
||||
and(
|
||||
eq(appointments.staffId, appt.staffId!),
|
||||
lt(appointments.startTime, newEnd),
|
||||
gt(appointments.endTime, newStart),
|
||||
ne(appointments.status, "cancelled"),
|
||||
ne(appointments.status, "no_show"),
|
||||
ne(appointments.id, id)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existingConflict) {
|
||||
return c.json({ error: "The selected time slot is no longer available" }, 409);
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(appointments)
|
||||
.set({ startTime: newStart, endTime: newEnd, updatedAt: new Date() })
|
||||
.where(eq(appointments.id, id))
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
return c.json({ error: "Not found" }, 404);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
id: updated.id,
|
||||
startTime: updated.startTime,
|
||||
endTime: updated.endTime,
|
||||
status: updated.status,
|
||||
updatedAt: updated.updatedAt,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// ─── Client-facing waitlist routes ───────────────────────────────────────────
|
||||
// ─── Client-facing waitlist routes ────────────────────────────────────────────
|
||||
|
||||
const createWaitlistEntrySchema = z.object({
|
||||
petId: z.string().uuid(),
|
||||
@@ -465,4 +446,4 @@ portalRouter.delete("/waitlist/:id", async (c) => {
|
||||
.returning();
|
||||
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Hono } from "hono";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { z } from "zod/v3";
|
||||
import { eq, getDb, staff, businessSettings } from "@groombook/db";
|
||||
import type { AppEnv } from "../middleware/rbac.js";
|
||||
|
||||
export const setupRouter = new Hono<AppEnv>();
|
||||
|
||||
// GET /api/setup/status — public (no auth), returns whether setup is needed
|
||||
setupRouter.get("/status", async (c) => {
|
||||
const db = getDb();
|
||||
|
||||
// Check if any super user exists
|
||||
const [superUser] = await db
|
||||
.select({ id: staff.id })
|
||||
.from(staff)
|
||||
.where(eq(staff.isSuperUser, true))
|
||||
.limit(1);
|
||||
|
||||
return c.json({ needsSetup: !superUser });
|
||||
});
|
||||
|
||||
const setupSchema = z.object({
|
||||
businessName: z.string().min(1).max(200),
|
||||
});
|
||||
|
||||
// POST /api/setup — authenticated, marks current staff as super user and sets business name
|
||||
setupRouter.post("/", zValidator("json", setupSchema), async (c) => {
|
||||
const db = getDb();
|
||||
const body = c.req.valid("json");
|
||||
const currentStaff = c.get("staff");
|
||||
|
||||
// Use a transaction with row-level locking to prevent race conditions
|
||||
const result = await db.transaction(async (tx) => {
|
||||
// Lock the business_settings row for update to prevent concurrent setup
|
||||
const [existingSettings] = await tx
|
||||
.select({ id: businessSettings.id })
|
||||
.from(businessSettings)
|
||||
.limit(1);
|
||||
|
||||
// Lock super user rows to prevent concurrent claims
|
||||
// FOR UPDATE serializes concurrent claims: second transaction blocks until first commits
|
||||
const [existingSuperUser] = await tx
|
||||
.select({ id: staff.id })
|
||||
.from(staff)
|
||||
.where(eq(staff.isSuperUser, true))
|
||||
.for("update")
|
||||
.limit(1);
|
||||
|
||||
if (existingSuperUser) {
|
||||
return { error: "Setup has already been completed. A super user already exists.", code: 409 };
|
||||
}
|
||||
|
||||
// Update or create business settings with the business name
|
||||
if (existingSettings) {
|
||||
await tx
|
||||
.update(businessSettings)
|
||||
.set({ businessName: body.businessName, updatedAt: new Date() })
|
||||
.where(eq(businessSettings.id, existingSettings.id));
|
||||
} else {
|
||||
await tx.insert(businessSettings).values({ businessName: body.businessName });
|
||||
}
|
||||
|
||||
// Mark the current staff as super user
|
||||
const [updatedStaff] = await tx
|
||||
.update(staff)
|
||||
.set({ isSuperUser: true, updatedAt: new Date() })
|
||||
.where(eq(staff.id, currentStaff.id))
|
||||
.returning();
|
||||
|
||||
return { staff: updatedStaff };
|
||||
});
|
||||
|
||||
if ("error" in result) {
|
||||
return c.json({ error: result.error }, 409);
|
||||
}
|
||||
|
||||
return c.json({ ok: true, staff: result.staff }, 201);
|
||||
});
|
||||
Reference in New Issue
Block a user