Compare commits

..

11 Commits

Author SHA1 Message Date
Chris Farhood 1674a7df4a fix(GRO-1272): update rbac tests and UAT playbook for auto-provision
CI / Lint & Typecheck (pull_request) Failing after 13s
CI / Test (pull_request) Failing after 20s
CI / Build (pull_request) Has been skipped
CI / Build & Push Docker Images (pull_request) Has been skipped
CI / Update Infra Image Tags (pull_request) Has been skipped
- Add user table mock and db.insert returning chain to rbac.test.ts
- Add three new tests: happy-path auto-provision, email-prefix fallback,
  and miss-path (no user → 403)
- Add TC-API-1.4 to UAT_PLAYBOOK.md §4.1 for first-login auto-provision

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 13:03:46 +00:00
Chris Farhood 09187ca277 fix(GRO-1272): auto-provision staff record on first OIDC login
When a user authenticates via OIDC but has no staff record (userId NULL,
oidcSub mismatch, email mismatch), resolveStaffMiddleware now checks for
a Better-Auth user record by jwt.sub and auto-creates a minimal groomer
staff record on first login.

This fixes the UAT regression where all API routes returned 403 for all
authenticated users after GRO-1207, because seedKnownUsers() sets
oidcSub to Authentik integer PKs or emails rather than the actual Authentik
OIDC sub (a UUID). The auto-provision path bridges the gap for all UAT
personas without requiring seed/Terraform changes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-14 19:03:09 +00:00
groombook-engineer[bot] 2c928ca4d7 fix(gro-1261): correct infra paths in CI Update Infra Image Tags job (#16)
The CI workflow referenced wrong paths in groombook/infra:
- apps/groombook/overlays/dev/ → apps/overlays/dev/
- apps/groombook/base/ → apps/base/

These paths don't exist in groombook/infra — the correct structure
is apps/overlays/dev/ and apps/base/.

Co-authored-by: Chris Farhood <chris@farhood.org>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-14 17:29:06 +00:00
the-dogfather-cto[bot] af75fecb66 Merge pull request #14 from groombook/flea-flicker/gro-1231-pnpm-workspace-dockerfile
fix(docker): add missing pnpm-workspace.yaml COPY in deps and runner stages (GRO-1231)
2026-05-14 17:10:25 +00:00
Chris Farhood 2d4df6fe1e fix(docker): add missing pnpm-workspace.yaml COPY in deps and runner stages
Without pnpm-workspace.yaml, pnpm install --frozen-lockfile can't discover
the apps/api workspace member, causing "Already up to date" and tsc not found.

Also removes stale packages/* entry from pnpm-workspace.yaml (no packages/
directory exists in the dev branch).

Fixes: GRO-1231

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-14 16:50:52 +00:00
the-dogfather-cto[bot] db10320c8f fix(auth): override Better Auth sign-in rate limit defaults (#11)
fix(auth): override Better Auth sign-in rate limit defaults
2026-05-14 10:52:31 +00:00
Chris Farhood 40a4023c65 feat(GRO-1202): add sign-in/sign-up rate limit overrides
Port rate limit customRules from groombook/app PR #392 to groombook/api.
Adds per-route limits for /sign-in/social, /sign-in/email, and /sign-up/email
to both AUTH_DISABLED and production better-auth() instances.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-14 10:34:32 +00:00
groombook-engineer[bot] d598511b75 fix: resolve pre-existing TypeScript errors for CI compliance (#9)
Merge PR #9: fix pre-existing TypeScript errors for CI compliance

All Lint & Typecheck and Test checks pass. Ready to merge.

cc @cpfarhood
2026-05-14 07:50:28 +00:00
the-dogfather-cto[bot] e714200b71 Merge pull request #7 from groombook/fix/uat-tester-oidc-sub
fix(api): add UAT Tester staff creation in seed script
2026-05-12 21:57:44 +00:00
Chris Farhood 1e70e01046 fix(api): add UAT Tester staff creation in seed script
Adds dedicated SEED_UAT_TESTER_OIDC_SUB handling to create the
uat-tester staff record with proper oidcSub mapping to Authentik user PK 237.

Fixes GRO-1151
2026-05-12 21:44:42 +00:00
the-dogfather-cto[bot] 83d7fecdd3 fix: correct test mock paths from "./db" to "../db" (#5)
fix: correct test mock paths from "./db" to "../db"
2026-05-12 21:33:02 +00:00
35 changed files with 194 additions and 65 deletions
+4 -4
View File
@@ -202,20 +202,20 @@ jobs:
echo "Updating dev overlay image tags to: $TAG"
echo "Updating migration/seed Job names with SHA: $SHORT_SHA"
cd /tmp/infra
DEV_KUST="apps/groombook/overlays/dev/kustomization.yaml"
DEV_KUST="apps/overlays/dev/kustomization.yaml"
yq -i '(.images[] | select(.name == "ghcr.io/groombook/api")).newTag = env(TAG)' "$DEV_KUST"
yq -i '(.images[] | select(.name == "ghcr.io/groombook/migrate")).newTag = env(TAG)' "$DEV_KUST"
yq -i '(.images[] | select(.name == "ghcr.io/groombook/seed")).newTag = env(TAG)' "$DEV_KUST"
yq -i '(.images[] | select(.name == "ghcr.io/groombook/reset")).newTag = env(TAG)' "$DEV_KUST"
MIGRATE_JOB="apps/groombook/base/migrate-job.yaml"
MIGRATE_JOB="apps/base/migrate-job.yaml"
if [ -f "$MIGRATE_JOB" ]; then
yq -i '.metadata.name = "migrate-schema-" + env(SHORT_SHA)' "$MIGRATE_JOB"
yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$MIGRATE_JOB"
yq -i '.spec.ttlSecondsAfterFinished = (.spec.ttlSecondsAfterFinished // 86400)' "$MIGRATE_JOB"
fi
SEED_JOB="apps/groombook/base/seed-job.yaml"
SEED_JOB="apps/base/seed-job.yaml"
if [ -f "$SEED_JOB" ]; then
yq -i '.metadata.name = "seed-test-data-" + env(SHORT_SHA)' "$SEED_JOB"
yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$SEED_JOB"
@@ -237,7 +237,7 @@ jobs:
git config user.name "groombook-engineer[bot]"
git config user.email "3141748+groombook-engineer[bot]@users.noreply.github.com"
git checkout -b "chore/update-image-tags-${TAG}"
git add apps/groombook/overlays/dev/ apps/groombook/base/migrate-job.yaml apps/groombook/base/seed-job.yaml
git add apps/overlays/dev/ apps/base/migrate-job.yaml apps/base/seed-job.yaml
git commit -m "chore: update image tags and migration/seed Job names to ${TAG}"
git push -u origin "chore/update-image-tags-${TAG}"
+2 -2
View File
@@ -3,7 +3,7 @@ RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
WORKDIR /app
FROM base AS deps
COPY package.json pnpm-lock.yaml ./
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/api/package.json apps/api/
RUN pnpm install --frozen-lockfile
@@ -17,7 +17,7 @@ RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
WORKDIR /app
ENV NODE_ENV=production
COPY package.json pnpm-lock.yaml ./
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY --from=builder /app/apps/api/package.json apps/api/
COPY --from=builder /app/apps/api/dist apps/api/dist
RUN pnpm install --frozen-lockfile --prod
+1
View File
@@ -28,6 +28,7 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet
| TC-API-1.1 | Login via OIDC | POST to OIDC provider callback, verify JWT token issued | 200 OK, JWT returned with valid claims |
| TC-API-1.2 | Session persistence | Make authenticated request, verify session token valid | 200 OK, request succeeds |
| TC-API-1.3 | Logout | Call logout endpoint, verify token invalidated | 200 OK, subsequent requests return 401 |
| TC-API-1.4 | Auto-provision on first OIDC login | First login as a Better-Auth user with no existing staff record | 200 OK, access granted; groomer staff record auto-created with name/email from user table |
### 4.2 Client Management
+99 -21
View File
@@ -45,40 +45,72 @@ const GROOMER: StaffRow = {
let staffLookupResult: StaffRow | null = null;
let managerFallbackResult: StaffRow | null = MANAGER;
let userLookupResult: { id: string; name: string | null; email: string | null } | null = null;
let insertedStaff: StaffRow | null = null;
vi.mock("../db", () => {
const staff = new Proxy(
{ _name: "staff" },
{
get(target, prop) {
if (prop === "_name") return "staff";
if (prop === "$inferSelect") return {};
return { table: "staff", column: prop };
const makeTableProxy = (name: string) =>
new Proxy(
{ _name: name },
{
get(target, prop) {
if (prop === "_name") return name;
if (prop === "$inferSelect") return {};
return { table: name, column: prop };
},
}
);
const staff = makeTableProxy("staff");
const user = makeTableProxy("user");
const buildQuery = (result: unknown, fallback: unknown) => ({
limit: () => ({
[Symbol.iterator]: function* () {
if (result) yield result;
},
}
);
0: result,
length: result ? 1 : 0,
}),
});
return {
getDb: () => ({
select: () => ({
from: () => ({
where: () => ({
limit: () => {
// dev mode fallback to first manager
return managerFallbackResult ? [managerFallbackResult] : [];
},
[Symbol.iterator]: function* () {
if (staffLookupResult) yield staffLookupResult;
},
0: staffLookupResult,
length: staffLookupResult ? 1 : 0,
}),
from: (table: unknown) => ({
where: () => buildQuery(
table === staff ? staffLookupResult : userLookupResult,
table === staff ? managerFallbackResult : null
),
}),
}),
insert: (table: unknown) => ({
values: (vals: Record<string, unknown>) => ({
returning: () => {
const newStaff: StaffRow = {
id: "new-staff-id",
oidcSub: null,
userId: vals.userId as string,
role: vals.role as StaffRow["role"],
isSuperUser: false,
name: vals.name as string,
email: vals.email as string,
active: true,
icalToken: null,
createdAt: new Date(),
updatedAt: new Date(),
};
insertedStaff = newStaff;
return [newStaff];
},
}),
}),
}),
staff,
user,
eq: vi.fn((_col: unknown, _val: unknown) => ({ col: _col, val: _val })),
and: vi.fn((..._clauses: unknown[]) => ({})),
sql: vi.fn((..._args: unknown[]) => ({})),
};
});
@@ -87,6 +119,8 @@ vi.mock("../db", () => {
function resetMocks() {
staffLookupResult = null;
managerFallbackResult = MANAGER;
userLookupResult = null;
insertedStaff = null;
}
/** Build a minimal Hono app with jwtPayload pre-set, then apply a middleware. */
@@ -202,6 +236,50 @@ describe("resolveStaffMiddleware", () => {
const body = await res.json();
expect(body.error).toMatch(/no staff records found/i);
});
it("auto-provision: creates groomer staff record on first login when Better-Auth user exists", async () => {
staffLookupResult = null;
userLookupResult = { id: "ba-user-new", name: "New User", email: "newuser@example.com" };
let capturedStaff: StaffRow | null = null;
const app = buildApp(resolveStaffMiddleware, (c) => {
capturedStaff = c.get("staff");
return c.json({ ok: true });
});
const res = await app.request("/test");
expect(res.status).toBe(200);
expect(capturedStaff).not.toBeNull();
expect(capturedStaff!.role).toBe("groomer");
expect(capturedStaff!.userId).toBe("ba-user-new");
expect(capturedStaff!.name).toBe("New User");
expect(capturedStaff!.email).toBe("newuser@example.com");
expect(capturedStaff!.isSuperUser).toBe(false);
});
it("auto-provision: falls back to email prefix when user has no name", async () => {
staffLookupResult = null;
userLookupResult = { id: "ba-user-noname", name: null, email: "firstlogin@example.com" };
let capturedStaff: StaffRow | null = null;
const app = buildApp(resolveStaffMiddleware, (c) => {
capturedStaff = c.get("staff");
return c.json({ ok: true });
});
const res = await app.request("/test");
expect(res.status).toBe(200);
expect(capturedStaff!.name).toBe("firstlogin");
});
it("auto-provision: returns 403 when no staff record and no Better-Auth user exists", async () => {
staffLookupResult = null;
userLookupResult = null;
const app = buildApp(resolveStaffMiddleware);
const res = await app.request("/test");
expect(res.status).toBe(403);
const body = await res.json();
expect(body.error).toMatch(/no staff record found for authenticated user/i);
});
});
// ─── requireRole tests ────────────────────────────────────────────────────────
+27 -6
View File
@@ -94,11 +94,6 @@ function pick<T>(arr: T[]): T {
return arr[Math.floor(rand() * arr.length)]!;
}
/** Return n distinct random elements from an array. */
function pickN<T>(arr: T[], n: number): T[] {
const shuffled = [...arr].sort(() => rand() - 0.5);
return shuffled.slice(0, n);
}
function randInt(min: number, max: number): number {
return Math.floor(rand() * (max - min + 1)) + min;
@@ -459,6 +454,32 @@ async function seedKnownUsers() {
}
}
// ── Staff: UAT Tester (oidcSub from SEED_UAT_TESTER_OIDC_SUB env var) ──
const uatTesterOidcSub = process.env.SEED_UAT_TESTER_OIDC_SUB;
if (uatTesterOidcSub) {
const UAT_TESTER_STAFF_ID = "00000000-0000-0000-0000-000000000007";
const [existingUatTester] = await db
.select()
.from(schema.staff)
.where(eq(schema.staff.email, "uat-tester@groombook.dev"))
.limit(1);
if (existingUatTester) {
console.log(`✓ Staff 'UAT Tester' already exists — skipping`);
} else {
await db.insert(schema.staff).values({
id: UAT_TESTER_STAFF_ID,
name: "UAT Tester",
email: "uat-tester@groombook.dev",
oidcSub: uatTesterOidcSub,
role: "groomer",
isSuperUser: false,
active: true,
});
console.log(`✓ Created staff 'UAT Tester' (oidcSub: ${uatTesterOidcSub})`);
}
}
// ── Staff: UAT Groomer Personas (SEED_UAT_GROOMER_EMAILS + SEED_UAT_GROOMER_NAMES) ──
const groomerEmails = process.env.SEED_UAT_GROOMER_EMAILS?.split(",").map((e) => e.trim()).filter(Boolean) ?? [];
const groomerNames = process.env.SEED_UAT_GROOMER_NAMES?.split(",").map((n) => n.trim()).filter(Boolean) ?? [];
@@ -1079,7 +1100,7 @@ async function seed() {
const groomer = pick(groomers);
const bather = bathers.length > 0 && rand() < 0.6 ? pick(bathers) : null;
let startTime = randDate(appointmentsBackDate, now);
const startTime = randDate(appointmentsBackDate, now);
startTime.setHours(randInt(8, 16), pick([0, 15, 30, 45]), 0, 0);
const endTime = new Date(startTime.getTime() + svc.dur * 60 * 1000);
const effectivePrice = svc.price;
+1 -1
View File
@@ -22,7 +22,7 @@ import { searchRouter } from "./routes/search.js";
import { getObject } from "./lib/s3.js";
import { calendarRouter } from "./routes/calendar.js";
import { setupRouter } from "./routes/setup.js";
import { getDb, businessSettings, eq, staff } from "./db";
import { getDb, businessSettings, eq, staff } from "./db/index.js";
import { authMiddleware } from "./middleware/auth.js";
import { resolveStaffMiddleware, requireRole, requireRoleOrSuperUser, requireSuperUser } from "./middleware/rbac.js";
import { devRouter } from "./routes/dev.js";
+8 -2
View File
@@ -1,8 +1,8 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { genericOAuth } from "better-auth/plugins";
import { getDb, authProviderConfig, eq } from "./db";
import { decryptSecret } from "./db";
import { getDb, authProviderConfig, eq } from "../db/index.js";
import { decryptSecret } from "../db/index.js";
import { sendEmail } from "../services/email.js";
const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET;
@@ -97,6 +97,9 @@ export async function initAuth(): Promise<void> {
window: 10,
storage: "memory",
customRules: {
"/sign-in/social": { max: 10, window: 60 },
"/sign-in/email": { max: 10, window: 60 },
"/sign-up/email": { max: 5, window: 60 },
"/get-session": false,
},
},
@@ -247,6 +250,9 @@ export async function initAuth(): Promise<void> {
window: 10,
storage: "memory",
customRules: {
"/sign-in/social": { max: 10, window: 60 },
"/sign-in/email": { max: 10, window: 60 },
"/sign-up/email": { max: 5, window: 60 },
"/get-session": false,
},
},
+1 -1
View File
@@ -1,5 +1,5 @@
import type { MiddlewareHandler } from "hono";
import { getDb, impersonationAuditLogs } from "../db";
import { getDb, impersonationAuditLogs } from "../db/index.js";
import type { PortalEnv } from "./portalSession.js";
/**
+1 -1
View File
@@ -1,5 +1,5 @@
import type { MiddlewareHandler } from "hono";
import { and, eq, getDb, impersonationSessions } from "../db";
import { and, eq, getDb, impersonationSessions } from "../db/index.js";
export interface PortalEnv {
Variables: {
+25 -1
View File
@@ -1,5 +1,5 @@
import type { MiddlewareHandler } from "hono";
import { and, eq, getDb, sql, staff } from "../db";
import { and, eq, getDb, sql, staff, user } from "../db/index.js";
export type StaffRole = "groomer" | "receptionist" | "manager";
export type StaffRow = typeof staff.$inferSelect;
@@ -110,6 +110,30 @@ export const resolveStaffMiddleware: MiddlewareHandler<AppEnv> = async (
return;
}
}
// Auto-provision: no staff record exists for this user at all, but a valid
// Better-Auth user session exists (jwt.sub = user.id from user table).
// Create a minimal groomer staff record on first login.
const [userRow] = await db
.select({ id: user.id, name: user.name, email: user.email })
.from(user)
.where(eq(user.id, jwt.sub))
.limit(1);
if (userRow) {
const [newStaff] = await db
.insert(staff)
.values({
name: userRow.name ?? jwt.email?.split("@")[0] ?? "Unknown",
email: userRow.email ?? jwt.email ?? "",
userId: jwt.sub,
role: "groomer",
isSuperUser: false,
active: true,
})
.returning();
c.set("staff", newStaff);
await next();
return;
}
return c.json(
{ error: "Forbidden: no staff record found for authenticated user" },
403
+1 -1
View File
@@ -10,7 +10,7 @@
*/
import { Hono } from "hono";
import { eq, getDb, staff, clients, pets, services } from "./db";
import { eq, getDb, staff, clients, pets, services } from "../../db/index.js";
export const adminSeedRouter = new Hono();
+1 -1
View File
@@ -15,7 +15,7 @@ import {
pets,
services,
staff,
} from "../db";
} from "../db/index.js";
import type { AppEnv } from "../middleware/rbac.js";
export const appointmentGroupsRouter = new Hono<AppEnv>();
+1 -1
View File
@@ -18,7 +18,7 @@ import {
reminderLogs,
services,
staff,
} from "../db";
} from "../db/index.js";
import { buildConfirmationEmail, sendEmail } from "../services/email.js";
import { notifyWaitlistForAppointment } from "../services/waitlistNotify.js";
import type { AppEnv } from "../middleware/rbac.js";
+1 -1
View File
@@ -1,7 +1,7 @@
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod/v3";
import { eq, getDb, authProviderConfig, encryptSecret } from "../db";
import { eq, getDb, authProviderConfig, encryptSecret } from "../db/index.js";
import { requireSuperUser } from "../middleware/rbac.js";
import { reinitAuth } from "../lib/auth.js";
+1 -1
View File
@@ -14,7 +14,7 @@ import {
appointments,
clients,
pets,
} from "../db";
} from "../db/index.js";
import {
generateAvailableSlots,
BUSINESS_START_HOUR,
+1 -1
View File
@@ -10,7 +10,7 @@ import {
pets,
services,
staff,
} from "../db";
} from "../db/index.js";
export const calendarRouter = new Hono();
+1 -1
View File
@@ -1,7 +1,7 @@
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod/v3";
import { and, eq, exists, getDb, or, clients, appointments } from "../db";
import { and, eq, exists, getDb, or, clients, appointments } from "../db/index.js";
import type { AppEnv } from "../middleware/rbac.js";
export const clientsRouter = new Hono<AppEnv>();
+1 -1
View File
@@ -1,5 +1,5 @@
import { Hono } from "hono";
import { getDb, staff, clients, eq, sql } from "../db";
import { getDb, staff, clients, eq, sql } from "../db/index.js";
const devRouter = new Hono();
+1 -1
View File
@@ -1,7 +1,7 @@
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod/v3";
import { and, desc, eq, getDb, groomingVisitLogs, appointments, or } from "../db";
import { and, desc, eq, getDb, groomingVisitLogs, appointments, or } from "../db/index.js";
import type { AppEnv } from "../middleware/rbac.js";
export const groomingLogsRouter = new Hono<AppEnv>();
+1 -1
View File
@@ -9,7 +9,7 @@ import {
impersonationAuditLogs,
clients,
desc,
} from "../db";
} from "../db/index.js";
import type { AppEnv } from "../middleware/rbac.js";
export const impersonationRouter = new Hono<AppEnv>();
+1 -1
View File
@@ -13,7 +13,7 @@ import {
services,
clients,
sql,
} from "../db";
} from "../db/index.js";
import type { AppEnv } from "../middleware/rbac.js";
export const invoicesRouter = new Hono<AppEnv>();
+1 -1
View File
@@ -1,7 +1,7 @@
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod/v3";
import { and, eq, exists, getDb, or, pets, appointments } from "../db";
import { and, eq, exists, getDb, or, pets, appointments } from "../db/index.js";
import type { AppEnv } from "../middleware/rbac.js";
import {
getPresignedUploadUrl,
+2 -2
View File
@@ -1,8 +1,8 @@
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod/v3";
import { eq, inArray } from "../db";
import { getDb, appointments, impersonationSessions, waitlistEntries, clients, pets, services, staff, invoices, invoiceLineItems } from "../db";
import { eq, inArray } from "../db/index.js";
import { getDb, appointments, impersonationSessions, waitlistEntries, clients, pets, services, staff, invoices, invoiceLineItems } from "../db/index.js";
import { validatePortalSession } from "../middleware/portalSession.js";
import { portalAudit } from "../middleware/portalAudit.js";
import type { PortalEnv } from "../middleware/portalSession.js";
+1 -1
View File
@@ -12,7 +12,7 @@ import {
invoiceTipSplits,
services,
staff,
} from "../db";
} from "../db/index.js";
export const reportsRouter = new Hono();
+1 -1
View File
@@ -1,5 +1,5 @@
import { Hono } from "hono";
import { and, eq, getDb, clients, ilike, or, pets } from "../db";
import { and, eq, getDb, clients, ilike, or, pets } from "../db/index.js";
export const searchRouter = new Hono();
+1 -1
View File
@@ -1,7 +1,7 @@
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod/v3";
import { eq, getDb, services } from "../db";
import { eq, getDb, services } from "../db/index.js";
export const servicesRouter = new Hono();
+1 -1
View File
@@ -1,7 +1,7 @@
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod/v3";
import { eq, getDb, businessSettings } from "../db";
import { eq, getDb, businessSettings } from "../db/index.js";
import { getPresignedUploadUrl, deleteObject, putObject, getObject } from "../lib/s3.js";
import { requireSuperUser } from "../middleware/rbac.js";
+1 -1
View File
@@ -1,7 +1,7 @@
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod/v3";
import { and, eq, getDb, sql, staff, businessSettings, authProviderConfig, encryptSecret } from "../db";
import { and, eq, getDb, sql, staff, businessSettings, authProviderConfig, encryptSecret } from "../db/index.js";
import type { AppEnv } from "../middleware/rbac.js";
const RATE_LIMIT_WINDOW_MS = 60_000;
+1 -1
View File
@@ -2,7 +2,7 @@ import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod/v3";
import { randomBytes } from "node:crypto";
import { and, eq, getDb, ne, staff, appointments } from "../db";
import { and, eq, getDb, ne, staff, appointments } from "../db/index.js";
import type { AppEnv } from "../middleware/rbac.js";
export const staffRouter = new Hono<AppEnv>();
+1 -1
View File
@@ -1,7 +1,7 @@
import { Hono } from "hono";
import Stripe from "stripe";
import { z } from "zod/v3";
import { eq, getDb, invoices } from "../db";
import { eq, getDb, invoices } from "../db/index.js";
import { getStripeClient } from "../services/payment.js";
export const webhooksRouter = new Hono();
+1 -1
View File
@@ -8,7 +8,7 @@ import {
clients,
pets,
services,
} from "../db";
} from "../db/index.js";
import type { AppEnv } from "../middleware/rbac.js";
export const waitlistRouter = new Hono<AppEnv>();
+1 -1
View File
@@ -1,5 +1,5 @@
import Stripe from "stripe";
import { getDb, clients, eq, inArray, invoices } from "../db";
import { getDb, clients, eq, inArray, invoices } from "../db/index.js";
let _stripe: Stripe | null | undefined;
+1 -1
View File
@@ -14,7 +14,7 @@ import {
staff,
reminderLogs,
session,
} from "../db";
} from "../db/index.js";
import {
buildReminderEmail,
sendEmail,
+1 -1
View File
@@ -1,4 +1,4 @@
import { and, eq, getDb, waitlistEntries, clients, pets, services } from "../db";
import { and, eq, getDb, waitlistEntries, clients, pets, services } from "../db/index.js";
import { buildWaitlistNotificationEmail, sendEmail } from "./email.js";
export async function notifyWaitlistForAppointment(
-1
View File
@@ -1,3 +1,2 @@
packages:
- "apps/*"
- "packages/*"