Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/backend/lambdas/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Lambda for auth handler.
| POST | /verify-email | |
| POST | /resend-code | |
| POST | /logout | |
| POST | /forgot-password | |
| POST | /reset-password | |

## Setup

Expand Down
100 changes: 95 additions & 5 deletions apps/backend/lambdas/auth/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
ResendConfirmationCodeCommandInput,
GlobalSignOutCommand,
GlobalSignOutCommandInput,
ForgotPasswordCommand,
ForgotPasswordCommandInput,
ConfirmForgotPasswordCommand,
ConfirmForgotPasswordCommandInput,
} from '@aws-sdk/client-cognito-identity-provider';
import { CognitoUser, CognitoUserPool, AuthenticationDetails } from 'amazon-cognito-identity-js';
import db from './db';
Expand Down Expand Up @@ -64,11 +68,22 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {
Username: email as string,
ConfirmationCode: code as string,
};
const response = await cognitoClient.send(new ConfirmSignUpCommand(params));
if (!response.Session) {
return json(400, { message: 'Invalid code or email' });
try {
await cognitoClient.send(new ConfirmSignUpCommand(params));
} catch (error: any) {
console.error('Email verification error:', error);
if (error.name === 'NotAuthorizedException' && error.message?.includes('CONFIRMED')) {
return json(200, { message: `Email already verified for ${email}` });
}
if (error.name === 'CodeMismatchException' || error.name === 'ExpiredCodeException') {
return json(400, { message: 'Invalid or expired verification code' });
}
if (error.name === 'UserNotFoundException') {
return json(400, { message: 'Invalid code or email' });
}
return json(500, { message: 'Failed to verify email' });
}
return json(200, { message: `Email verified successfully for ${email}, session: ${response.Session}` });
return json(200, { message: `Email verified successfully for ${email}` });
}

// POST /resend-code
Expand Down Expand Up @@ -122,7 +137,82 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {
return json(500, { message: 'Failed to logout' });
}
}
// <<< ROUTES-END

// POST /forgot-password
if (normalizedPath === '/forgot-password' && method === 'POST') {
const body = event.body ? JSON.parse(event.body) as Record<string, unknown> : {};
const { email } = body;
if (!email) {
return json(400, { message: 'email is required' });
}

const params: ForgotPasswordCommandInput = {
ClientId: USER_POOL_CLIENT_ID,
Username: (email as string).toLowerCase(),
};

try {
const response = await cognitoClient.send(new ForgotPasswordCommand(params));
return json(200, {
message: 'Password reset code sent',
deliveryMedium: response.CodeDeliveryDetails?.DeliveryMedium,
destination: response.CodeDeliveryDetails?.Destination,
});
} catch (error: any) {
console.error('Forgot password error:', error);
if (error.name === 'UserNotFoundException') {
// Don't reveal whether the user exists
return json(200, { message: 'If an account with that email exists, a reset code has been sent' });
}
if (error.name === 'LimitExceededException') {
return json(429, { message: 'Too many requests, please try again later' });
}
if (error.name === 'InvalidParameterException') {
return json(400, { message: 'Cannot reset password for unverified email. Please verify your email first.' });
}
return json(500, { message: 'Failed to initiate password reset' });
}
}

// POST /reset-password
if (normalizedPath === '/reset-password' && method === 'POST') {
const body = event.body ? JSON.parse(event.body) as Record<string, unknown> : {};
const { email, code, newPassword } = body;
if (!email || !code || !newPassword) {
return json(400, { message: 'email, code, and newPassword are required' });
}

const params: ConfirmForgotPasswordCommandInput = {
ClientId: USER_POOL_CLIENT_ID,
Username: (email as string).toLowerCase(),
ConfirmationCode: code as string,
Password: newPassword as string,
};

try {
await cognitoClient.send(new ConfirmForgotPasswordCommand(params));
return json(200, { message: 'Password reset successfully' });
} catch (error: any) {
console.error('Reset password error:', error);
if (error.name === 'CodeMismatchException') {
return json(400, { message: 'Invalid verification code' });
}
if (error.name === 'ExpiredCodeException') {
return json(400, { message: 'Verification code has expired, please request a new one' });
}
if (error.name === 'InvalidPasswordException') {
return json(400, { message: 'Password does not meet requirements (min 8 chars, uppercase, lowercase, number)' });
}
if (error.name === 'UserNotFoundException') {
return json(400, { message: 'Invalid email or code' });
}
if (error.name === 'LimitExceededException') {
return json(429, { message: 'Too many attempts, please try again later' });
}
return json(500, { message: 'Failed to reset password' });
}
}
// <<< ROUTES-END

return json(404, { message: 'Not Found', path: normalizedPath, method });
} catch (err) {
Expand Down
Loading