Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8dbec1be1 | |||
| 4a65c30d40 | |||
| cab17e0230 |
@@ -0,0 +1,90 @@
|
|||||||
|
# Contributing to GroomBook
|
||||||
|
|
||||||
|
## Branch Strategy
|
||||||
|
|
||||||
|
GroomBook uses a three-branch GitOps model:
|
||||||
|
|
||||||
|
| Branch | Environment | Purpose |
|
||||||
|
|--------|-------------|---------|
|
||||||
|
| `dev` | Development | Active development target — all feature/fix PRs target this branch |
|
||||||
|
| `uat` | UAT / Staging | Promoted from `dev` by the CTO for acceptance testing |
|
||||||
|
| `main` | Production | Promoted from `uat` by the CEO; triggers production deployment |
|
||||||
|
|
||||||
|
**Never open a PR directly to `uat` or `main`.** All work flows through `dev` first.
|
||||||
|
|
||||||
|
## Developer Workflow
|
||||||
|
|
||||||
|
1. **Branch from `dev`** — create a feature or fix branch:
|
||||||
|
```bash
|
||||||
|
git checkout dev
|
||||||
|
git pull origin dev
|
||||||
|
git checkout -b feat/my-feature
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Open a PR targeting `dev`** — include the issue identifier in the title and cc @cpfarhood:
|
||||||
|
```bash
|
||||||
|
gh pr create --base dev --title "feat: description (GRO-NNN)" \
|
||||||
|
--body $'Closes GRO-NNN\n\ncc @cpfarhood'
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Pipeline gates before merge to `dev`:**
|
||||||
|
- QA (Lint Roller) reviews first — code quality, test coverage, CI pass
|
||||||
|
- CTO (The Dogfather) reviews second — architecture and final approval
|
||||||
|
- Both must approve; 2 approving reviews required by branch protection
|
||||||
|
|
||||||
|
## Promotion Flow
|
||||||
|
|
||||||
|
### Dev → UAT
|
||||||
|
|
||||||
|
After merging to `dev`, the CTO opens a PR from `dev` → `uat`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh pr create --base uat --head dev \
|
||||||
|
--title "chore: promote dev to uat (YYYY.MM.DD)" \
|
||||||
|
--body $'Promoting dev to UAT for regression and security review.\n\ncc @cpfarhood'
|
||||||
|
```
|
||||||
|
|
||||||
|
Gates:
|
||||||
|
- Shedward Scissorhands runs regression/acceptance tests
|
||||||
|
- Barkley Trimsworth performs security review
|
||||||
|
- CTO approves and merges (1 approving review required)
|
||||||
|
|
||||||
|
### UAT → Main (Production)
|
||||||
|
|
||||||
|
After UAT passes, the CTO opens a PR from `uat` → `main` and assigns it to the CEO:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh pr create --base main --head uat \
|
||||||
|
--title "chore: promote uat to main (YYYY.MM.DD)" \
|
||||||
|
--body $'Promoting UAT to production.\n\ncc @cpfarhood'
|
||||||
|
```
|
||||||
|
|
||||||
|
Gates:
|
||||||
|
- CEO (Scrubs McBarkley) reviews for business alignment and merges
|
||||||
|
- 1 approving review required; triggers auto-deploy to Production
|
||||||
|
|
||||||
|
## Branch Protection Summary
|
||||||
|
|
||||||
|
| Branch | Required Approvals | Who approves |
|
||||||
|
|--------|--------------------|-------------|
|
||||||
|
| `dev` | 2 | QA (Lint Roller) + CTO (The Dogfather) |
|
||||||
|
| `uat` | 1 | CTO (The Dogfather) |
|
||||||
|
| `main` | 1 | CEO (Scrubs McBarkley) |
|
||||||
|
|
||||||
|
Force-pushes and branch deletions are disabled on all three branches.
|
||||||
|
|
||||||
|
## Commit Style
|
||||||
|
|
||||||
|
Use [Conventional Commits](https://www.conventionalcommits.org/):
|
||||||
|
- `feat:` — new feature
|
||||||
|
- `fix:` — bug fix
|
||||||
|
- `chore:` — maintenance (dependency updates, build config, promotions)
|
||||||
|
- `docs:` — documentation only
|
||||||
|
- `ci:` — CI/CD changes
|
||||||
|
- `refactor:` — code restructure without behaviour change
|
||||||
|
|
||||||
|
Reference the Paperclip issue in the commit body: `Refs GRO-NNN`.
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
Open a Paperclip issue in the GRO project or ask in the team channel.
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import type { MiddlewareHandler } from "hono";
|
|
||||||
import { getDb, impersonationAuditLogs } from "@groombook/db";
|
|
||||||
import type { PortalEnv } from "./portalSession.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Server-side audit logging middleware for portal routes.
|
|
||||||
* Applied after validatePortalSession in the middleware chain.
|
|
||||||
*
|
|
||||||
* After the route handler completes (await next()), inserts an audit log entry
|
|
||||||
* into impersonationAuditLogs:
|
|
||||||
* - sessionId: from c.get("portalSessionId")
|
|
||||||
* - action: "{METHOD} {routePath}" (e.g., "GET /portal/appointments")
|
|
||||||
* - pageVisited: c.req.path
|
|
||||||
* - metadata: { method, statusCode: c.res.status }
|
|
||||||
*
|
|
||||||
* Log entries are written for both success and error responses.
|
|
||||||
* Does NOT throw if audit logging fails — errors are logged but the user's
|
|
||||||
* request is not affected.
|
|
||||||
*/
|
|
||||||
export const portalAudit: MiddlewareHandler<PortalEnv> = async (c, next) => {
|
|
||||||
await next();
|
|
||||||
|
|
||||||
const sessionId = c.get("portalSessionId");
|
|
||||||
if (!sessionId) return;
|
|
||||||
|
|
||||||
const method = c.req.method;
|
|
||||||
const routePath = c.req.path;
|
|
||||||
const pageVisited = c.req.path;
|
|
||||||
const statusCode = c.res.status;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const db = getDb();
|
|
||||||
await db
|
|
||||||
.insert(impersonationAuditLogs)
|
|
||||||
.values({
|
|
||||||
sessionId,
|
|
||||||
action: `${method} ${routePath}`,
|
|
||||||
pageVisited,
|
|
||||||
metadata: { method, statusCode },
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[portalAudit] Failed to write audit log:", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import type { MiddlewareHandler } from "hono";
|
|
||||||
import { and, eq, getDb, impersonationSessions } from "@groombook/db";
|
|
||||||
|
|
||||||
export interface PortalEnv {
|
|
||||||
Variables: {
|
|
||||||
portalClientId: string;
|
|
||||||
portalSessionId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the X-Impersonation-Session-Id header against the impersonationSessions table.
|
|
||||||
* Must be applied to all portal routes.
|
|
||||||
*
|
|
||||||
* Reads x-session-id from request headers, queries impersonationSessions for a row where
|
|
||||||
* id = sessionId AND status = 'active', and checks session.expiresAt > new Date().
|
|
||||||
* Returns 401 if session is invalid/missing/expired.
|
|
||||||
* On success, sets c.set("portalClientId", session.clientId) and c.set("portalSessionId", session.id).
|
|
||||||
*/
|
|
||||||
export const validatePortalSession: MiddlewareHandler<PortalEnv> = async (c, next) => {
|
|
||||||
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
|
||||||
if (!sessionId) {
|
|
||||||
return c.json({ error: "Unauthorized" }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 c.json({ error: "Unauthorized" }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
c.set("portalClientId", session.clientId);
|
|
||||||
c.set("portalSessionId", session.id);
|
|
||||||
await next();
|
|
||||||
};
|
|
||||||
+122
-23
@@ -1,22 +1,33 @@
|
|||||||
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 { eq, inArray } 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 { getDb, appointments, impersonationSessions, waitlistEntries, clients, pets, services, staff, invoices, invoiceLineItems } from "@groombook/db";
|
||||||
import { validatePortalSession } from "../middleware/portalSession.js";
|
import type { AppEnv } from "../middleware/rbac.js";
|
||||||
import { portalAudit } from "../middleware/portalAudit.js";
|
|
||||||
import type { PortalEnv } from "../middleware/portalSession.js";
|
|
||||||
|
|
||||||
export const portalRouter = new Hono<PortalEnv>();
|
export const portalRouter = new Hono<AppEnv>();
|
||||||
|
|
||||||
// Apply middleware to all portal routes
|
// ─── Session helper ───────────────────────────────────────────────────────────
|
||||||
portalRouter.use("/*", validatePortalSession, portalAudit);
|
|
||||||
|
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 ──────────────────────────────────────────────────────────────
|
// ─── GET routes ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
portalRouter.get("/me", async (c) => {
|
portalRouter.get("/me", async (c) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const clientId = c.get("portalClientId");
|
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);
|
const [client] = await db.select().from(clients).where(eq(clients.id, clientId)).limit(1);
|
||||||
if (!client) return c.json({ error: "Not found" }, 404);
|
if (!client) return c.json({ error: "Not found" }, 404);
|
||||||
@@ -38,7 +49,9 @@ portalRouter.get("/services", async (c) => {
|
|||||||
|
|
||||||
portalRouter.get("/appointments", async (c) => {
|
portalRouter.get("/appointments", async (c) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const clientId = c.get("portalClientId");
|
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 now = new Date();
|
||||||
const allAppts = await db
|
const allAppts = await db
|
||||||
@@ -88,7 +101,9 @@ portalRouter.get("/appointments", async (c) => {
|
|||||||
|
|
||||||
portalRouter.get("/pets", async (c) => {
|
portalRouter.get("/pets", async (c) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const clientId = c.get("portalClientId");
|
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));
|
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 })));
|
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 })));
|
||||||
@@ -96,7 +111,9 @@ portalRouter.get("/pets", async (c) => {
|
|||||||
|
|
||||||
portalRouter.get("/invoices", async (c) => {
|
portalRouter.get("/invoices", async (c) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const clientId = c.get("portalClientId");
|
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 clientInvoices = await db.select().from(invoices).where(eq(invoices.clientId, clientId));
|
||||||
const invoiceIds = clientInvoices.map(i => i.id);
|
const invoiceIds = clientInvoices.map(i => i.id);
|
||||||
@@ -131,7 +148,12 @@ portalRouter.patch(
|
|||||||
const db = getDb();
|
const db = getDb();
|
||||||
const id = c.req.param("id");
|
const id = c.req.param("id");
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const clientId = c.get("portalClientId");
|
|
||||||
|
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||||
|
const clientId = await getClientIdFromSession(sessionId);
|
||||||
|
if (!clientId) {
|
||||||
|
return c.json({ error: "Unauthorized" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
const [appt] = await db
|
const [appt] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -174,7 +196,12 @@ portalRouter.patch(
|
|||||||
portalRouter.post("/appointments/:id/confirm", async (c) => {
|
portalRouter.post("/appointments/:id/confirm", async (c) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const id = c.req.param("id");
|
const id = c.req.param("id");
|
||||||
const clientId = c.get("portalClientId");
|
|
||||||
|
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||||
|
const clientId = await getClientIdFromSession(sessionId);
|
||||||
|
if (!clientId) {
|
||||||
|
return c.json({ error: "Unauthorized" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
const [appt] = await db
|
const [appt] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -223,7 +250,12 @@ portalRouter.post("/appointments/:id/confirm", async (c) => {
|
|||||||
portalRouter.post("/appointments/:id/cancel", async (c) => {
|
portalRouter.post("/appointments/:id/cancel", async (c) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const id = c.req.param("id");
|
const id = c.req.param("id");
|
||||||
const clientId = c.get("portalClientId");
|
|
||||||
|
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||||
|
const clientId = await getClientIdFromSession(sessionId);
|
||||||
|
if (!clientId) {
|
||||||
|
return c.json({ error: "Unauthorized" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
const [appt] = await db
|
const [appt] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -287,7 +319,28 @@ portalRouter.post(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const clientId = c.get("portalClientId");
|
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||||
|
|
||||||
|
let clientId: string | null = null;
|
||||||
|
if (sessionId) {
|
||||||
|
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()) {
|
||||||
|
clientId = session.clientId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientId) {
|
||||||
|
return c.json({ error: "Unauthorized" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
const [entry] = await db
|
const [entry] = await db
|
||||||
.insert(waitlistEntries)
|
.insert(waitlistEntries)
|
||||||
@@ -311,7 +364,26 @@ portalRouter.patch(
|
|||||||
const db = getDb();
|
const db = getDb();
|
||||||
const id = c.req.param("id");
|
const id = c.req.param("id");
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const clientId = c.get("portalClientId");
|
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 [existing] = await db
|
const [existing] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -320,7 +392,7 @@ portalRouter.patch(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!existing) return c.json({ error: "Not found" }, 404);
|
if (!existing) return c.json({ error: "Not found" }, 404);
|
||||||
if (existing.clientId !== clientId) {
|
if (existing.clientId !== session.clientId) {
|
||||||
return c.json({ error: "Forbidden" }, 403);
|
return c.json({ error: "Forbidden" }, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +414,26 @@ portalRouter.patch(
|
|||||||
portalRouter.delete("/waitlist/:id", async (c) => {
|
portalRouter.delete("/waitlist/:id", async (c) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const id = c.req.param("id");
|
const id = c.req.param("id");
|
||||||
const clientId = c.get("portalClientId");
|
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 [entry] = await db
|
const [entry] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -351,7 +442,7 @@ portalRouter.delete("/waitlist/:id", async (c) => {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!entry) return c.json({ error: "Not found" }, 404);
|
if (!entry) return c.json({ error: "Not found" }, 404);
|
||||||
if (entry.clientId !== clientId) {
|
if (entry.clientId !== session.clientId) {
|
||||||
return c.json({ error: "Forbidden" }, 403);
|
return c.json({ error: "Forbidden" }, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +475,9 @@ portalRouter.post(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const clientId = c.get("portalClientId");
|
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||||
|
const clientId = await getClientIdFromSession(sessionId);
|
||||||
|
if (!clientId) return c.json({ error: "Unauthorized" }, 401);
|
||||||
|
|
||||||
const invoiceRows = await db
|
const invoiceRows = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -421,7 +514,9 @@ portalRouter.post(
|
|||||||
);
|
);
|
||||||
|
|
||||||
portalRouter.get("/payment-methods", async (c) => {
|
portalRouter.get("/payment-methods", async (c) => {
|
||||||
const clientId = c.get("portalClientId");
|
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||||
|
const clientId = await getClientIdFromSession(sessionId);
|
||||||
|
if (!clientId) return c.json({ error: "Unauthorized" }, 401);
|
||||||
|
|
||||||
const methods = await listPaymentMethods(clientId);
|
const methods = await listPaymentMethods(clientId);
|
||||||
if (methods === null) return c.json({ error: "Payment service unavailable" }, 503);
|
if (methods === null) return c.json({ error: "Payment service unavailable" }, 503);
|
||||||
@@ -429,7 +524,9 @@ portalRouter.get("/payment-methods", async (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
portalRouter.post("/payment-methods", async (c) => {
|
portalRouter.post("/payment-methods", async (c) => {
|
||||||
const clientId = c.get("portalClientId");
|
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||||
|
const clientId = await getClientIdFromSession(sessionId);
|
||||||
|
if (!clientId) return c.json({ error: "Unauthorized" }, 401);
|
||||||
|
|
||||||
const stripePublishableKey = process.env.STRIPE_PUBLISHABLE_KEY ?? "";
|
const stripePublishableKey = process.env.STRIPE_PUBLISHABLE_KEY ?? "";
|
||||||
const customerId = await getOrCreateStripeCustomer(clientId);
|
const customerId = await getOrCreateStripeCustomer(clientId);
|
||||||
@@ -442,7 +539,9 @@ portalRouter.post("/payment-methods", async (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
portalRouter.delete("/payment-methods/:id", async (c) => {
|
portalRouter.delete("/payment-methods/:id", async (c) => {
|
||||||
const clientId = c.get("portalClientId");
|
const sessionId = c.req.header("X-Impersonation-Session-Id");
|
||||||
|
const clientId = await getClientIdFromSession(sessionId);
|
||||||
|
if (!clientId) return c.json({ error: "Unauthorized" }, 401);
|
||||||
|
|
||||||
const paymentMethodId = c.req.param("id");
|
const paymentMethodId = c.req.param("id");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user