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
2 changes: 2 additions & 0 deletions apps/backend/lambdas/reports/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ TODO: Add a description of the reports lambda.
| GET | /reports | |
| GET | /reports/upload-url | |
| POST | /reports | |
| GET | /reports/{id} | |
| DELETE | /reports/{id} | |

## Setup

Expand Down
91 changes: 68 additions & 23 deletions apps/backend/lambdas/reports/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ const MIME_TYPES: Record<string, string> = {
pdf: 'application/pdf',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
};
const REPORT_ID_ROUTE = /^\/(\d+)$/;

async function requireAuth(
event: any
): Promise<{ user: NonNullable<Awaited<ReturnType<typeof authenticateRequest>>['user']> } | { errorResponse: APIGatewayProxyResult }> {
const authContext = await authenticateRequest(event);
if (!authContext.isAuthenticated || !authContext.user) {
return { errorResponse: json(401, { message: 'Authentication required' }) };
}
return { user: authContext.user };
}

type FileType = typeof ALLOWED_EXTENSIONS[number];

Expand All @@ -47,12 +58,9 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {

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

const { user } = authContext;
const authResult = await requireAuth(event);
if ('errorResponse' in authResult) return authResult.errorResponse;
const { user } = authResult;
const body = event.body ? JSON.parse(event.body) as Record<string, unknown> : {};

const projectId = body.project_id;
Expand Down Expand Up @@ -107,10 +115,8 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {

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

const queryParams = event.queryStringParameters || {};
const pageStr = queryParams.page as string | undefined;
Expand Down Expand Up @@ -168,12 +174,9 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {

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

const { user } = authContext;
const authResult = await requireAuth(event);
if ('errorResponse' in authResult) return authResult.errorResponse;
const { user } = authResult;

const queryParams = event.queryStringParameters || {};
const { fileName, projectId: projectIdStr } = queryParams;
Expand Down Expand Up @@ -215,12 +218,9 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {

// 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 authResult = await requireAuth(event);
if ('errorResponse' in authResult) return authResult.errorResponse;
const { user } = authResult;

let body: Record<string, unknown>;
try {
Expand Down Expand Up @@ -263,7 +263,52 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {

return json(201, report);
}
// <<< ROUTES-END

// GET /reports/{id}
const getIdMatch = method === 'GET' ? normalizedPath.match(REPORT_ID_ROUTE) : null;
if (getIdMatch) {
const id = getIdMatch[1];

const authResult = await requireAuth(event);
if ('errorResponse' in authResult) return authResult.errorResponse;
const { user } = authResult;

const report = await db.selectFrom('branch.reports').where('report_id', '=', Number(id)).selectAll().executeTakeFirst();
if (!report) return json(404, { message: 'Report not found' });

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

return json(200, { ok: true, route: 'GET /reports/{id}', pathParams: { id }, body: report });
}

// DELETE /reports/{id}
const deleteIdMatch = method === 'DELETE' ? normalizedPath.match(REPORT_ID_ROUTE) : null;
if (deleteIdMatch) {
const id = deleteIdMatch[1];

const authResult = await requireAuth(event);
if ('errorResponse' in authResult) return authResult.errorResponse;
const { user } = authResult;

const report = await db.selectFrom('branch.reports').where('report_id', '=', Number(id)).selectAll().executeTakeFirst();
if (!report) return json(404, { message: 'Report not found' });

const hasAccess = await checkProjectAccess(user.userId!, report.project_id, user.isAdmin);
if (!hasAccess) {
return json(403, { message: 'You do not have access to delete this report' });
}

const deleted = await db.deleteFrom('branch.reports').where('report_id', '=', Number(id)).execute();
if (!deleted[0] || deleted[0].numDeletedRows === 0n) {
return json(404, { message: 'Report not found' });
}

return json(200, { ok: true, route: 'DELETE /reports/{id}', pathParams: { id } });
}
// <<< ROUTES-END

return json(404, { message: 'Not Found', path: normalizedPath, method });
} catch (err) {
Expand Down
28 changes: 26 additions & 2 deletions apps/backend/lambdas/reports/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ paths:

/reports:
get:
summary: GET /reports paginated list
summary: GET /reports paginated list
parameters:
- in: query
name: page
Expand Down Expand Up @@ -86,7 +86,7 @@ paths:
'401':
description: Unauthorized
post:
summary: POST /reports save a manually uploaded report
summary: POST /reports save a manually uploaded report
description: >
Creates a report record using the S3 object URL obtained from
GET /reports/upload-url after the client has uploaded the file directly
Expand Down Expand Up @@ -214,6 +214,30 @@ paths:
'500':
description: Internal server error

/reports/{id}:
get:
summary: GET /reports/{id}
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
'200':
description: OK
delete:
summary: DELETE /reports/{id}
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
'200':
description: OK

components:
securitySchemes:
BearerAuth:
Expand Down
Loading
Loading