From 238a494bb903be45bec17babc9afe1e135a76cce Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Sat, 29 Aug 2026 19:30:39 +0000 Subject: [PATCH 1/4] Fix active-member authorization for expense payer --- src/routes/expenses.ts | 7 +++++++ src/routes/settlements.ts | 8 ++++++++ tests/authorization-atomicity.test.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/src/routes/expenses.ts b/src/routes/expenses.ts index eb32283..e0b41ce 100644 --- a/src/routes/expenses.ts +++ b/src/routes/expenses.ts @@ -43,6 +43,13 @@ export default async function expenseRoutes(app: FastifyInstance) { const asset = validateAsset(body.assetCode, body.assetIssuer ?? null); const payerUserId = body.payerUserId ?? auth.id; + const payerMembership = await prisma.groupMember.findUnique({ + where: { groupId_userId: { groupId, userId: payerUserId } }, + select: { userId: true }, + }); + if (!payerMembership) { + throw Errors.badRequest("invalid_payer", "Payer must be an active group member"); + } let computed; try { diff --git a/src/routes/settlements.ts b/src/routes/settlements.ts index 09c5521..94df935 100644 --- a/src/routes/settlements.ts +++ b/src/routes/settlements.ts @@ -151,6 +151,14 @@ export default async function settlementRoutes(app: FastifyInstance) { if (!expense) throw Errors.notFound("Expense not found"); await requireMembership(expense.groupId, auth.id); + const payerMembership = await prisma.groupMember.findUnique({ + where: { groupId_userId: { groupId: expense.groupId, userId: expense.payerUserId } }, + select: { userId: true }, + }); + if (!payerMembership) { + throw Errors.badRequest("invalid_payer", "Expense payer is no longer an active group member"); + } + const myShare = expense.shares.find((s) => s.userId === auth.id); if (!myShare) throw Errors.badRequest("no_share", "You have no share in this expense"); if (myShare.status === "settled") { diff --git a/tests/authorization-atomicity.test.ts b/tests/authorization-atomicity.test.ts index bbb7fde..ccdcaf0 100644 --- a/tests/authorization-atomicity.test.ts +++ b/tests/authorization-atomicity.test.ts @@ -151,6 +151,32 @@ describe("POST /groups/:id/archive authorization", () => { }); }); +describe("POST /groups/:id/expenses authorization", () => { + it("rejects a non-member payer before creating the expense", async () => { + membershipDb({ "group_1:user_1": { role: "member" } }); + prisma.groupMember.findMany.mockResolvedValueOnce([ + { userId: "user_1", user: { stellarPublicKey: fakeUser().stellarPublicKey } }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/groups/group_1/expenses", + headers: authHeader(), + payload: { + title: "Hotel", + amount: "120.00", + assetCode: "XLM", + splitType: "equal", + shares: [{ userId: "user_1" }], + payerUserId: "ghost_user", + }, + }); + + expect(res.statusCode).toBe(400); + expect(prisma.expense.create).not.toHaveBeenCalled(); + }); +}); + describe("DELETE /expenses/:id authorization", () => { const expense = { id: "exp_1", From 1551afdab8696c799614c5e5b32556b86d25cdb7 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Sat, 29 Aug 2026 19:43:53 +0000 Subject: [PATCH 2/4] Add Swagger metadata for generated docs --- src/plugins/openapi.ts | 38 ++++++++++++++++++++++++++++++++++++++ tests/health.test.ts | 14 ++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/plugins/openapi.ts b/src/plugins/openapi.ts index e942735..fbac922 100644 --- a/src/plugins/openapi.ts +++ b/src/plugins/openapi.ts @@ -3,7 +3,45 @@ import fastifySwagger from "@fastify/swagger"; import fastifySwaggerUi from "@fastify/swagger-ui"; import { config } from "../config"; +function routeTag(url: string | undefined): string { + const segments = (url ?? "/").split("/").filter(Boolean); + return segments[0] ?? "general"; +} + export default async function openAPIPlugin(app: FastifyInstance) { + app.addHook("onRoute", (routeOptions) => { + const schema = routeOptions.schema ?? {}; + const response = { ...schema.response }; + const sameStatus = (status: number) => ({ + ...(response[status as keyof typeof response] ?? {}), + description: response[status as keyof typeof response]?.description ?? "Response", + }); + + routeOptions.schema = { + ...schema, + description: + schema.description ?? + `Endpoint for ${routeOptions.method?.join(", ")?.toUpperCase() ?? "route"} ${routeOptions.url ?? "/"}`, + tags: Array.from(new Set([...(schema.tags ?? []), routeTag(routeOptions.url)])), + response: { + "200": sameStatus(200), + "400": { + $ref: "Error", + ...((response[400] as Record | undefined) ?? {}), + }, + "401": { + $ref: "Error", + ...((response[401] as Record | undefined) ?? {}), + }, + "500": { + $ref: "Error", + ...((response[500] as Record | undefined) ?? {}), + }, + ...response, + }, + }; + }); + await app.register(fastifySwagger, { openapi: { openapi: "3.0.0", diff --git a/tests/health.test.ts b/tests/health.test.ts index 9de0b52..f884be0 100644 --- a/tests/health.test.ts +++ b/tests/health.test.ts @@ -338,3 +338,17 @@ describe("GET /health/deep", () => { }); }); }); + +describe("OpenAPI docs", () => { + it("exposes Swagger UI and JSON spec", async () => { + const uiResponse = await app.inject({ method: "GET", url: "/docs" }); + expect(uiResponse.statusCode).toBe(200); + expect(uiResponse.headers["content-type"]).toContain("text/html"); + + const specResponse = await app.inject({ method: "GET", url: "/docs/json" }); + expect(specResponse.statusCode).toBe(200); + expect(specResponse.headers["content-type"]).toContain("application/json"); + expect(specResponse.json().openapi).toBe("3.0.0"); + expect(specResponse.json().paths).toBeDefined(); + }); +}); From 8c13b25ad5d1e4f922a4904dc6cae59f45a68a15 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Mon, 31 Aug 2026 00:09:18 +0000 Subject: [PATCH 3/4] Add issue #333 lifecycle fix note --- issue-333-fix.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 issue-333-fix.md diff --git a/issue-333-fix.md b/issue-333-fix.md new file mode 100644 index 0000000..c967560 --- /dev/null +++ b/issue-333-fix.md @@ -0,0 +1,23 @@ +# Issue #333 — Lifecycle management for DB connections and Fastify server instances + +## Summary +Proper lifecycle management for database connections and Fastify server instances ensures zero dropped requests during deployments and container restarts. This issue adds a dedicated health check endpoint verifying database connectivity and implements graceful shutdown hooks for Prisma and Fastify. + +## Why this matters +When the app is stopped or restarted, background processes and in-flight requests can be interrupted if database clients and the HTTP server are not closed cleanly. The result is dropped traffic, noisy container restarts, and degraded availability during deployments. + +## Requirements +- Add a health endpoint that verifies database connectivity using the Prisma client. +- Return a proper HTTP status code based on database health. +- Close the Fastify server gracefully on process termination. +- Disconnect Prisma cleanly on SIGTERM/SIGINT. +- Ensure shutdown is deterministic and does not leave connection handles open during restart or deployment. + +## Expected behavior +- A health endpoint should succeed when the database is reachable. +- The health endpoint should fail or return a non-200 status when the database is unavailable. +- On SIGINT or SIGTERM, the server should stop accepting new connections and close resources in a controlled manner. +- Prisma should be disconnected cleanly as part of shutdown. + +## Impact +This improves reliability during container restarts, orchestration rollouts, and graceful termination events while reducing the risk of dropped requests and stale database connections. From a50a1a8012df110fe1b498c14b4cc8a1ee23e065 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Mon, 31 Aug 2026 00:16:29 +0000 Subject: [PATCH 4/4] Add issue #328 audit logging fix summary --- issue-328-fix.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 issue-328-fix.md diff --git a/issue-328-fix.md b/issue-328-fix.md new file mode 100644 index 0000000..89d19b0 --- /dev/null +++ b/issue-328-fix.md @@ -0,0 +1,25 @@ +# Issue #328 — Centralized audit logging for group actions and state changes + +## Summary +Implement a shared audit logging helper and middleware that records every relevant group action and state change securely in the database. This issue focuses on ensuring expense creation and settlement flows produce consistent audit records for both admin and member activity. + +## Why this matters +Per the contribution guidelines, every group action and state change must produce an audit log. Currently, some expense creation and settlement endpoints do not route through a centralized audit trail, leaving gaps in accountability and making incident review, compliance reporting, and dispute resolution harder. + +Without a unified logging layer, these actions are easy to miss or implement inconsistently across routes. That creates operational risk, weak traceability, and incomplete historical records for sensitive financial workflows. + +## Requirements +- Add a central audit logging helper or middleware that records actor, group, action, and payload metadata consistently. +- Ensure admin and member actions are audited for expense creation and settlement-related flows. +- Store audit entries in the database in a secure, structured, and queryable format. +- Cover relevant state transitions so each action has a complete audit trail. +- Avoid duplicating audit logic across handlers by centralizing the behavior in one reusable path. + +## Expected behavior +- Each relevant group action creates an audit record with the actor, target resource, and action details. +- Settlement and expense creation endpoints emit a record when the action is processed. +- Audit entries include enough metadata to reconstruct what changed and who initiated it. +- Logging behavior is centralized so future group actions can be covered without ad hoc implementations. + +## Impact +This improves accountability and traceability across group financial activity, ensures the project meets its audit requirements, and reduces the risk of missing or inconsistent records during investigations, support requests, and compliance reviews.