From f209276e774e2a1095109105abc200db86993a15 Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Mon, 13 Jul 2026 19:49:58 -0400 Subject: [PATCH 01/12] infra: lock Cognito to admin-only signup + wire Cognito env vars to lambdas --- infrastructure/aws/cognito.tf | 11 +++++++++++ infrastructure/aws/lambda.tf | 33 +++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/infrastructure/aws/cognito.tf b/infrastructure/aws/cognito.tf index dc0323cd..20950351 100644 --- a/infrastructure/aws/cognito.tf +++ b/infrastructure/aws/cognito.tf @@ -49,6 +49,17 @@ resource "aws_cognito_user_pool" "branch_user_pool" { } } + # Disable self-signup — accounts are created by admins only + admin_create_user_config { + allow_admin_create_user_only = true + + invite_message_template { + email_subject = "Your BRANCH Accounting Platform Invitation" + email_message = "You have been invited to the BRANCH Accounting Platform. Your username is {username} and temporary password is {####}. Log in and set a new password to get started." + sms_message = "BRANCH invite: username {username}, temp password {####}." + } + } + # Email configuration (using Cognito default for now) email_configuration { email_sending_account = "COGNITO_DEFAULT" diff --git a/infrastructure/aws/lambda.tf b/infrastructure/aws/lambda.tf index 4aa36d0e..f4ccb7e5 100644 --- a/infrastructure/aws/lambda.tf +++ b/infrastructure/aws/lambda.tf @@ -17,6 +17,25 @@ resource "aws_iam_role_policy_attachment" "lambda_basic" { policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } +# Cognito admin permissions for the users lambda (AdminCreateUser / AdminDeleteUser) +resource "aws_iam_role_policy" "lambda_cognito" { + name = "branch-lambda-cognito-policy" + role = aws_iam_role.lambda_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = [ + "cognito-idp:AdminCreateUser", + "cognito-idp:AdminDeleteUser", + "cognito-idp:AdminGetUser", + ] + Resource = aws_cognito_user_pool.branch_user_pool.arn + }] + }) +} + # Get AWS account ID for unique bucket naming data "aws_caller_identity" "current" {} @@ -97,12 +116,14 @@ resource "aws_lambda_function" "functions" { environment { variables = { - NODE_ENV = "production" - DB_HOST = aws_db_instance.branch_rds.address - DB_USER = data.infisical_secrets.rds_folder.secrets["username"].value - DB_PASSWORD = data.infisical_secrets.rds_folder.secrets["password"].value - DB_PORT = try(data.infisical_secrets.rds_folder.secrets["db_port"].value, "5432") - DB_NAME = try(data.infisical_secrets.rds_folder.secrets["db_name"].value, aws_db_instance.branch_rds.db_name) + NODE_ENV = "production" + DB_HOST = aws_db_instance.branch_rds.address + DB_USER = data.infisical_secrets.rds_folder.secrets["username"].value + DB_PASSWORD = data.infisical_secrets.rds_folder.secrets["password"].value + DB_PORT = try(data.infisical_secrets.rds_folder.secrets["db_port"].value, "5432") + DB_NAME = try(data.infisical_secrets.rds_folder.secrets["db_name"].value, aws_db_instance.branch_rds.db_name) + COGNITO_USER_POOL_ID = aws_cognito_user_pool.branch_user_pool.id + COGNITO_CLIENT_ID = aws_cognito_user_pool_client.branch_client.id } } } \ No newline at end of file From cd1c6f0a64dab9b06d9a08d257a43c4ac693c9c1 Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Mon, 13 Jul 2026 19:50:27 -0400 Subject: [PATCH 02/12] feat(users): provision Cognito account on POST /users --- apps/backend/lambdas/users/handler.ts | 54 ++++++++++++++++++++++--- apps/backend/lambdas/users/package.json | 1 + 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/apps/backend/lambdas/users/handler.ts b/apps/backend/lambdas/users/handler.ts index e82348e0..3fb5de65 100644 --- a/apps/backend/lambdas/users/handler.ts +++ b/apps/backend/lambdas/users/handler.ts @@ -1,8 +1,18 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; +import { + CognitoIdentityProviderClient, + AdminCreateUserCommand, + AdminDeleteUserCommand, +} from '@aws-sdk/client-cognito-identity-provider'; import db from './db' import { authenticateRequest, checkAuthorization, AuthContext } from './auth'; import { UserValidationUtils } from './validation-utils'; +const cognitoClient = new CognitoIdentityProviderClient({ + region: process.env.AWS_REGION || 'us-east-2', +}); +const USER_POOL_ID = process.env.COGNITO_USER_POOL_ID || ''; + function requireAuth(authContext: AuthContext, level: Parameters[1], resourceUserId?: number | string): APIGatewayProxyResult | undefined { const authCheck = checkAuthorization(authContext, level, resourceUserId); if (!authCheck.allowed) { @@ -216,7 +226,7 @@ export const handler = async (event: any): Promise => { const isAdmin = isAdminResult.value as boolean; const profile_image = profileImageResult.value ?? undefined; - // Check if user with this email already exists + // Check if user with this email already exists in DB const existingUser = await db .selectFrom('branch.users') .where('email', '=', email) @@ -226,15 +236,49 @@ export const handler = async (event: any): Promise => { if (existingUser) { return json(409, { message: 'User with this email already exists' }); } - - // insert new user (user_id auto-increments) + + // Create user in Cognito via AdminCreateUser — sends invite email with temp password + let cognitoSub: string; + try { + const cognitoResponse = await cognitoClient.send(new AdminCreateUserCommand({ + UserPoolId: USER_POOL_ID, + Username: email, + DesiredDeliveryMediums: ['EMAIL'], + UserAttributes: [ + { Name: 'email', Value: email }, + { Name: 'email_verified', Value: 'true' }, + { Name: 'name', Value: name }, + ], + })); + const sub = cognitoResponse.User?.Attributes?.find(a => a.Name === 'sub')?.Value; + if (!sub) throw new Error('No sub returned from AdminCreateUser'); + cognitoSub = sub; + } catch (err: any) { + console.error('Cognito AdminCreateUser error:', err); + if (err.name === 'UsernameExistsException') { + return json(409, { message: 'User with this email already exists' }); + } + return json(500, { message: 'Failed to create user in authentication service' }); + } + + // Insert into database with cognito_sub try { await db .insertInto('branch.users') - .values({ email, name, is_admin: isAdmin, profile_image }) + .values({ cognito_sub: cognitoSub, email, name, is_admin: isAdmin, profile_image }) .execute(); - } catch (err) { + } catch (err: any) { console.error('Database insert error:', err); + // Rollback: delete Cognito user to keep systems in sync + try { + await cognitoClient.send(new AdminDeleteUserCommand({ + UserPoolId: USER_POOL_ID, + Username: email, + })); + console.log('Rolled back Cognito user after database failure'); + } catch (rollbackErr) { + console.error('Failed to rollback Cognito user:', rollbackErr); + } return json(500, { message: 'Failed to create user' }); } diff --git a/apps/backend/lambdas/users/package.json b/apps/backend/lambdas/users/package.json index 72a66e8e..35345665 100644 --- a/apps/backend/lambdas/users/package.json +++ b/apps/backend/lambdas/users/package.json @@ -24,6 +24,7 @@ "typescript": "^5.4.5" }, "dependencies": { + "@aws-sdk/client-cognito-identity-provider": "^3.978.0", "@branch/lambda-auth": "file:../../../../shared/lambda-auth", "aws-jwt-verify": "^5.1.1", "kysely": "^0.28.8", From c60bc1af727fc8504168545b6472fee8359cf0aa Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Mon, 13 Jul 2026 19:51:25 -0400 Subject: [PATCH 03/12] feat(auth): disable self-register; wire NEW_PASSWORD_REQUIRED challength --- apps/backend/lambdas/auth/handler.ts | 246 +++++++++------------------ 1 file changed, 77 insertions(+), 169 deletions(-) diff --git a/apps/backend/lambdas/auth/handler.ts b/apps/backend/lambdas/auth/handler.ts index 5136c431..3bff87ac 100644 --- a/apps/backend/lambdas/auth/handler.ts +++ b/apps/backend/lambdas/auth/handler.ts @@ -1,15 +1,11 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import { CognitoIdentityProviderClient, - SignUpCommand, - SignUpCommandInput, - AdminDeleteUserCommand, InitiateAuthCommand, - InitiateAuthCommandInput, + RespondToAuthChallengeCommand, ConfirmSignUpCommand, ConfirmSignUpCommandInput, ResendConfirmationCodeCommand, - GlobalSignOutCommand, GlobalSignOutCommandInput, ForgotPasswordCommand, @@ -17,7 +13,6 @@ import { ConfirmForgotPasswordCommand, ConfirmForgotPasswordCommandInput, } from '@aws-sdk/client-cognito-identity-provider'; -import { CognitoUser, CognitoUserPool, AuthenticationDetails } from 'amazon-cognito-identity-js'; import db from './db'; // Initialize Cognito client (region defaults to us-east-2) @@ -26,7 +21,6 @@ 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 => { try { @@ -53,16 +47,20 @@ export const handler = async (event: any): Promise => { // >>> ROUTES-START (do not remove this marker) // CLI-generated routes will be inserted here - // POST /register + // POST /register — disabled: accounts are created by admins only if (normalizedPath === '/register' && method === 'POST') { - return await handleRegister(event); + return json(410, { message: 'Self-registration is not available. Contact an administrator to create an account.' }); } - // POST /login if (normalizedPath === '/login' && method === 'POST') { return await handleLogin(event); } + + // POST /set-password — complete NEW_PASSWORD_REQUIRED challenge for invited users + if (normalizedPath === '/set-password' && method === 'POST') { + return await handleSetPassword(event); + } // POST /verify-email if (normalizedPath === '/verify-email' && method === 'POST') { @@ -255,183 +253,93 @@ async function handleLogin(event: any): Promise { 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((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 })); + try { + const response = await cognitoClient.send(new InitiateAuthCommand({ + AuthFlow: 'USER_PASSWORD_AUTH', + ClientId: USER_POOL_CLIENT_ID, + AuthParameters: { + USERNAME: (email as string).toLowerCase(), + PASSWORD: password as string, }, - }); - }); -} + })); -async function handleRegister(event: any): Promise { - try { - // Parse request body - const body = event.body ? JSON.parse(event.body) : {}; - const { email, password, name } = body; - - // Validate required fields - if (!email || !password || !name) { - return json(400, { - message: 'Missing required fields', - required: ['email', 'password', 'name'], + if (response.ChallengeName === 'NEW_PASSWORD_REQUIRED') { + return json(200, { + challengeName: 'NEW_PASSWORD_REQUIRED', + session: response.Session, + email: (email as string).toLowerCase(), }); } - // Validate email format - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { - return json(400, { message: 'Invalid email format' }); - } - - // Validate password requirements - if (password.length < 8) { - return json(400, { message: 'Password must be at least 8 characters long' }); - } - if (!/[a-z]/.test(password)) { - return json(400, { message: 'Password must contain at least one lowercase letter' }); - } - if (!/[A-Z]/.test(password)) { - return json(400, { message: 'Password must contain at least one uppercase letter' }); + return json(200, { + AccessToken: response.AuthenticationResult!.AccessToken, + IdToken: response.AuthenticationResult!.IdToken, + RefreshToken: response.AuthenticationResult!.RefreshToken, + }); + } catch (err: any) { + console.error('Login error:', err); + if (err.name === 'NotAuthorizedException' || err.name === 'UserNotFoundException') { + return json(401, { message: 'Invalid email or password' }); } - if (!/[0-9]/.test(password)) { - return json(400, { message: 'Password must contain at least one number' }); + if (err.name === 'UserNotConfirmedException') { + return json(403, { message: 'Email not verified' }); } + return json(500, { message: 'Authentication failed' }); + } +} - // Validate name - if (name.trim().length < 2) { - return json(400, { message: 'Name must be at least 2 characters long' }); - } +async function handleSetPassword(event: any): Promise { + let body: Record; + try { + body = event.body ? JSON.parse(event.body) as Record : {}; + } catch (e) { + return json(400, { message: 'Invalid JSON in request body' }); + } - // Check if user already exists in database - const existingUser = await db - .selectFrom('branch.users') - .where('email', '=', email.toLowerCase()) - .selectAll() - .executeTakeFirst(); + const { email, session, newPassword } = body; + if (!email || !session || !newPassword) { + return json(400, { message: 'email, session, and newPassword are required' }); + } - if (existingUser) { - return json(409, { message: 'User with this email already exists' }); - } + const pwd = newPassword as string; + if ( + pwd.length < 8 || + !/[a-z]/.test(pwd) || + !/[A-Z]/.test(pwd) || + !/[0-9]/.test(pwd) + ) { + return json(400, { message: 'Password must be at least 8 characters and include uppercase, lowercase, and a number' }); + } - // Prepare Cognito SignUp parameters - const signUpParams: SignUpCommandInput = { + try { + const response = await cognitoClient.send(new RespondToAuthChallengeCommand({ + ChallengeName: 'NEW_PASSWORD_REQUIRED', ClientId: USER_POOL_CLIENT_ID, - Username: email.toLowerCase(), - Password: password, - UserAttributes: [ - { - Name: 'email', - Value: email.toLowerCase(), - }, - { - Name: 'name', - Value: name.trim(), - }, - ], - }; - - // Register user in Cognito - let cognitoUserSub: string; - try { - const command = new SignUpCommand(signUpParams); - const response = await cognitoClient.send(command); - cognitoUserSub = response.UserSub!; - } catch (error: any) { - console.error('Cognito registration error:', error); - - // Handle specific Cognito errors - if (error.name === 'UsernameExistsException') { - return json(409, { message: 'User with this email already exists' }); - } - if (error.name === 'InvalidPasswordException') { - return json(400, { message: 'Password does not meet requirements' }); - } - if (error.name === 'InvalidParameterException') { - return json(400, { message: error.message || 'Invalid parameters provided' }); - } + Session: session as string, + ChallengeResponses: { + USERNAME: (email as string).toLowerCase(), + NEW_PASSWORD: pwd, + }, + })); - return json(500, { message: 'Failed to register user in authentication service' }); + return json(200, { + AccessToken: response.AuthenticationResult!.AccessToken, + IdToken: response.AuthenticationResult!.IdToken, + RefreshToken: response.AuthenticationResult!.RefreshToken, + }); + } catch (err: any) { + console.error('Set password error:', err); + if (err.name === 'InvalidPasswordException') { + return json(400, { message: 'Password does not meet requirements (min 8 chars, uppercase, lowercase, number)' }); } - - // Create user in database - try { - await db - .insertInto('branch.users') - .values({ - cognito_sub: cognitoUserSub, - email: email.toLowerCase(), - name: name.trim(), - is_admin: false, - }) - .execute(); - } catch (dbError: any) { - console.error('Database insert error:', dbError); - - // Rollback: Delete user from Cognito if database insert fails - try { - await cognitoClient.send( - new AdminDeleteUserCommand({ - UserPoolId: process.env.COGNITO_USER_POOL_ID || '', - Username: email.toLowerCase(), - }) - ); - console.log('Rolled back Cognito user after database failure'); - } catch (rollbackError) { - console.error('Failed to rollback Cognito user:', rollbackError); - } - - return json(500, { message: 'Failed to create user account' }); + if (err.name === 'ExpiredCodeException' || err.name === 'NotAuthorizedException') { + return json(401, { message: 'Session expired. Please log in again.' }); } - - return json(201, { - message: 'User registered successfully', - userId: cognitoUserSub, - email: email.toLowerCase(), - name: name.trim(), - emailVerificationRequired: true, - details: 'Please check your email for verification code', - }); - } catch (error: any) { - console.error('Registration error:', error); - return json(500, { message: 'Internal server error during registration' }); + return json(500, { message: 'Failed to set password' }); } } + function json(statusCode: number, body: unknown): APIGatewayProxyResult { return { statusCode, From f5b8b30c9fb1cf1e150489627e905c8a6746ce16 Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Mon, 13 Jul 2026 19:51:51 -0400 Subject: [PATCH 04/12] feat(frontend): set-password flow for invited users --- apps/frontend/src/app/login/page.tsx | 75 +++++++++++++++++++++-- apps/frontend/src/context/AuthContext.tsx | 60 +++++++++--------- 2 files changed, 101 insertions(+), 34 deletions(-) diff --git a/apps/frontend/src/app/login/page.tsx b/apps/frontend/src/app/login/page.tsx index 6a79314e..c2a338d0 100644 --- a/apps/frontend/src/app/login/page.tsx +++ b/apps/frontend/src/app/login/page.tsx @@ -8,15 +8,20 @@ import { useAuth } from '@/context/AuthContext'; import { useRouter } from 'next/navigation'; export default function LoginPage() { - const { login } = useAuth(); + const { login, setPassword } = useAuth(); const router = useRouter(); const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); + const [password, setPasswordValue] = useState(''); const [emailError, setEmailError] = useState(''); const [passwordError, setPasswordError] = useState(''); const [isLoading, setIsLoading] = useState(false); + // NEW_PASSWORD_REQUIRED challenge state + const [challenge, setChallenge] = useState<{ session: string; email: string } | null>(null); + const [newPassword, setNewPassword] = useState(''); + const [newPasswordError, setNewPasswordError] = useState(''); + function validate(): boolean { let valid = true; @@ -42,8 +47,12 @@ export default function LoginPage() { setIsLoading(true); try { - await login(email, password); - router.push('/'); + const result = await login(email, password); + if (result?.challengeName === 'NEW_PASSWORD_REQUIRED') { + setChallenge({ session: result.session, email: result.email }); + } else { + router.push('/'); + } } catch { setPasswordError('Incorrect email or password. Please try again.'); } finally { @@ -51,6 +60,62 @@ export default function LoginPage() { } } + async function handleSetPassword() { + if (!newPassword) { + setNewPasswordError('Please enter a new password'); + return; + } + if ( + newPassword.length < 8 || + !/[A-Z]/.test(newPassword) || + !/[a-z]/.test(newPassword) || + !/[0-9]/.test(newPassword) + ) { + setNewPasswordError('Password must be at least 8 characters and include uppercase, lowercase, and a number'); + return; + } + setNewPasswordError(''); + setIsLoading(true); + try { + await setPassword(challenge!.email, challenge!.session, newPassword); + router.push('/'); + } catch { + setNewPasswordError('Failed to set password. Please try again.'); + } finally { + setIsLoading(false); + } + } + + if (challenge) { + return ( +
+
+

Set Password

+

+ Welcome! Please set a permanent password to continue. +

+
+ setNewPassword(value)} + /> +
+ +
+
+ ); + } + return (
@@ -71,7 +136,7 @@ export default function LoginPage() { errorMessage={passwordError} isError={!!passwordError} value={password} - onChange={(value) => setPassword(value)} + onChange={(value) => setPasswordValue(value)} />
+

Core BRANCH Facilitation Team

{facilitationTeam.map(user => ( @@ -50,6 +65,12 @@ export default function AccountsPage() { ))}
+ setIsModalOpen(false)} + onSuccess={() => setIsModalOpen(false)} + token={getAccessToken() ?? ''} + /> ); -} \ No newline at end of file +} diff --git a/apps/frontend/src/app/components/AddUserModal.tsx b/apps/frontend/src/app/components/AddUserModal.tsx new file mode 100644 index 00000000..721caa70 --- /dev/null +++ b/apps/frontend/src/app/components/AddUserModal.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { useState } from 'react'; +import { Button, Dialog, Portal, CloseButton, Stack } from '@chakra-ui/react'; +import TextInputField from './TextInputField'; +import { apiFetch } from '@/lib/api'; + +interface AddUserModalProps { + open: boolean; + onClose: () => void; + onSuccess: () => void; + token: string; +} + +export default function AddUserModal({ open, onClose, onSuccess, token }: AddUserModalProps) { + const [email, setEmail] = useState(''); + const [name, setName] = useState(''); + const [isAdmin, setIsAdmin] = useState(false); + const [emailError, setEmailError] = useState(''); + const [nameError, setNameError] = useState(''); + const [submitError, setSubmitError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + function resetForm() { + setEmail(''); + setName(''); + setIsAdmin(false); + setEmailError(''); + setNameError(''); + setSubmitError(null); + } + + function handleClose() { + resetForm(); + onClose(); + } + + async function handleSubmit() { + const hasEmailError = !email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + const hasNameError = !name.trim() || name.trim().length < 2; + + setEmailError(hasEmailError ? 'Please enter a valid email address' : ''); + setNameError(hasNameError ? 'Name must be at least 2 characters' : ''); + setSubmitError(null); + + if (hasEmailError || hasNameError) return; + + setIsLoading(true); + try { + await apiFetch('/users/', { + method: 'POST', + token, + body: JSON.stringify({ email: email.toLowerCase(), name: name.trim(), isAdmin }), + }); + resetForm(); + onSuccess(); + } catch (err) { + setSubmitError(err instanceof Error ? err.message : 'Failed to create user'); + } finally { + setIsLoading(false); + } + } + + return ( + { if (!e.open) handleClose(); }}> + + + + + + + Add User + + + + + + + + + {submitError && ( +

{submitError}

+ )} +
+
+ + + + +
+
+
+
+ ); +} From 6b2c50aca19b1d5e426b2e3c7f840f19bad5cc6f Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Sun, 2 Aug 2026 19:34:31 -0400 Subject: [PATCH 06/12] fixes for ci (auth imports in accounts page & updated users lamda package lock) --- apps/backend/lambdas/users/package-lock.json | 342 ++++++++++++++++++- apps/frontend/src/app/accounts/page.tsx | 3 +- 2 files changed, 342 insertions(+), 3 deletions(-) diff --git a/apps/backend/lambdas/users/package-lock.json b/apps/backend/lambdas/users/package-lock.json index 6efa5120..d3501a4a 100644 --- a/apps/backend/lambdas/users/package-lock.json +++ b/apps/backend/lambdas/users/package-lock.json @@ -8,6 +8,7 @@ "name": "lambda-local", "version": "1.0.0", "dependencies": { + "@aws-sdk/client-cognito-identity-provider": "^3.978.0", "@branch/lambda-auth": "file:../../../../shared/lambda-auth", "aws-jwt-verify": "^5.1.1", "kysely": "^0.28.8", @@ -35,7 +36,11 @@ "aws-jwt-verify": "^5.1.1" }, "devDependencies": { + "@jest/globals": "^30.2.0", + "@types/jest": "^30.0.0", "@types/node": "^20.11.30", + "jest": "^30.2.0", + "ts-jest": "^29.4.5", "typescript": "^5.4.5" } }, @@ -44,6 +49,262 @@ "version": "1.0.0", "dev": true }, + "node_modules/@aws-sdk/client-cognito-identity-provider": { + "version": "3.1101.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity-provider/-/client-cognito-identity-provider-3.1101.0.tgz", + "integrity": "sha512-iINP4bJzVeCN1UDDQ5aRIEaGOCUUf8esOkHp7P7q/mBB+X8icJUitDsCcf4F1poymUO5YK4ArARGF45dH74rSg==", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/credential-provider-node": "^3.972.76", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.977.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.4.tgz", + "integrity": "sha512-CEkcQlMOQJCvul60U7wdAOACjtdgFWDsfJI+6wUOGdhGNV2lGbuJpi/R50QLpFG3Tp+sQxa/RmzC3X7KHbhuTA==", + "dependencies": { + "@aws-sdk/types": "^3.974.2", + "@aws-sdk/xml-builder": "^3.972.37", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.31.1", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.65", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.65.tgz", + "integrity": "sha512-lJT2aRw9wCV8jPHyFJjdZLD4HTydL6/22AnCSOB8e/LqOc55nEJGLHkJQeSxhn8QiqyjFwPKQFtMw0ovjRUY/g==", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.67", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.67.tgz", + "integrity": "sha512-N7fw/15hSwI/CPxe5ohOyb7O4ge9f5me1gVIn8OIkBRB0squ8OJqQyDyH/HoL+Sb1W5xdC88jVC+bHkw73iu+Q==", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.10.tgz", + "integrity": "sha512-Zh9XRaPnDN9buO7GfWBubS22R6Nq5D6hbyYEMN05LiOnXugm/8WDjUx6y756bSPbdn3aJB2qG4zFW3bN82QhoQ==", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/credential-provider-env": "^3.972.65", + "@aws-sdk/credential-provider-http": "^3.972.67", + "@aws-sdk/credential-provider-login": "^3.972.72", + "@aws-sdk/credential-provider-process": "^3.972.65", + "@aws-sdk/credential-provider-sso": "^3.973.9", + "@aws-sdk/credential-provider-web-identity": "^3.972.71", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.72.tgz", + "integrity": "sha512-zZapIKwaHp7TdTf9hbH1I3CVUdEupmt7FXO/BoTQGC+4h6NkXKWpqF2p5WyfpjurDLHCpSyh+BzMlAg8arqWLA==", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.76.tgz", + "integrity": "sha512-1yzLmRiYSgGC25v7ZZEwJn/auhHHTIHgFOmzL2f36hf1+7jSLcX+1QrAz4760WEzPiiQl8xmlpFhHfl2OoyVzA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.65", + "@aws-sdk/credential-provider-http": "^3.972.67", + "@aws-sdk/credential-provider-ini": "^3.973.10", + "@aws-sdk/credential-provider-process": "^3.972.65", + "@aws-sdk/credential-provider-sso": "^3.973.9", + "@aws-sdk/credential-provider-web-identity": "^3.972.71", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.65", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.65.tgz", + "integrity": "sha512-e5DbbNteOSalN58U83G6kFa4ECLEuGbGqNBHIXE7zYXA/m4GHblIGjFbSH7wYv6gBV8iNSDcRZBKfQZF5vF9nw==", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.9.tgz", + "integrity": "sha512-0V0u4t+KBku9fbh5CPCaC5hUWwSzDafp8nCuDy817zWbp2gz80jO44rMQkiwnZ+k54B+tjAtzRy00DJRGTKGBg==", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/token-providers": "3.1100.0", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.71", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.71.tgz", + "integrity": "sha512-e4dwiRltGAaQ+2yxw57Hj0l/BF3BHiG14+QpYE7bGYBlpAq/fkIri2BDhjWon8c0mhhtd2txQBAkQb9BcTStFg==", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.39.tgz", + "integrity": "sha512-wU5NPnj62Sb7A8xn/Zb+xThe05P3otNtDl37iOIi5DDMeCesNeCckaG+eXWGUs12Z9R34I8CD05TaTe6SIa61g==", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/signature-v4-multi-region": "^3.996.43", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.43.tgz", + "integrity": "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==", + "dependencies": { + "@aws-sdk/types": "^3.974.2", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1100.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1100.0.tgz", + "integrity": "sha512-THf3MkgY3fNJZ3zdgSenLqR7gSE68KccCj1RCKretlG73Ppszvues02VpCUO9NlB/tZDC483FvGCld+AiPCkvg==", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.2.tgz", + "integrity": "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.37.tgz", + "integrity": "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1627,6 +1888,81 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@smithy/core": { + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.31.1.tgz", + "integrity": "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.4.16", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.16.tgz", + "integrity": "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.6.13", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.13.tgz", + "integrity": "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.9.13", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.13.tgz", + "integrity": "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.6.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.12.tgz", + "integrity": "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz", + "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -2335,6 +2671,11 @@ "dev": true, "license": "MIT" }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==" + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -5624,7 +5965,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type-detect": { diff --git a/apps/frontend/src/app/accounts/page.tsx b/apps/frontend/src/app/accounts/page.tsx index 12aa5fec..660ed95c 100644 --- a/apps/frontend/src/app/accounts/page.tsx +++ b/apps/frontend/src/app/accounts/page.tsx @@ -3,9 +3,9 @@ import React, { useState } from 'react'; import StaffCard from '../components/StaffCard'; import AddUserModal from '../components/AddUserModal'; -import { useAuth } from '@/context/AuthContext'; import { Button } from '@chakra-ui/react'; import { User } from '@/types'; +import { getAccessToken } from '@/lib/authTokens'; const mockUsers: User[] = [ { user_id: 1, name: 'Mehana Nagarur', email: 'nagarur.m@northeastern.edu', is_admin: true }, @@ -31,7 +31,6 @@ export const teamMembers = mockUsers.filter(u => !u.is_admin); export default function AccountsPage() { - const { getAccessToken } = useAuth(); const [isModalOpen, setIsModalOpen] = useState(false); return ( From d774dbe6e244d2f91dec299e288e65c9ee40d419 Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Mon, 3 Aug 2026 20:46:25 -0400 Subject: [PATCH 07/12] update tests --- apps/backend/lambdas/users/test/user.unit.test.ts | 10 ++++++++++ apps/backend/lambdas/users/test/users.test.ts | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/apps/backend/lambdas/users/test/user.unit.test.ts b/apps/backend/lambdas/users/test/user.unit.test.ts index 56afd973..661a0d29 100644 --- a/apps/backend/lambdas/users/test/user.unit.test.ts +++ b/apps/backend/lambdas/users/test/user.unit.test.ts @@ -4,6 +4,16 @@ import { describe, test, expect, beforeEach, jest } from '@jest/globals'; jest.mock('../db'); jest.mock('../auth'); +jest.mock('@aws-sdk/client-cognito-identity-provider', () => ({ + CognitoIdentityProviderClient: jest.fn().mockImplementation(() => ({ + send: jest.fn().mockImplementation(async () => ({ + User: { Attributes: [{ Name: 'sub', Value: 'test-cognito-sub-123' }] }, + })), + })), + AdminCreateUserCommand: jest.fn().mockImplementation((args: unknown) => args), + AdminDeleteUserCommand: jest.fn().mockImplementation((args: unknown) => args), +})); + import { handler } from '../handler'; import db from '../db'; import { authenticateRequest, checkAuthorization } from '../auth'; diff --git a/apps/backend/lambdas/users/test/users.test.ts b/apps/backend/lambdas/users/test/users.test.ts index e7f34a0b..163c885a 100644 --- a/apps/backend/lambdas/users/test/users.test.ts +++ b/apps/backend/lambdas/users/test/users.test.ts @@ -6,6 +6,16 @@ import { authenticateRequest, checkAuthorization } from '../auth'; jest.mock('../auth'); +jest.mock('@aws-sdk/client-cognito-identity-provider', () => ({ + CognitoIdentityProviderClient: jest.fn().mockImplementation(() => ({ + send: jest.fn().mockResolvedValue({ + User: { Attributes: [{ Name: 'sub', Value: 'test-cognito-sub-123' }] }, + }), + })), + AdminCreateUserCommand: jest.fn().mockImplementation((args) => args), + AdminDeleteUserCommand: jest.fn().mockImplementation((args) => args), +})); + const mockAuthenticateRequest = authenticateRequest as jest.MockedFunction; const mockCheckAuthorization = checkAuthorization as jest.MockedFunction; From 9c5113906acd0fd41eed658e77f406b0642ca144 Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Wed, 12 Aug 2026 19:25:40 -0400 Subject: [PATCH 08/12] fix(frontend): restore imports dropped in main merge on accounts/page.tsx The merge of main into this branch kept this branch's Button/AddUserModal JSX but took main's import block (which moved mock data into mockUsers.ts), silently dropping Button, AddUserModal, and getAccessToken. Broke frontend-ci's typecheck step. Co-Authored-By: Claude Sonnet 5 --- apps/frontend/src/app/accounts/page.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/frontend/src/app/accounts/page.tsx b/apps/frontend/src/app/accounts/page.tsx index 0b26ee21..0f36a905 100644 --- a/apps/frontend/src/app/accounts/page.tsx +++ b/apps/frontend/src/app/accounts/page.tsx @@ -2,6 +2,9 @@ import React, { useState } from 'react'; import StaffCard from '../components/StaffCard'; +import AddUserModal from '../components/AddUserModal'; +import { Button } from '@chakra-ui/react'; +import { getAccessToken } from '@/lib/authTokens'; import { facilitationTeam, teamMembers } from './mockUsers'; export default function AccountsPage() { From 22076eb8696dd8c271f23410791a07603601ad5d Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Wed, 12 Aug 2026 19:25:46 -0400 Subject: [PATCH 09/12] fix(users): stub Cognito AdminCreateUser response in success-case test The merge of main into this branch kept main's version of the CognitoIdentityProviderClient mock (a bare jest.fn() with no default resolution, added for main's DELETE-route tests), dropping this branch's default that resolved a User.Attributes shape. The POST /users success test never stubbed it itself, so handler.ts's cognitoResponse.User?.Attributes threw against an undefined response, returning 500 instead of 201. Co-Authored-By: Claude Sonnet 5 --- apps/backend/lambdas/users/test/user.unit.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/backend/lambdas/users/test/user.unit.test.ts b/apps/backend/lambdas/users/test/user.unit.test.ts index e60c7e7e..077e5b3c 100644 --- a/apps/backend/lambdas/users/test/user.unit.test.ts +++ b/apps/backend/lambdas/users/test/user.unit.test.ts @@ -336,6 +336,9 @@ describe('POST /users unit tests', () => { describe('Success Cases', () => { beforeEach(() => { mockAdminAuth(); + (mockSend as any).mockResolvedValue({ + User: { Attributes: [{ Name: 'sub', Value: 'test-cognito-sub-123' }] }, + }); }); test('201: successful POST returns 201 status and correct response shape', async () => { From 834be7268bdd953af62fb5f749da013065548ed7 Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Wed, 12 Aug 2026 19:25:54 -0400 Subject: [PATCH 10/12] fix(backend): copy shared/types into every lambda image before building shared/lambda-auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit shared/lambda-auth/src/types.ts imports @branch/types, but none of the per-lambda Dockerfiles copied shared/types into the build context before running shared/lambda-auth's own `npm run build` — only auth's build context was even wide enough to reach shared/ at all, and it was missing this step too. `tsc` failed with "Cannot find module '@branch/types'" inside the shared/lambda-auth build step for every lambda, and auth's `npm install` failed even earlier since its context/dockerfile in docker-compose.yml didn't reach shared/ at all. `make up` never actually succeeded from a clean build. Co-Authored-By: Claude Sonnet 5 --- apps/backend/lambdas/auth/Dockerfile | 5 +++++ apps/backend/lambdas/donors/Dockerfile | 4 ++++ apps/backend/lambdas/expenditures/Dockerfile | 4 ++++ apps/backend/lambdas/projects/Dockerfile | 4 ++++ apps/backend/lambdas/reports/Dockerfile | 4 ++++ apps/backend/lambdas/users/Dockerfile | 4 ++++ 6 files changed, 25 insertions(+) diff --git a/apps/backend/lambdas/auth/Dockerfile b/apps/backend/lambdas/auth/Dockerfile index 82c773b7..3b62f649 100644 --- a/apps/backend/lambdas/auth/Dockerfile +++ b/apps/backend/lambdas/auth/Dockerfile @@ -6,6 +6,11 @@ WORKDIR /shared/types COPY shared/types/package.json ./ COPY shared/types/ ./ +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 + WORKDIR /app # Copy package files diff --git a/apps/backend/lambdas/donors/Dockerfile b/apps/backend/lambdas/donors/Dockerfile index dec1aba3..55811090 100644 --- a/apps/backend/lambdas/donors/Dockerfile +++ b/apps/backend/lambdas/donors/Dockerfile @@ -1,5 +1,9 @@ FROM node:20-alpine +WORKDIR /shared/types +COPY shared/types/package.json ./ +COPY shared/types/ ./ + WORKDIR /shared/lambda-auth COPY shared/lambda-auth/package.json shared/lambda-auth/tsconfig.json ./ COPY shared/lambda-auth/src ./src/ diff --git a/apps/backend/lambdas/expenditures/Dockerfile b/apps/backend/lambdas/expenditures/Dockerfile index 17c7ce17..40e719df 100644 --- a/apps/backend/lambdas/expenditures/Dockerfile +++ b/apps/backend/lambdas/expenditures/Dockerfile @@ -1,5 +1,9 @@ FROM node:20-alpine +WORKDIR /shared/types +COPY shared/types/package.json ./ +COPY shared/types/ ./ + WORKDIR /shared/lambda-auth COPY shared/lambda-auth/package.json shared/lambda-auth/tsconfig.json ./ COPY shared/lambda-auth/src ./src/ diff --git a/apps/backend/lambdas/projects/Dockerfile b/apps/backend/lambdas/projects/Dockerfile index 09292832..f925ad85 100644 --- a/apps/backend/lambdas/projects/Dockerfile +++ b/apps/backend/lambdas/projects/Dockerfile @@ -1,5 +1,9 @@ FROM node:20-alpine +WORKDIR /shared/types +COPY shared/types/package.json ./ +COPY shared/types/ ./ + WORKDIR /shared/lambda-auth COPY shared/lambda-auth/package.json shared/lambda-auth/tsconfig.json ./ COPY shared/lambda-auth/src ./src/ diff --git a/apps/backend/lambdas/reports/Dockerfile b/apps/backend/lambdas/reports/Dockerfile index a959f797..25f88a0f 100644 --- a/apps/backend/lambdas/reports/Dockerfile +++ b/apps/backend/lambdas/reports/Dockerfile @@ -1,5 +1,9 @@ FROM node:20-alpine +WORKDIR /shared/types +COPY shared/types/package.json ./ +COPY shared/types/ ./ + WORKDIR /shared/lambda-auth COPY shared/lambda-auth/package.json shared/lambda-auth/tsconfig.json ./ COPY shared/lambda-auth/src ./src/ diff --git a/apps/backend/lambdas/users/Dockerfile b/apps/backend/lambdas/users/Dockerfile index aec6cb5e..0560151b 100644 --- a/apps/backend/lambdas/users/Dockerfile +++ b/apps/backend/lambdas/users/Dockerfile @@ -1,5 +1,9 @@ FROM node:20-alpine +WORKDIR /shared/types +COPY shared/types/package.json ./ +COPY shared/types/ ./ + WORKDIR /shared/lambda-auth COPY shared/lambda-auth/package.json shared/lambda-auth/tsconfig.json ./ COPY shared/lambda-auth/src ./src/ From f65debc44adc42790179abc6bb90559549678a86 Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Wed, 12 Aug 2026 19:26:06 -0400 Subject: [PATCH 11/12] debug(frontend): log the real error behind login's generic failure message reportError()'s fallback ("Cannot reach the server...") fires for any non-ApiError throw, not just a genuine fetch TypeError, and never logged what actually happened. A login attempt in the PR's test-environment preview hit this exact message with zero corresponding network request or console output, so the real cause is currently undiagnosable from the browser alone. This just surfaces it next time. Co-Authored-By: Claude Sonnet 5 --- apps/frontend/src/app/login/page.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/frontend/src/app/login/page.tsx b/apps/frontend/src/app/login/page.tsx index e4cf13a2..5375f413 100644 --- a/apps/frontend/src/app/login/page.tsx +++ b/apps/frontend/src/app/login/page.tsx @@ -68,6 +68,9 @@ function LoginPageContent() { return; } // fetch rejects with a TypeError when the request never reached a server. + // Logged because that's not the only way to land here — any non-ApiError + // throw (e.g. a bug elsewhere in the login path) shows this same message. + console.error('Login failed with a non-ApiError:', err); setFormError('Cannot reach the server. Check your connection and try again.'); } From 46c5a0e04bd0c2980c600feb946892a00345b8f3 Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Wed, 12 Aug 2026 20:47:49 -0400 Subject: [PATCH 12/12] use a hook for post /users request --- apps/frontend/src/app/accounts/page.tsx | 2 -- apps/frontend/src/app/components/AddUserModal.tsx | 12 ++++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/apps/frontend/src/app/accounts/page.tsx b/apps/frontend/src/app/accounts/page.tsx index 0f36a905..5bd1f7a5 100644 --- a/apps/frontend/src/app/accounts/page.tsx +++ b/apps/frontend/src/app/accounts/page.tsx @@ -4,7 +4,6 @@ import React, { useState } from 'react'; import StaffCard from '../components/StaffCard'; import AddUserModal from '../components/AddUserModal'; import { Button } from '@chakra-ui/react'; -import { getAccessToken } from '@/lib/authTokens'; import { facilitationTeam, teamMembers } from './mockUsers'; export default function AccountsPage() { @@ -38,7 +37,6 @@ export default function AccountsPage() { open={isModalOpen} onClose={() => setIsModalOpen(false)} onSuccess={() => setIsModalOpen(false)} - token={getAccessToken() ?? ''} /> ); diff --git a/apps/frontend/src/app/components/AddUserModal.tsx b/apps/frontend/src/app/components/AddUserModal.tsx index 721caa70..8d0501db 100644 --- a/apps/frontend/src/app/components/AddUserModal.tsx +++ b/apps/frontend/src/app/components/AddUserModal.tsx @@ -3,16 +3,16 @@ import { useState } from 'react'; import { Button, Dialog, Portal, CloseButton, Stack } from '@chakra-ui/react'; import TextInputField from './TextInputField'; -import { apiFetch } from '@/lib/api'; +import { useApi } from '@/hooks/useApi'; interface AddUserModalProps { open: boolean; onClose: () => void; onSuccess: () => void; - token: string; } -export default function AddUserModal({ open, onClose, onSuccess, token }: AddUserModalProps) { +export default function AddUserModal({ open, onClose, onSuccess }: AddUserModalProps) { + const api = useApi(); const [email, setEmail] = useState(''); const [name, setName] = useState(''); const [isAdmin, setIsAdmin] = useState(false); @@ -47,11 +47,7 @@ export default function AddUserModal({ open, onClose, onSuccess, token }: AddUse setIsLoading(true); try { - await apiFetch('/users/', { - method: 'POST', - token, - body: JSON.stringify({ email: email.toLowerCase(), name: name.trim(), isAdmin }), - }); + await api.post('/users/', { email: email.toLowerCase(), name: name.trim(), isAdmin }); resetForm(); onSuccess(); } catch (err) {