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 .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.git
**/node_modules
2 changes: 2 additions & 0 deletions .github/workflows/lambda-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ jobs:
with:
node-version: '20'

- name: Build shared lambda-auth package
run: npm ci --prefix shared/lambda-auth && npm run build --prefix shared/lambda-auth
- name: Install dependencies
working-directory: ${{ matrix.lambda }}
run: npm ci --legacy-peer-deps
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/lambda-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ jobs:
node-version: '20'
- name: Setup database schema
run: psql postgres://branch_dev:password@localhost:5432/branch_db -f apps/backend/db/db_setup.sql
- name: Build shared lambda-auth package
run: npm ci --prefix shared/lambda-auth && npm run build --prefix shared/lambda-auth
- name: Install dependencies
working-directory: ${{ matrix.lambda }}
run: npm ci --legacy-peer-deps
Expand Down
24 changes: 14 additions & 10 deletions apps/backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ services:
# Users Service
users:
build:
context: ./lambdas/users
dockerfile: Dockerfile
context: ../..
dockerfile: apps/backend/lambdas/users/Dockerfile
container_name: branch-users
restart: unless-stopped
environment:
Expand All @@ -36,6 +36,8 @@ services:
DB_USER: ${DB_USER:-branch_dev}
DB_PASSWORD: ${DB_PASSWORD:-password}
DB_NAME: ${DB_NAME:-branch_db}
COGNITO_USER_POOL_ID: ${COGNITO_USER_POOL_ID}
COGNITO_CLIENT_ID: ${COGNITO_CLIENT_ID}
ports:
- '3001:3000'
depends_on:
Expand All @@ -45,8 +47,8 @@ services:
# Projects Service
projects:
build:
context: ./lambdas/projects
dockerfile: Dockerfile
context: ../..
dockerfile: apps/backend/lambdas/projects/Dockerfile
container_name: branch-projects
restart: unless-stopped
environment:
Expand All @@ -55,6 +57,8 @@ services:
DB_USER: ${DB_USER:-branch_dev}
DB_PASSWORD: ${DB_PASSWORD:-password}
DB_NAME: ${DB_NAME:-branch_db}
COGNITO_USER_POOL_ID: ${COGNITO_USER_POOL_ID}
COGNITO_CLIENT_ID: ${COGNITO_CLIENT_ID}
ports:
- '3002:3000'
depends_on:
Expand All @@ -64,8 +68,8 @@ services:
# Donors Service
donors:
build:
context: ./lambdas/donors
dockerfile: Dockerfile
context: ../..
dockerfile: apps/backend/lambdas/donors/Dockerfile
container_name: branch-donors
restart: unless-stopped
environment:
Expand All @@ -85,8 +89,8 @@ services:
# Expenditures Service
expenditures:
build:
context: ./lambdas/expenditures
dockerfile: Dockerfile
context: ../..
dockerfile: apps/backend/lambdas/expenditures/Dockerfile
container_name: branch-expenditures
restart: unless-stopped
environment:
Expand All @@ -106,8 +110,8 @@ services:
# Reports Service
reports:
build:
context: ./lambdas/reports
dockerfile: Dockerfile
context: ../..
dockerfile: apps/backend/lambdas/reports/Dockerfile
container_name: branch-reports
restart: unless-stopped
environment:
Expand Down
20 changes: 8 additions & 12 deletions apps/backend/lambdas/donors/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
FROM node:20-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./
WORKDIR /shared/lambda-auth
COPY shared/lambda-auth/package.json shared/lambda-auth/tsconfig.json ./
COPY shared/lambda-auth/src ./src/
RUN npm install && npm run build

# Install dependencies
RUN npm install

# Copy source files
COPY . .
WORKDIR /app
COPY apps/backend/lambdas/donors/package*.json ./
RUN npm install --no-package-lock
COPY apps/backend/lambdas/donors/ .

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/donors/health || exit 1

# Start the dev server
CMD ["npm", "run", "dev"]
84 changes: 6 additions & 78 deletions apps/backend/lambdas/donors/auth.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,10 @@
import { CognitoJwtVerifier } from 'aws-jwt-verify';
import type { AuthenticatedUser, AuthContext } from '@branch/types';
import { authenticateRequest as _authenticateRequest } from '@branch/lambda-auth';
import db from './db';

export type { AuthenticatedUser, AuthContext };
export * from '@branch/lambda-auth';

const COGNITO_USER_POOL_ID = process.env.COGNITO_USER_POOL_ID!;
const COGNITO_CLIENT_ID = process.env.COGNITO_APP_CLIENT_ID!;

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

function getVerifier() {
if (!verifier) {
if (!COGNITO_USER_POOL_ID) {
throw new Error('COGNITO_USER_POOL_ID environment variable is not set');
}
verifier = CognitoJwtVerifier.create({
userPoolId: COGNITO_USER_POOL_ID,
tokenUse: 'access',
clientId: COGNITO_CLIENT_ID,
});
}
return verifier;
}

/**
* Encode a JWT token
*/
function extractToken(event: any): string | null {
const authHeader = event.headers?.authorization || event.headers?.Authorization;

if (!authHeader) {
return null;
}

const parts = authHeader.split(' ');
if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') {
return parts[1];
}

return authHeader;
export async function authenticateRequest(
event: any,
): Promise<import('@branch/lambda-auth').AuthContext> {
return _authenticateRequest(db, event);
}

export async function authenticateRequest(event: any): Promise<AuthContext> {
const token = extractToken(event);

if (!token) {
return { isAuthenticated: false };
}

try {
const payload = await getVerifier().verify(token);

const dbUser = await db
.selectFrom('branch.users')
.where('cognito_sub', '=', payload.sub)
.selectAll()
.executeTakeFirst();

if (!dbUser) {
return { isAuthenticated: false };
}

const user: AuthenticatedUser = {
cognitoSub: payload.sub,
email: payload.email,
userId: dbUser.user_id,
isAdmin: dbUser.is_admin || false,
cognitoGroups: payload['cognito:groups'] || [],
};

if (user.cognitoGroups?.includes('Admins')) {
user.isAdmin = true;
}

return { user, isAuthenticated: true };
} catch (error) {
console.error('Authentication error:', error);
return { isAuthenticated: false };
}
}
2 changes: 1 addition & 1 deletion apps/backend/lambdas/donors/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {

const authContext = await authenticateRequest(event);
if (!authContext.isAuthenticated) {
return json(401, { message: 'Unauthorized' });
return json(401, { message: 'Authentication required' });
}

// >>> ROUTES-START (do not remove this marker)
Expand Down
15 changes: 15 additions & 0 deletions apps/backend/lambdas/donors/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/backend/lambdas/donors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"typescript": "^5.4.5"
},
"dependencies": {
"@branch/lambda-auth": "file:../../../../shared/lambda-auth",
"aws-jwt-verify": "^5.1.1",
"jest": "^30.2.0",
"kysely": "^0.28.8",
Expand Down
20 changes: 8 additions & 12 deletions apps/backend/lambdas/expenditures/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
FROM node:20-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./
WORKDIR /shared/lambda-auth
COPY shared/lambda-auth/package.json shared/lambda-auth/tsconfig.json ./
COPY shared/lambda-auth/src ./src/
RUN npm install && npm run build

# Install dependencies
RUN npm install

# Copy source files
COPY . .
WORKDIR /app
COPY apps/backend/lambdas/expenditures/package*.json ./
RUN npm install --no-package-lock
COPY apps/backend/lambdas/expenditures/ .

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/expenditures/health || exit 1

# Start the dev server
CMD ["npm", "run", "dev"]
93 changes: 6 additions & 87 deletions apps/backend/lambdas/expenditures/auth.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,10 @@
import { CognitoJwtVerifier } from 'aws-jwt-verify';
import type { AuthenticatedUser, AuthContext } from '@branch/types';
import { authenticateRequest as _authenticateRequest } from '@branch/lambda-auth';
import db from './db';

export type { AuthenticatedUser, AuthContext };
export * from '@branch/lambda-auth';

// 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() {
if (!verifier) {
if (!COGNITO_USER_POOL_ID) {
throw new Error('COGNITO_USER_POOL_ID environment variable is not set');
}
verifier = CognitoJwtVerifier.create({
userPoolId: COGNITO_USER_POOL_ID,
tokenUse: 'access',
clientId: COGNITO_CLIENT_ID,
});
}
return verifier;
}

/**
* Extract JWT token from Authorization header
*/
function extractToken(event: any): string | null {
const authHeader = event.headers?.Authorization || event.headers?.authorization;

if (!authHeader) {
return null;
}

const parts = authHeader.split(' ');
if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') {
return parts[1];
}

return authHeader;
}

/**
* Verify and decode Cognito JWT token, then load user from database
*/
export async function authenticateRequest(event: any): Promise<AuthContext> {
const token = extractToken(event);

if (!token) {
return { isAuthenticated: false };
}

try {
const payload = await getVerifier().verify(token);

const dbUser = await db
.selectFrom('branch.users')
.where('cognito_sub', '=', payload.sub)
.selectAll()
.executeTakeFirst();

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

const user: AuthenticatedUser = {
cognitoSub: payload.sub,
userId: dbUser.user_id,
email: payload.email as string | undefined,
isAdmin: dbUser.is_admin === true,
cognitoGroups: payload['cognito:groups'] as string[] | undefined,
};

if (user.cognitoGroups?.includes('Admins')) {
user.isAdmin = true;
}

return {
user,
isAuthenticated: true,
};
} catch (error) {
console.error('Token verification failed:', error);
return { isAuthenticated: false };
}
export async function authenticateRequest(
event: any,
): Promise<import('@branch/lambda-auth').AuthContext> {
return _authenticateRequest(db, event);
}

Loading
Loading