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
2 changes: 2 additions & 0 deletions apps/backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ services:
DB_PASSWORD: ${DB_PASSWORD:-password}
DB_NAME: ${DB_NAME:-branch_db}
JWT_SECRET: ${JWT_SECRET:-dev-secret-change-in-production}
COGNITO_CLIENT_ID: ${COGNITO_CLIENT_ID}
COGNITO_USER_POOL_ID: ${COGNITO_USER_POOL_ID}
ports:
- '3006:3000'
depends_on:
Expand Down
145 changes: 144 additions & 1 deletion apps/backend/lambdas/auth/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ import {
SignUpCommand,
SignUpCommandInput,
AdminDeleteUserCommand,
InitiateAuthCommand,
InitiateAuthCommandInput,
ConfirmSignUpCommandInput,
ConfirmSignUpCommand,
ResendConfirmationCodeCommand,
ResendConfirmationCodeCommandInput,
GlobalSignOutCommand,
GlobalSignOutCommandInput,
} from '@aws-sdk/client-cognito-identity-provider';
import { CognitoUser, CognitoUserPool, AuthenticationDetails } from 'amazon-cognito-identity-js';
import db from './db';

// Initialize Cognito client
Expand All @@ -13,6 +22,7 @@ const cognitoClient = new CognitoIdentityProviderClient({
});

const USER_POOL_CLIENT_ID = process.env.COGNITO_CLIENT_ID || '';
const USER_POOL_ID = process.env.COGNITO_USER_POOL_ID || '';

export const handler = async (event: any): Promise<APIGatewayProxyResult> => {
try {
Expand All @@ -35,7 +45,84 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {
if (normalizedPath === '/register' && method === 'POST') {
return await handleRegister(event);
}
// <<< ROUTES-END


// POST /login
if (normalizedPath === '/login' && method === 'POST') {
return await handleLogin(event);
}

// POST /verify-email
if (normalizedPath === '/verify-email' && method === 'POST') {
const body = event.body ? JSON.parse(event.body) as Record<string, unknown> : {};
const { email, code } = body;
if (!email || !code) {
return json(400, { message: 'email and code are required' });
}
const params: ConfirmSignUpCommandInput = {
ClientId: USER_POOL_CLIENT_ID,
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' });
}
return json(200, { message: `Email verified successfully for ${email}, session: ${response.Session}` });
}

// POST /resend-code
if (normalizedPath === '/resend-code' && 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: ResendConfirmationCodeCommandInput = {
ClientId: USER_POOL_CLIENT_ID,
Username: email as string,
};
const response = await cognitoClient.send(new ResendConfirmationCodeCommand(params));
if (!response.CodeDeliveryDetails) {
return json(400, { message: 'Failed to resend code' });
}
return json(200, { message: `Code resent successfully for ${email}, delivery details: ${response.CodeDeliveryDetails}` });
}

// POST /logout
if (normalizedPath === '/logout' && method === 'POST') {
const authHeader = event.headers?.authorization || event.headers?.Authorization;
if (!authHeader) {
return json(401, { message: 'Authorization header is required' });
}

// Extract token (remove "Bearer " prefix if present)
const accessToken = authHeader.startsWith('Bearer ')
? authHeader.slice(7)
: authHeader;

if (!accessToken) {
return json(401, { message: 'Access token is required' });
}

const params: GlobalSignOutCommandInput = {
AccessToken: accessToken,
};

try {
await cognitoClient.send(new GlobalSignOutCommand(params));
return json(200, { message: 'Logged out successfully' });
} catch (error: any) {
console.error('Logout error:', error);

if (error.name === 'NotAuthorizedException') {
return json(401, { message: 'Invalid or expired token' });
}

return json(500, { message: 'Failed to logout' });
}
}
// <<< ROUTES-END

return json(404, { message: 'Not Found', path: normalizedPath, method });
} catch (err) {
Expand All @@ -44,6 +131,62 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {
}
};

async function handleLogin(event: any): Promise<APIGatewayProxyResult> {
let body: Record<string, unknown>;
try {
body = event.body ? JSON.parse(event.body) as Record<string, unknown> : {};
} catch (e) {
return json(400, { message: 'Invalid JSON in request body' });
}

const { email, password } = body;
if (!email || !password) {
return json(400, { message: 'email and password are required' });
}

const userPool = new CognitoUserPool({
UserPoolId: USER_POOL_ID,
ClientId: USER_POOL_CLIENT_ID,
});

const cognitoUser = new CognitoUser({
Username: email as string,
Pool: userPool,
});

const authDetails = new AuthenticationDetails({
Username: email as string,
Password: password as string,
});

return new Promise<APIGatewayProxyResult>((resolve) => {
cognitoUser.authenticateUser(authDetails, {
onSuccess: (result) => {
resolve(json(200, {
AccessToken: result.getAccessToken().getJwtToken(),
IdToken: result.getIdToken().getJwtToken(),
RefreshToken: result.getRefreshToken().getToken(),
}));
},
onFailure: (err) => {
console.error('SRP auth error:', err);
if (err.code === 'UserNotConfirmedException') {
resolve(json(403, { message: 'Email not verified' }));
} else if (err.code === 'NotAuthorizedException') {
resolve(json(401, { message: 'Invalid email or password' }));
} else if (err.code === 'UserNotFoundException') {
resolve(json(401, { message: 'Invalid email or password' }));
} else {
resolve(json(500, { message: 'Authentication failed', error: err.message }));
}
},
newPasswordRequired: (userAttributes) => {
resolve(json(403, { message: 'Password change required', userAttributes }));
},
});
});
}

async function handleRegister(event: any): Promise<APIGatewayProxyResult> {
try {
// Parse request body
Expand Down
54 changes: 54 additions & 0 deletions apps/backend/lambdas/auth/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,57 @@ paths:
message:
type: string
example: Failed to create user account

/login:
post:
summary: POST /login
parameters:
- in: query
name: username
required: false
schema:
type: string
responses:
'200':
description: OK

/verify-email:
post:
summary: POST /verify-email
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
code:
type: string
responses:
'200':
description: OK

/resend-code:
post:
summary: POST /resend-code
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
responses:
'200':
description: OK

/logout:
post:
summary: POST /logout
responses:
'200':
description: OK
Loading