Compare commits

...

13 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
Chris Farhood 2448887924 fix: regenerate pnpm-lock.yaml to sync with package.json
- Adds missing drizzle-kit, drizzle-orm, postgres dependencies
- Addresses CI failures from Lint & Typecheck and Test jobs
- Resolves QA feedback from Lint Roller on PR #5
2026-05-12 21:13:55 +00:00
Chris Farhood f4995d987d fix: correct test mock paths from "./db" to "../db"
Fixes incorrect vi.mock paths that were causing tests to fail.
The mock path should match the import path in the route files.

This addresses the authProvider test mock path issue on PR #2.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-12 19:54:29 +00:00
46 changed files with 222 additions and 114 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
+1 -1
View File
@@ -5,7 +5,7 @@ let dbSelectResult: unknown[] = [];
const mockEq = vi.fn((_col: unknown, _val: unknown) => ({ col: _col, val: _val }));
const mockDecryptSecret = vi.fn((s: string) => `decrypted:${s}`);
vi.mock("./db", () => {
vi.mock("../db", () => {
const authProviderConfig = new Proxy(
{ _name: "auth_provider_config" },
{
+1 -1
View File
@@ -38,7 +38,7 @@ const mockGroomer: MockStaff = { id: "staff-3", role: "groomer", isSuperUser: fa
// ─── Mock db module ───────────────────────────────────────────────────────────
vi.mock("./db", () => {
vi.mock("../db", () => {
const authProviderConfig = new Proxy(
{ _name: "auth_provider_config" },
{
+1 -1
View File
@@ -40,7 +40,7 @@ function resetMock() {
deletedId = null;
}
vi.mock("./db", () => {
vi.mock("../db", () => {
function makeChainable(data: unknown[]): unknown {
const arr = [...data];
const chain = new Proxy(arr, {
+1 -1
View File
@@ -39,7 +39,7 @@ function resetMock() {
lastUpdate = {};
}
vi.mock("./db", () => {
vi.mock("../db", () => {
const appointments = new Proxy(
{ _name: "appointments" },
{ get: (t, p) => (p === "_name" ? "appointments" : { table: "appointments", column: p }) }
+1 -1
View File
@@ -76,7 +76,7 @@ function makeChainableResult(data: unknown[]): unknown {
});
}
vi.mock("./db", () => {
vi.mock("../db", () => {
function makeTable(name: string) {
return new Proxy(
{ _name: name },
+1 -1
View File
@@ -40,7 +40,7 @@ function resetDb() {
// ─── Module mocks ─────────────────────────────────────────────────────────────
vi.mock("./db", () => {
vi.mock("../db", () => {
const pets = new Proxy(
{ _name: "pets" },
{ get(t, p) { return p === "_name" ? "pets" : {}; } }
+1 -1
View File
@@ -47,7 +47,7 @@ function resetMock() {
updatedValues = [];
}
vi.mock("./db", () => {
vi.mock("../db", () => {
function makeChainable(data: unknown[]): unknown {
const arr = [...data];
const chain = new Proxy(arr, {
+100 -22
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 };
vi.mock("../db", () => {
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 ────────────────────────────────────────────────────────
+1 -1
View File
@@ -23,7 +23,7 @@ const PET_ROW = {
let clientResults: typeof ACTIVE_CLIENT[] = [];
let petResults: typeof PET_ROW[] = [];
vi.mock("./db", () => {
vi.mock("../db", () => {
// Proxy objects for table/column references — values don't matter for tests
const tableProxy = (name: string) =>
new Proxy(
+1 -1
View File
@@ -39,7 +39,7 @@ function clearAuthEnv() {
// ─── Mock db module ───────────────────────────────────────────────────────────
vi.mock("./db", () => {
vi.mock("../db", () => {
const authProviderConfig = new Proxy(
{ _name: "auth_provider_config" },
{
+1 -1
View File
@@ -49,7 +49,7 @@ function resetMock() {
updatedValues = [];
}
vi.mock("./db", () => {
vi.mock("../db", () => {
function makeChainable(data: unknown[]): unknown {
const arr = [...data];
const chain = new Proxy(arr, {
+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(
+17 -38
View File
@@ -16,12 +16,6 @@ importers:
'@aws-sdk/s3-request-presigner':
specifier: ^3.800.0
version: 3.1041.0
'@groombook/db':
specifier: workspace:*
version: link:../../packages/db
'@groombook/types':
specifier: workspace:*
version: link:../../packages/types
'@hono/node-server':
specifier: ^1.13.7
version: 1.19.14(hono@4.12.16)
@@ -30,7 +24,10 @@ importers:
version: 0.7.6(hono@4.12.16)(zod@4.4.2)
better-auth:
specifier: ^1.5.6
version: 1.6.9(vitest@3.2.4(@types/node@22.19.17)(tsx@4.21.0))
version: 1.6.9(drizzle-kit@0.30.6)(drizzle-orm@0.38.4(kysely@0.28.16)(postgres@3.4.9))(vitest@3.2.4(@types/node@22.19.17)(tsx@4.21.0))
drizzle-orm:
specifier: ^0.38.4
version: 0.38.4(kysely@0.28.16)(postgres@3.4.9)
hono:
specifier: ^4.6.17
version: 4.12.16
@@ -40,6 +37,9 @@ importers:
nodemailer:
specifier: ^6.9.16
version: 6.10.1
postgres:
specifier: ^3.4.5
version: 3.4.9
stripe:
specifier: ^22.0.0
version: 22.1.0(@types/node@22.19.17)
@@ -62,6 +62,9 @@ importers:
'@vitest/coverage-v8':
specifier: ^3.2.4
version: 3.2.4(vitest@3.2.4(@types/node@22.19.17)(tsx@4.21.0))
drizzle-kit:
specifier: ^0.30.4
version: 0.30.6
eslint:
specifier: ^9.18.0
version: 9.39.4
@@ -78,34 +81,6 @@ importers:
specifier: ^3.2.4
version: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)
packages/db:
dependencies:
drizzle-orm:
specifier: ^0.38.4
version: 0.38.4(kysely@0.28.16)(postgres@3.4.9)
postgres:
specifier: ^3.4.5
version: 3.4.9
devDependencies:
'@types/node':
specifier: ^22.10.7
version: 22.19.17
drizzle-kit:
specifier: ^0.30.4
version: 0.30.6
tsx:
specifier: ^4.19.0
version: 4.21.0
typescript:
specifier: ^5.7.3
version: 5.9.3
packages/types:
devDependencies:
typescript:
specifier: ^5.7.3
version: 5.9.3
packages:
'@ampproject/remapping@2.3.0':
@@ -2932,10 +2907,12 @@ snapshots:
nanostores: 1.3.0
zod: 4.4.2
'@better-auth/drizzle-adapter@1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)':
'@better-auth/drizzle-adapter@1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.38.4(kysely@0.28.16)(postgres@3.4.9))':
dependencies:
'@better-auth/core': 1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0)
'@better-auth/utils': 0.4.0
optionalDependencies:
drizzle-orm: 0.38.4(kysely@0.28.16)(postgres@3.4.9)
'@better-auth/kysely-adapter@1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(kysely@0.28.16)':
dependencies:
@@ -3925,10 +3902,10 @@ snapshots:
balanced-match@4.0.4: {}
better-auth@1.6.9(vitest@3.2.4(@types/node@22.19.17)(tsx@4.21.0)):
better-auth@1.6.9(drizzle-kit@0.30.6)(drizzle-orm@0.38.4(kysely@0.28.16)(postgres@3.4.9))(vitest@3.2.4(@types/node@22.19.17)(tsx@4.21.0)):
dependencies:
'@better-auth/core': 1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0)
'@better-auth/drizzle-adapter': 1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)
'@better-auth/drizzle-adapter': 1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.38.4(kysely@0.28.16)(postgres@3.4.9))
'@better-auth/kysely-adapter': 1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(kysely@0.28.16)
'@better-auth/memory-adapter': 1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)
'@better-auth/mongo-adapter': 1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)
@@ -3945,6 +3922,8 @@ snapshots:
nanostores: 1.3.0
zod: 4.4.2
optionalDependencies:
drizzle-kit: 0.30.6
drizzle-orm: 0.38.4(kysely@0.28.16)(postgres@3.4.9)
vitest: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)
transitivePeerDependencies:
- '@cloudflare/workers-types'
-1
View File
@@ -1,3 +1,2 @@
packages:
- "apps/*"
- "packages/*"