Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions apps/backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,10 @@
# Default environment file
ENV_FILE ?= .env

# Start all services
# Start all services (foreground, Ctrl+C stops everything)
up:
@echo "Starting all services..."
docker compose --env-file $(ENV_FILE) up -d --build
@echo ""
@echo "Services are starting up..."
@echo "Waiting for health checks to pass..."
@sleep 5
@$(MAKE) health --no-print-directory
docker compose --env-file $(ENV_FILE) up --build --abort-on-container-exit

# Stop all services
down:
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ services:
DB_USER: ${DB_USER:-branch_dev}
DB_PASSWORD: ${DB_PASSWORD:-password}
DB_NAME: ${DB_NAME:-branch_db}
REPORTS_BUCKET_NAME: ${REPORTS_BUCKET_NAME:-}
AWS_REGION: ${AWS_REGION:-us-east-2}
COGNITO_CLIENT_ID: ${COGNITO_CLIENT_ID}
COGNITO_USER_POOL_ID: ${COGNITO_USER_POOL_ID}
ports:
- '3005:3000'
depends_on:
Expand Down
49 changes: 49 additions & 0 deletions apps/backend/lambdas/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Lambda CLI

When adding new API endpoints or scaffolding new Lambda handlers, use the CLI at `tools/lambda-cli.js`. Run all commands from this directory (`apps/backend/lambdas/`).

## Commands

### `init-handler <name>`
Creates a new Lambda handler with boilerplate (handler.ts, dev-server.ts, openapi.yaml, swagger-utils.ts, package.json, tsconfig.json, README.md, test/).

```bash
node tools/lambda-cli.js init-handler orders
```

### `add-route <handler> <METHOD> <path> [options]`
Adds a route stub to both `handler.ts` (between the ROUTES-START/ROUTES-END markers) and `openapi.yaml`.

Options:
- `--body field:type,field:type` — request body fields
- `--query field:type,field:type` — query parameters
- `--headers field:type,field:type` — header parameters
- `--status <code>` — response status code (default: 200)

```bash
node tools/lambda-cli.js add-route auth POST /reset-password --body email:string,code:string,newPassword:string
node tools/lambda-cli.js add-route users GET /users/{id}
node tools/lambda-cli.js add-route users GET /users --query page:number,limit:number
node tools/lambda-cli.js add-route users POST /users --body name:string --headers authorization:string --status 201
```

### `list-routes <handler>`
Lists all routes defined in a handler (from both handler.ts and openapi.yaml).

```bash
node tools/lambda-cli.js list-routes auth
```

### `generate-readme [handler]`
Generates/regenerates README.md for a handler. Omit handler name to regenerate all.

```bash
node tools/lambda-cli.js generate-readme auth
node tools/lambda-cli.js generate-readme
```

## After using add-route

The CLI generates stub code with `// TODO: Add your business logic here`. You must:
1. Replace the TODO stub with actual implementation
2. Update the generated OpenAPI spec in `openapi.yaml` with proper request/response schemas, descriptions, and status codes
3 changes: 2 additions & 1 deletion apps/backend/lambdas/reports/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

## Description

Lambda for generating reports.
TODO: Add a description of the reports lambda.

## Endpoints

| Method | Path | Description |
|--------|------|-------------|
| GET | /health | Health check |
| POST | /reports | |
| GET | /reports | |

## Setup
Expand Down
3 changes: 1 addition & 2 deletions apps/backend/lambdas/reports/auth.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { CognitoJwtVerifier } from 'aws-jwt-verify';
import db from './db';

// Load from environment variables
const COGNITO_USER_POOL_ID = process.env.COGNITO_USER_POOL_ID || '';
const COGNITO_CLIENT_ID = process.env.COGNITO_CLIENT_ID || '';

// Create verifier instance lazily (only when needed)
let verifier: any = null;

function getVerifier() {
Expand Down Expand Up @@ -67,6 +65,7 @@ export async function authenticateRequest(event: any): Promise<AuthContext> {
.executeTakeFirst();

if (!dbUser) {
console.warn('User authenticated with Cognito but not found in database:', payload.sub);
return { isAuthenticated: false };
}

Expand Down
3 changes: 3 additions & 0 deletions apps/backend/lambdas/reports/dev-server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { config } from 'dotenv';
config(); // Load .env file

import { handler } from './handler';
import { loadOpenApiSpec, getSwaggerHtml } from './swagger-utils';
import * as http from 'http';
Expand Down
64 changes: 60 additions & 4 deletions apps/backend/lambdas/reports/handler.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,80 @@
import { APIGatewayProxyResult } from 'aws-lambda';
import db from './db';
import { authenticateRequest } from './auth';
import {
checkProjectAccess,
fetchReportData,
generatePdf,
uploadToS3,
saveReportRecord,
} from './report-service';

export const handler = async (event: any): Promise<APIGatewayProxyResult> => {
try {
// Support both API Gateway and Lambda Function URL events
// API Gateway: event.path, event.httpMethod
// Function URL: event.rawPath, event.requestContext.http.method
const rawPath = event.rawPath || event.path || '/';
const normalizedPath = rawPath.replace(/\/$/, '');
const method = (event.requestContext?.http?.method || event.httpMethod || 'GET').toUpperCase();

// Health check
if ((normalizedPath.endsWith('/health') || normalizedPath === '/health') && method === 'GET') {
return json(200, { ok: true, timestamp: new Date().toISOString() });
}

// >>> ROUTES-START (do not remove this marker)
// CLI-generated routes will be inserted here

// POST /reports
if ((normalizedPath === '/reports' || normalizedPath === '' || normalizedPath === '/') && method === 'POST') {
const authContext = await authenticateRequest(event);
if (!authContext.isAuthenticated || !authContext.user) {
return json(401, { message: 'Authentication required' });
}

const { user } = authContext;
const body = event.body ? JSON.parse(event.body) as Record<string, unknown> : {};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: we should add in a check in case the JSON is malformed.


const projectId = body.project_id;
if (projectId === undefined || projectId === null) {
return json(400, { message: 'project_id is required' });
}
if (typeof projectId !== 'number' || !Number.isInteger(projectId) || projectId <= 0) {
return json(400, { message: 'project_id must be a positive integer' });
}

const reportData = await fetchReportData(projectId);
if (!reportData) {
return json(404, { message: 'Project not found' });
}

const hasAccess = await checkProjectAccess(user.userId!, projectId, user.isAdmin);
if (!hasAccess) {
return json(403, { message: 'You do not have access to generate reports for this project' });
}

let pdfBuffer: Buffer;
try {
pdfBuffer = await generatePdf(reportData);
} catch (err) {
console.error('PDF generation error:', err);
return json(500, { message: 'Failed to generate report PDF' });
}

let objectUrl: string;
try {
objectUrl = await uploadToS3(pdfBuffer, projectId);
} catch (err) {
console.error('S3 upload error:', err);
return json(500, { message: 'Failed to upload report' });
}

const record = await saveReportRecord(projectId, objectUrl);

return json(201, {
ok: true,
report_id: record.report_id,
object_url: record.object_url,
});
}

// GET /reports
if ((normalizedPath === '/reports' || normalizedPath === '' || normalizedPath === '/') && method === 'GET') {
const authContext = await authenticateRequest(event);
Expand Down
54 changes: 54 additions & 0 deletions apps/backend/lambdas/reports/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ info:
title: reports (Local)
version: 1.0.0
servers:
- url: http://localhost:3005/reports
- url: http://localhost:3000/reports
security:
- BearerAuth: []
paths:
/health:
get:
summary: Health check
security: []
responses:
'200':
description: OK
Expand Down Expand Up @@ -78,3 +82,53 @@ paths:
description: Invalid pagination params
'401':
description: Unauthorized
post:
summary: Generate a project report PDF
description: >
Generates a PDF report for the given project containing project info,
participants and roles, donations, and expenditures. Uploads the PDF to
S3 and records it in the database. Requires the caller to be a member
of the project or a global admin.
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- project_id
properties:
project_id:
type: integer
description: The ID of the project to generate a report for
example: 1
responses:
'201':
description: Report generated successfully
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
report_id:
type: integer
object_url:
type: string
'400':
description: Invalid input
'401':
description: Authentication required
'403':
description: Not a member of this project
'404':
description: Project not found
'500':
description: Internal server error
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
Loading
Loading