Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b9e82adff | |||
| b796d36aed |
@@ -2,9 +2,9 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, dev, uat]
|
branches: [main, dev]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, dev, uat]
|
branches: [main, dev]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
ref:
|
ref:
|
||||||
|
|||||||
+1
-131
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { z } from "zod/v3";
|
import { z } from "zod/v3";
|
||||||
import { and, desc, eq, exists, getDb, gte, groomingVisitLogs, or, pets, appointments, staff, services, sql } from "../db/index.js";
|
import { and, eq, exists, getDb, or, pets, appointments } from "../db/index.js";
|
||||||
import type { AppEnv } from "../middleware/rbac.js";
|
import type { AppEnv } from "../middleware/rbac.js";
|
||||||
import {
|
import {
|
||||||
getPresignedUploadUrl,
|
getPresignedUploadUrl,
|
||||||
@@ -283,133 +283,3 @@ petsRouter.get("/:petId/photo", async (c) => {
|
|||||||
const url = await getPresignedGetUrl(pet.photoKey);
|
const url = await getPresignedGetUrl(pet.photoKey);
|
||||||
return c.json({ url, photoKey: pet.photoKey, photoUploadedAt: pet.photoUploadedAt });
|
return c.json({ url, photoKey: pet.photoKey, photoUploadedAt: pet.photoUploadedAt });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Profile Summary ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
async function groomerLinkageCheck(
|
|
||||||
db: ReturnType<typeof getDb>,
|
|
||||||
clientId: string,
|
|
||||||
staffRow: NonNullable<AppEnv["Variables"]["staff"]>
|
|
||||||
): Promise<boolean> {
|
|
||||||
const [linkage] = await db
|
|
||||||
.select({ id: appointments.id })
|
|
||||||
.from(appointments)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(appointments.clientId, clientId),
|
|
||||||
or(
|
|
||||||
eq(appointments.staffId, staffRow.id),
|
|
||||||
eq(appointments.batherStaffId, staffRow.id)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
return !!linkage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /:id/profile-summary
|
|
||||||
* Returns aggregated profile: basic pet fields + grooming history + visit stats + upcoming appointment.
|
|
||||||
* Groomer RBAC: same visibility rules as GET /:id.
|
|
||||||
*/
|
|
||||||
petsRouter.get("/:id/profile-summary", async (c) => {
|
|
||||||
const db = getDb();
|
|
||||||
const petId = c.req.param("id");
|
|
||||||
const staffRow = c.get("staff");
|
|
||||||
const isGroomer = staffRow?.role === "groomer";
|
|
||||||
|
|
||||||
const [row] = await db.select().from(pets).where(eq(pets.id, petId));
|
|
||||||
if (!row) return c.json({ error: "Not found" }, 404);
|
|
||||||
|
|
||||||
if (isGroomer) {
|
|
||||||
const hasLinkage = await groomerLinkageCheck(db, row.clientId, staffRow);
|
|
||||||
if (!hasLinkage) return c.json({ error: "Forbidden" }, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recent grooming history: last 10, with staff name join
|
|
||||||
const historyRows = await db
|
|
||||||
.select({
|
|
||||||
id: groomingVisitLogs.id,
|
|
||||||
petId: groomingVisitLogs.petId,
|
|
||||||
appointmentId: groomingVisitLogs.appointmentId,
|
|
||||||
staffId: groomingVisitLogs.staffId,
|
|
||||||
staffName: staff.name,
|
|
||||||
cutStyle: groomingVisitLogs.cutStyle,
|
|
||||||
productsUsed: groomingVisitLogs.productsUsed,
|
|
||||||
notes: groomingVisitLogs.notes,
|
|
||||||
groomedAt: groomingVisitLogs.groomedAt,
|
|
||||||
createdAt: groomingVisitLogs.createdAt,
|
|
||||||
})
|
|
||||||
.from(groomingVisitLogs)
|
|
||||||
.leftJoin(staff, eq(staff.id, groomingVisitLogs.staffId))
|
|
||||||
.where(eq(groomingVisitLogs.petId, petId))
|
|
||||||
.orderBy(desc(groomingVisitLogs.groomedAt))
|
|
||||||
.limit(10);
|
|
||||||
|
|
||||||
const recentGroomingHistory = historyRows.map((r) => ({
|
|
||||||
id: r.id,
|
|
||||||
petId: r.petId,
|
|
||||||
appointmentId: r.appointmentId,
|
|
||||||
staffId: r.staffId,
|
|
||||||
staffName: r.staffName,
|
|
||||||
cutStyle: r.cutStyle,
|
|
||||||
productsUsed: r.productsUsed,
|
|
||||||
notes: r.notes,
|
|
||||||
groomedAt: r.groomedAt?.toISOString() ?? null,
|
|
||||||
createdAt: r.createdAt?.toISOString() ?? null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const lastVisitDate = historyRows[0]?.groomedAt?.toISOString() ?? null;
|
|
||||||
|
|
||||||
// Completed appointment count for this pet
|
|
||||||
const [{ count: visitCount }] = await db
|
|
||||||
.select({ count: sql<number>`count(*)::int` })
|
|
||||||
.from(appointments)
|
|
||||||
.where(and(eq(appointments.petId, petId), eq(appointments.status, "completed")));
|
|
||||||
|
|
||||||
// Upcoming appointment: next scheduled or confirmed
|
|
||||||
const [nextAppt] = await db
|
|
||||||
.select({
|
|
||||||
id: appointments.id,
|
|
||||||
serviceId: appointments.serviceId,
|
|
||||||
staffId: appointments.staffId,
|
|
||||||
startTime: appointments.startTime,
|
|
||||||
endTime: appointments.endTime,
|
|
||||||
status: appointments.status,
|
|
||||||
serviceName: services.name,
|
|
||||||
staffName: staff.name,
|
|
||||||
})
|
|
||||||
.from(appointments)
|
|
||||||
.leftJoin(services, eq(services.id, appointments.serviceId))
|
|
||||||
.leftJoin(staff, eq(staff.id, appointments.staffId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(appointments.petId, petId),
|
|
||||||
or(eq(appointments.status, "scheduled"), eq(appointments.status, "confirmed")),
|
|
||||||
gte(appointments.startTime, new Date())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.orderBy(appointments.startTime)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const upcomingAppointment = nextAppt
|
|
||||||
? {
|
|
||||||
id: nextAppt.id,
|
|
||||||
serviceId: nextAppt.serviceId,
|
|
||||||
serviceName: nextAppt.serviceName,
|
|
||||||
staffId: nextAppt.staffId,
|
|
||||||
staffName: nextAppt.staffName,
|
|
||||||
startTime: nextAppt.startTime?.toISOString() ?? null,
|
|
||||||
endTime: nextAppt.endTime?.toISOString() ?? null,
|
|
||||||
status: nextAppt.status,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return c.json({
|
|
||||||
...row,
|
|
||||||
recentGroomingHistory,
|
|
||||||
lastVisitDate,
|
|
||||||
visitCount,
|
|
||||||
upcomingAppointment,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
Generated
-13
@@ -970,79 +970,66 @@ packages:
|
|||||||
resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==}
|
resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.60.3':
|
'@rollup/rollup-linux-arm-musleabihf@4.60.3':
|
||||||
resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==}
|
resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.60.3':
|
'@rollup/rollup-linux-arm64-gnu@4.60.3':
|
||||||
resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==}
|
resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.60.3':
|
'@rollup/rollup-linux-arm64-musl@4.60.3':
|
||||||
resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==}
|
resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-gnu@4.60.3':
|
'@rollup/rollup-linux-loong64-gnu@4.60.3':
|
||||||
resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==}
|
resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-musl@4.60.3':
|
'@rollup/rollup-linux-loong64-musl@4.60.3':
|
||||||
resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==}
|
resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-gnu@4.60.3':
|
'@rollup/rollup-linux-ppc64-gnu@4.60.3':
|
||||||
resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==}
|
resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-musl@4.60.3':
|
'@rollup/rollup-linux-ppc64-musl@4.60.3':
|
||||||
resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==}
|
resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.60.3':
|
'@rollup/rollup-linux-riscv64-gnu@4.60.3':
|
||||||
resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==}
|
resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.60.3':
|
'@rollup/rollup-linux-riscv64-musl@4.60.3':
|
||||||
resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==}
|
resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.60.3':
|
'@rollup/rollup-linux-s390x-gnu@4.60.3':
|
||||||
resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==}
|
resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.60.3':
|
'@rollup/rollup-linux-x64-gnu@4.60.3':
|
||||||
resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==}
|
resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.60.3':
|
'@rollup/rollup-linux-x64-musl@4.60.3':
|
||||||
resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==}
|
resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-openbsd-x64@4.60.3':
|
'@rollup/rollup-openbsd-x64@4.60.3':
|
||||||
resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==}
|
resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
|
|||||||
c,
|
c,
|
||||||
next
|
next
|
||||||
) => {
|
) => {
|
||||||
// Better-Auth's own routes handle their own auth — skip staff resolution
|
// Better-Auth\'s own routes handle their own auth — skip staff resolution
|
||||||
// OOBE setup routes also handle their own auth — staff record is created during setup
|
// OOBE setup routes also handle their own auth — staff record is created during setup
|
||||||
if (c.req.path.startsWith("/api/auth/") || c.req.path.startsWith("/api/setup")) {
|
if (c.req.path.startsWith("/api/auth/") || c.req.path.startsWith("/api/setup")) {
|
||||||
await next();
|
await next();
|
||||||
@@ -120,7 +120,7 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(account.userId, jwt.sub),
|
eq(account.userId, jwt.sub),
|
||||||
sql`${account.providerId} IN ('authentik', 'google', 'github')`
|
sql`${account.providerId} IN (\'authentik\', \'google\', \'github\')`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
@@ -180,7 +180,7 @@ export function requireRole(
|
|||||||
if (!(allowedRoles as string[]).includes(staffRow.role)) {
|
if (!(allowedRoles as string[]).includes(staffRow.role)) {
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
error: `Forbidden: role '${staffRow.role}' is not permitted to access this resource`,
|
error: `Forbidden: role \'${staffRow.role}\' is not permitted to access this resource`,
|
||||||
},
|
},
|
||||||
403
|
403
|
||||||
);
|
);
|
||||||
@@ -213,7 +213,7 @@ export function requireRoleOrSuperUser(
|
|||||||
{
|
{
|
||||||
error: hasAllowedRole
|
error: hasAllowedRole
|
||||||
? "Forbidden: super user privileges required"
|
? "Forbidden: super user privileges required"
|
||||||
: `Forbidden: role '${staffRow.role}' is not permitted`,
|
: `Forbidden: role \'${staffRow.role}\' is not permitted`,
|
||||||
},
|
},
|
||||||
403
|
403
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user