From f209276e774e2a1095109105abc200db86993a15 Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Mon, 13 Jul 2026 19:49:58 -0400 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 6/7] 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 7/7] 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;