feat(auth): #243 Admin Invite User - #302

Merged
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user
Aug 16, 2026
Merged

feat(auth): #243 Admin Invite User#302
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user

Conversation

@tsudhakar87

@tsudhakar87tsudhakar87 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Issue

Closes#243

📝 Description

Here's what this PR adds on top of PR #290:

Backend (lambdas/users)

  • POST /users now calls AdminCreateUser before the DB insert - invited user gets an email with a temp password and their cognito_sub is written to the DB at creation time (fixes the orphaned-row bug where users could never authenticate)
  • Rollback: if the DB insert fails after a successful Cognito call, AdminDeleteUser is called to keep the two systems in sync
  • UsernameExistsException from Cognito → 409 (deduplication at both layers)

Infrastructure

  • cognito.tf: admin_create_user_config block - disables self-signup (allow_admin_create_user_only = true), sets invite email template
  • lambda.tf: IAM policy grants AdminCreateUser + AdminDeleteUser + AdminGetUser to the shared lambda role

Frontend

  • New AddUserModal component (email, name, is-admin checkbox) - calls POST /users with the admin's access token
  • "+ Add User" button on the Accounts page wired to the modal

✔️ Verification

What steps did you take to verify your changes work? These should be clear enough for someone to be able to clone the branch and follow the steps themselves.

Screenshot 2026-08-12 at 7 32 51 PMScreenshot 2026-08-12 at 7 33 08 PMScreenshot 2026-08-12 at 7 49 12 PMScreenshot 2026-08-12 at 7 49 58 PMScreenshot 2026-08-12 at 7 50 09 PMScreenshot 2026-08-12 at 7 50 28 PM

🏕️ (Optional) Future Work / Notes

Did you notice anything ugly during the course of this ticket? Any bugs, design challenges, or unexpected behavior? Write it down so we can clean it up in a future ticket!

@tsudhakar87tsudhakar87 changed the title 243 admin invite userfeat(auth): #243 Admin Invite UserAug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bd1htawwsa.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@nourshoreibahnourshoreibah added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 4, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bhctek80u7.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 5b652f0 · logs

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

tsudhakar87and others added 4 commits August 12, 2026 19:25
….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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ng shared/lambda-auth
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 <noreply@anthropic.com>
…ssage
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 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 097c0a5 · logs

@tsudhakar87
tsudhakar87 marked this pull request as ready for review August 12, 2026 23:52
github-actionsBot added a commit that referenced this pull request Aug 12, 2026
@tsudhakar87

Copy link
Copy Markdown
ContributorAuthor

I think I need to match this to Figma

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good, thank you! just one nit


setIsLoading(true);
try {
await apiFetch('/users/', {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit: Can we move this to a hook instead of fetching inline?

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

I think I need to match this to Figma

@tsudhakar87 i think it's ok to do this in a separate pr but totally up to you

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

@tsudhakar87

tsudhakar87 commented Aug 13, 2026

Copy link
Copy Markdown
ContributorAuthor

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

ok sounds good I can fix that spacing in this PR I don't see it in Figma so I will just leave it as is lol

@github-actions

Copy link
Copy Markdown
Contributor

Terraform Plan 📖 infrastructure/aws

Terraform Initialization ⚙️success

Terraform Validation 🤖success

Terraform Plan 📖success

Show Plan
data.archive_file.lambda_placeholder: Reading...
data.archive_file.lambda_placeholder: Read complete after 0s [id=96878a51e358033297a32b882fd5223cc95fb8a7]
data.aws_vpc.default: Reading...
aws_cloudfront_function.rewrite_index: Refreshing state... [id=branch-frontend-rewrite-index]
data.aws_caller_identity.current: Reading...
aws_cloudfront_origin_access_control.frontend: Refreshing state... [id=E2T090T8V5CDLN]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::489881683177:oidc-provider/token.actions.githubusercontent.com]
aws_api_gateway_rest_api.branch_api: Refreshing state... [id=2apxzxb0r8]
aws_iam_role.lambda_role: Refreshing state... [id=branch-lambda-role]
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_cognito_user_pool.branch_user_pool: Refreshing state... [id=us-east-2_CxTueqe6g]
data.aws_caller_identity.current: Read complete after 0s [id=489881683177]
aws_s3_bucket.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_s3_bucket.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
data.infisical_secrets.github_folder: Reading...
data.infisical_secrets.rds_folder: Reading...
data.infisical_secrets.github_folder: Read complete after 0s
data.aws_iam_policy_document.ci_plan_assume: Reading...
data.aws_iam_policy_document.ci_plan_assume: Read complete after 0s [id=3057813384]
data.aws_iam_policy_document.ci_apply_assume: Reading...
data.aws_iam_policy_document.ci_apply_assume: Read complete after 0s [id=813913]
data.aws_iam_policy_document.ci_migrate_assume: Reading...
data.aws_iam_policy_document.ci_migrate_assume: Read complete after 0s [id=3474878989]
data.aws_iam_policy_document.ci_preview_assume: Reading...
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
data.aws_iam_policy_document.ci_preview_assume: Read complete after 0s [id=282080688]
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
data.infisical_secrets.rds_folder: Read complete after 0s
aws_iam_role.ci_migrate: Refreshing state... [id=branch-ci-migrate]
aws_api_gateway_gateway_response.cors["DEFAULT_5XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_5XX]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=wsnfk2]
aws_api_gateway_gateway_response.cors["DEFAULT_4XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_4XX]
aws_api_gateway_resource.lambda_resources["users"]: Refreshing state... [id=0dkbds]
aws_api_gateway_resource.lambda_resources["auth"]: Refreshing state... [id=u8unad]
aws_api_gateway_resource.lambda_resources["donors"]: Refreshing state... [id=hybur2]
aws_api_gateway_resource.lambda_resources["expenditures"]: Refreshing state... [id=6sdj3w]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=chhy2i]
aws_iam_role.ci_preview: Refreshing state... [id=branch-ci-preview]
aws_cognito_user_pool_client.branch_client: Refreshing state... [id=570i6ocj0882qu0ditm4vrr60f]
aws_iam_role_policy.lambda_cognito_admin: Refreshing state... [id=branch-lambda-role:branch-lambda-cognito-admin]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
data.aws_vpc.default: Read complete after 1s [id=vpc-0a9ccfb59c8918ce9]
aws_iam_role_policy.ci_plan_state_lock: Refreshing state... [id=branch-ci-plan:tfstate-lock]
aws_iam_role_policy_attachment.ci_plan_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/ReadOnlyAccess]
aws_api_gateway_resource.lambda_proxy["auth"]: Refreshing state... [id=srhf9j]
aws_api_gateway_resource.lambda_proxy["donors"]: Refreshing state... [id=xkazax]
aws_api_gateway_resource.lambda_proxy["expenditures"]: Refreshing state... [id=14khv0]
aws_api_gateway_resource.lambda_proxy["users"]: Refreshing state... [id=4sjlu3]
aws_api_gateway_resource.lambda_proxy["reports"]: Refreshing state... [id=elsvn3]
aws_api_gateway_resource.lambda_proxy["projects"]: Refreshing state... [id=kmwcxq]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_method.lambda_methods["projects-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_method.lambda_methods["users-GET"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_method.lambda_methods["auth-POST"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-POST]
aws_api_gateway_method.lambda_methods["auth-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_method.lambda_methods["reports-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-GET]
aws_api_gateway_method.lambda_methods["expenditures-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_method.lambda_methods["users-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-GET]
aws_api_gateway_method.lambda_methods["donors-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-GET]
aws_security_group.rds: Refreshing state... [id=sg-0c8a1ff1676a14edb]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_iam_role_policy.lambda_s3_objects: Refreshing state... [id=branch-lambda-role:branch-lambda-s3-objects]
aws_cloudfront_distribution.frontend: Refreshing state... [id=E37FDHRYNZNF4R]
aws_s3_bucket_public_access_block.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_api_gateway_method.lambda_proxy_any["reports"]: Refreshing state... [id=agm-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_method.lambda_proxy_any["auth"]: Refreshing state... [id=agm-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_method.lambda_proxy_any["users"]: Refreshing state... [id=agm-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_method.lambda_proxy_any["expenditures"]: Refreshing state... [id=agm-2apxzxb0r8-14khv0-ANY]
aws_api_gateway_method.lambda_proxy_any["donors"]: Refreshing state... [id=agm-2apxzxb0r8-xkazax-ANY]
aws_api_gateway_method.lambda_proxy_any["projects"]: Refreshing state... [id=agm-2apxzxb0r8-kmwcxq-ANY]
aws_iam_role_policy.ci_preview: Refreshing state... [id=branch-ci-preview:preview-env]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-489881683177/donors/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-489881683177/reports/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-489881683177/users/initial.zip]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-489881683177/expenditures/initial.zip]
aws_s3_object.lambda_placeholder["projects"]: Refreshing state... [id=branch-lambda-deployments-489881683177/projects/initial.zip]
aws_s3_object.lambda_placeholder["auth"]: Refreshing state... [id=branch-lambda-deployments-489881683177/auth/initial.zip]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_vpc_security_group_ingress_rule.rds_postgres: Refreshing state... [id=sgr-0594341dc6234d55c]
aws_db_instance.branch_rds: Refreshing state... [id=db-AMMYFTORW6XJGRELV7WQZCNHQI]
aws_vpc_security_group_egress_rule.rds_all: Refreshing state... [id=sgr-099fe0d98d4b3f3b5]
data.aws_iam_policy_document.frontend_bucket: Reading...
data.aws_iam_policy_document.frontend_bucket: Read complete after 0s [id=1471335443]
aws_s3_bucket_policy.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_iam_role_policy.ci_migrate: Refreshing state... [id=branch-ci-migrate:db-migrate]
aws_lambda_permission.api_gateway_permissions["users"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["expenditures"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["donors"]: Refreshing state... [id=agi-2apxzxb0r8-xkazax-ANY]
aws_lambda_permission.api_gateway_permissions["projects"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["reports"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["expenditures"]: Refreshing state... [id=agi-2apxzxb0r8-14khv0-ANY]
aws_lambda_permission.api_gateway_permissions["auth"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["projects"]: Refreshing state... [id=agi-2apxzxb0r8-kmwcxq-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["users"]: Refreshing state... [id=agi-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["reports"]: Refreshing state... [id=agi-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["auth"]: Refreshing state... [id=agi-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-POST"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-GET]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-GET]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_integration.lambda_integrations["reports-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_integration.lambda_integrations["users-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_integration.lambda_integrations["donors-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-GET"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_integration.lambda_integrations["users-GET"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_integration.lambda_integrations["projects-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_integration.lambda_integrations["auth-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_deployment.branch_deployment: Refreshing state... [id=od3a3y]
aws_api_gateway_stage.branch_stage: Refreshing state... [id=ags-2apxzxb0r8-prod]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:# aws_api_gateway_gateway_response.cors["DEFAULT_4XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_4XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
# aws_api_gateway_gateway_response.cors["DEFAULT_5XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_5XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
Plan:0 to add, 2 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"

Pushed by: @tsudhakar87, Action: pull_request

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 74d24d4 · logs

@nourshoreibah
nourshoreibah merged commit 81c19d7 into mainAug 16, 2026
18 checks passed
@nourshoreibah
nourshoreibah deleted the 243-admin-invite-user branch August 16, 2026 19:25
github-actionsBot added a commit that referenced this pull request Aug 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-environmentCreates a temporary (nearly free) test environment. Uses prod DB and cognito

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Backend] Admin-invite user provisioning via Cognito AdminCreateUser

2 participants

@tsudhakar87@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(auth): #243 Admin Invite User - #302

Merged
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user
Aug 16, 2026
Merged

feat(auth): #243 Admin Invite User#302
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user

Conversation

@tsudhakar87

@tsudhakar87tsudhakar87 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Issue

Closes#243

📝 Description

Here's what this PR adds on top of PR #290:

Backend (lambdas/users)

  • POST /users now calls AdminCreateUser before the DB insert - invited user gets an email with a temp password and their cognito_sub is written to the DB at creation time (fixes the orphaned-row bug where users could never authenticate)
  • Rollback: if the DB insert fails after a successful Cognito call, AdminDeleteUser is called to keep the two systems in sync
  • UsernameExistsException from Cognito → 409 (deduplication at both layers)

Infrastructure

  • cognito.tf: admin_create_user_config block - disables self-signup (allow_admin_create_user_only = true), sets invite email template
  • lambda.tf: IAM policy grants AdminCreateUser + AdminDeleteUser + AdminGetUser to the shared lambda role

Frontend

  • New AddUserModal component (email, name, is-admin checkbox) - calls POST /users with the admin's access token
  • "+ Add User" button on the Accounts page wired to the modal

✔️ Verification

What steps did you take to verify your changes work? These should be clear enough for someone to be able to clone the branch and follow the steps themselves.

Screenshot 2026-08-12 at 7 32 51 PMScreenshot 2026-08-12 at 7 33 08 PMScreenshot 2026-08-12 at 7 49 12 PMScreenshot 2026-08-12 at 7 49 58 PMScreenshot 2026-08-12 at 7 50 09 PMScreenshot 2026-08-12 at 7 50 28 PM

🏕️ (Optional) Future Work / Notes

Did you notice anything ugly during the course of this ticket? Any bugs, design challenges, or unexpected behavior? Write it down so we can clean it up in a future ticket!

@tsudhakar87tsudhakar87 changed the title 243 admin invite userfeat(auth): #243 Admin Invite UserAug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bd1htawwsa.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@nourshoreibahnourshoreibah added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 4, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bhctek80u7.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 5b652f0 · logs

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

tsudhakar87and others added 4 commits August 12, 2026 19:25
….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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ng shared/lambda-auth
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 <noreply@anthropic.com>
…ssage
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 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 097c0a5 · logs

@tsudhakar87
tsudhakar87 marked this pull request as ready for review August 12, 2026 23:52
github-actionsBot added a commit that referenced this pull request Aug 12, 2026
@tsudhakar87

Copy link
Copy Markdown
ContributorAuthor

I think I need to match this to Figma

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good, thank you! just one nit


setIsLoading(true);
try {
await apiFetch('/users/', {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit: Can we move this to a hook instead of fetching inline?

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

I think I need to match this to Figma

@tsudhakar87 i think it's ok to do this in a separate pr but totally up to you

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

@tsudhakar87

tsudhakar87 commented Aug 13, 2026

Copy link
Copy Markdown
ContributorAuthor

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

ok sounds good I can fix that spacing in this PR I don't see it in Figma so I will just leave it as is lol

@github-actions

Copy link
Copy Markdown
Contributor

Terraform Plan 📖 infrastructure/aws

Terraform Initialization ⚙️success

Terraform Validation 🤖success

Terraform Plan 📖success

Show Plan
data.archive_file.lambda_placeholder: Reading...
data.archive_file.lambda_placeholder: Read complete after 0s [id=96878a51e358033297a32b882fd5223cc95fb8a7]
data.aws_vpc.default: Reading...
aws_cloudfront_function.rewrite_index: Refreshing state... [id=branch-frontend-rewrite-index]
data.aws_caller_identity.current: Reading...
aws_cloudfront_origin_access_control.frontend: Refreshing state... [id=E2T090T8V5CDLN]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::489881683177:oidc-provider/token.actions.githubusercontent.com]
aws_api_gateway_rest_api.branch_api: Refreshing state... [id=2apxzxb0r8]
aws_iam_role.lambda_role: Refreshing state... [id=branch-lambda-role]
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_cognito_user_pool.branch_user_pool: Refreshing state... [id=us-east-2_CxTueqe6g]
data.aws_caller_identity.current: Read complete after 0s [id=489881683177]
aws_s3_bucket.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_s3_bucket.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
data.infisical_secrets.github_folder: Reading...
data.infisical_secrets.rds_folder: Reading...
data.infisical_secrets.github_folder: Read complete after 0s
data.aws_iam_policy_document.ci_plan_assume: Reading...
data.aws_iam_policy_document.ci_plan_assume: Read complete after 0s [id=3057813384]
data.aws_iam_policy_document.ci_apply_assume: Reading...
data.aws_iam_policy_document.ci_apply_assume: Read complete after 0s [id=813913]
data.aws_iam_policy_document.ci_migrate_assume: Reading...
data.aws_iam_policy_document.ci_migrate_assume: Read complete after 0s [id=3474878989]
data.aws_iam_policy_document.ci_preview_assume: Reading...
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
data.aws_iam_policy_document.ci_preview_assume: Read complete after 0s [id=282080688]
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
data.infisical_secrets.rds_folder: Read complete after 0s
aws_iam_role.ci_migrate: Refreshing state... [id=branch-ci-migrate]
aws_api_gateway_gateway_response.cors["DEFAULT_5XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_5XX]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=wsnfk2]
aws_api_gateway_gateway_response.cors["DEFAULT_4XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_4XX]
aws_api_gateway_resource.lambda_resources["users"]: Refreshing state... [id=0dkbds]
aws_api_gateway_resource.lambda_resources["auth"]: Refreshing state... [id=u8unad]
aws_api_gateway_resource.lambda_resources["donors"]: Refreshing state... [id=hybur2]
aws_api_gateway_resource.lambda_resources["expenditures"]: Refreshing state... [id=6sdj3w]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=chhy2i]
aws_iam_role.ci_preview: Refreshing state... [id=branch-ci-preview]
aws_cognito_user_pool_client.branch_client: Refreshing state... [id=570i6ocj0882qu0ditm4vrr60f]
aws_iam_role_policy.lambda_cognito_admin: Refreshing state... [id=branch-lambda-role:branch-lambda-cognito-admin]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
data.aws_vpc.default: Read complete after 1s [id=vpc-0a9ccfb59c8918ce9]
aws_iam_role_policy.ci_plan_state_lock: Refreshing state... [id=branch-ci-plan:tfstate-lock]
aws_iam_role_policy_attachment.ci_plan_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/ReadOnlyAccess]
aws_api_gateway_resource.lambda_proxy["auth"]: Refreshing state... [id=srhf9j]
aws_api_gateway_resource.lambda_proxy["donors"]: Refreshing state... [id=xkazax]
aws_api_gateway_resource.lambda_proxy["expenditures"]: Refreshing state... [id=14khv0]
aws_api_gateway_resource.lambda_proxy["users"]: Refreshing state... [id=4sjlu3]
aws_api_gateway_resource.lambda_proxy["reports"]: Refreshing state... [id=elsvn3]
aws_api_gateway_resource.lambda_proxy["projects"]: Refreshing state... [id=kmwcxq]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_method.lambda_methods["projects-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_method.lambda_methods["users-GET"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_method.lambda_methods["auth-POST"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-POST]
aws_api_gateway_method.lambda_methods["auth-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_method.lambda_methods["reports-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-GET]
aws_api_gateway_method.lambda_methods["expenditures-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_method.lambda_methods["users-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-GET]
aws_api_gateway_method.lambda_methods["donors-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-GET]
aws_security_group.rds: Refreshing state... [id=sg-0c8a1ff1676a14edb]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_iam_role_policy.lambda_s3_objects: Refreshing state... [id=branch-lambda-role:branch-lambda-s3-objects]
aws_cloudfront_distribution.frontend: Refreshing state... [id=E37FDHRYNZNF4R]
aws_s3_bucket_public_access_block.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_api_gateway_method.lambda_proxy_any["reports"]: Refreshing state... [id=agm-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_method.lambda_proxy_any["auth"]: Refreshing state... [id=agm-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_method.lambda_proxy_any["users"]: Refreshing state... [id=agm-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_method.lambda_proxy_any["expenditures"]: Refreshing state... [id=agm-2apxzxb0r8-14khv0-ANY]
aws_api_gateway_method.lambda_proxy_any["donors"]: Refreshing state... [id=agm-2apxzxb0r8-xkazax-ANY]
aws_api_gateway_method.lambda_proxy_any["projects"]: Refreshing state... [id=agm-2apxzxb0r8-kmwcxq-ANY]
aws_iam_role_policy.ci_preview: Refreshing state... [id=branch-ci-preview:preview-env]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-489881683177/donors/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-489881683177/reports/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-489881683177/users/initial.zip]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-489881683177/expenditures/initial.zip]
aws_s3_object.lambda_placeholder["projects"]: Refreshing state... [id=branch-lambda-deployments-489881683177/projects/initial.zip]
aws_s3_object.lambda_placeholder["auth"]: Refreshing state... [id=branch-lambda-deployments-489881683177/auth/initial.zip]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_vpc_security_group_ingress_rule.rds_postgres: Refreshing state... [id=sgr-0594341dc6234d55c]
aws_db_instance.branch_rds: Refreshing state... [id=db-AMMYFTORW6XJGRELV7WQZCNHQI]
aws_vpc_security_group_egress_rule.rds_all: Refreshing state... [id=sgr-099fe0d98d4b3f3b5]
data.aws_iam_policy_document.frontend_bucket: Reading...
data.aws_iam_policy_document.frontend_bucket: Read complete after 0s [id=1471335443]
aws_s3_bucket_policy.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_iam_role_policy.ci_migrate: Refreshing state... [id=branch-ci-migrate:db-migrate]
aws_lambda_permission.api_gateway_permissions["users"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["expenditures"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["donors"]: Refreshing state... [id=agi-2apxzxb0r8-xkazax-ANY]
aws_lambda_permission.api_gateway_permissions["projects"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["reports"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["expenditures"]: Refreshing state... [id=agi-2apxzxb0r8-14khv0-ANY]
aws_lambda_permission.api_gateway_permissions["auth"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["projects"]: Refreshing state... [id=agi-2apxzxb0r8-kmwcxq-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["users"]: Refreshing state... [id=agi-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["reports"]: Refreshing state... [id=agi-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["auth"]: Refreshing state... [id=agi-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-POST"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-GET]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-GET]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_integration.lambda_integrations["reports-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_integration.lambda_integrations["users-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_integration.lambda_integrations["donors-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-GET"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_integration.lambda_integrations["users-GET"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_integration.lambda_integrations["projects-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_integration.lambda_integrations["auth-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_deployment.branch_deployment: Refreshing state... [id=od3a3y]
aws_api_gateway_stage.branch_stage: Refreshing state... [id=ags-2apxzxb0r8-prod]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:# aws_api_gateway_gateway_response.cors["DEFAULT_4XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_4XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
# aws_api_gateway_gateway_response.cors["DEFAULT_5XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_5XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
Plan:0 to add, 2 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"

Pushed by: @tsudhakar87, Action: pull_request

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 74d24d4 · logs

@nourshoreibah
nourshoreibah merged commit 81c19d7 into mainAug 16, 2026
18 checks passed
@nourshoreibah
nourshoreibah deleted the 243-admin-invite-user branch August 16, 2026 19:25
github-actionsBot added a commit that referenced this pull request Aug 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-environmentCreates a temporary (nearly free) test environment. Uses prod DB and cognito

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Backend] Admin-invite user provisioning via Cognito AdminCreateUser

2 participants

@tsudhakar87@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(auth): #243 Admin Invite User - #302

Merged
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user
Aug 16, 2026
Merged

feat(auth): #243 Admin Invite User#302
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user

Conversation

@tsudhakar87

@tsudhakar87tsudhakar87 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Issue

Closes#243

📝 Description

Here's what this PR adds on top of PR #290:

Backend (lambdas/users)

  • POST /users now calls AdminCreateUser before the DB insert - invited user gets an email with a temp password and their cognito_sub is written to the DB at creation time (fixes the orphaned-row bug where users could never authenticate)
  • Rollback: if the DB insert fails after a successful Cognito call, AdminDeleteUser is called to keep the two systems in sync
  • UsernameExistsException from Cognito → 409 (deduplication at both layers)

Infrastructure

  • cognito.tf: admin_create_user_config block - disables self-signup (allow_admin_create_user_only = true), sets invite email template
  • lambda.tf: IAM policy grants AdminCreateUser + AdminDeleteUser + AdminGetUser to the shared lambda role

Frontend

  • New AddUserModal component (email, name, is-admin checkbox) - calls POST /users with the admin's access token
  • "+ Add User" button on the Accounts page wired to the modal

✔️ Verification

What steps did you take to verify your changes work? These should be clear enough for someone to be able to clone the branch and follow the steps themselves.

Screenshot 2026-08-12 at 7 32 51 PMScreenshot 2026-08-12 at 7 33 08 PMScreenshot 2026-08-12 at 7 49 12 PMScreenshot 2026-08-12 at 7 49 58 PMScreenshot 2026-08-12 at 7 50 09 PMScreenshot 2026-08-12 at 7 50 28 PM

🏕️ (Optional) Future Work / Notes

Did you notice anything ugly during the course of this ticket? Any bugs, design challenges, or unexpected behavior? Write it down so we can clean it up in a future ticket!

@tsudhakar87tsudhakar87 changed the title 243 admin invite userfeat(auth): #243 Admin Invite UserAug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bd1htawwsa.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@nourshoreibahnourshoreibah added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 4, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bhctek80u7.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 5b652f0 · logs

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

tsudhakar87and others added 4 commits August 12, 2026 19:25
….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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ng shared/lambda-auth
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 <noreply@anthropic.com>
…ssage
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 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 097c0a5 · logs

@tsudhakar87
tsudhakar87 marked this pull request as ready for review August 12, 2026 23:52
github-actionsBot added a commit that referenced this pull request Aug 12, 2026
@tsudhakar87

Copy link
Copy Markdown
ContributorAuthor

I think I need to match this to Figma

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good, thank you! just one nit


setIsLoading(true);
try {
await apiFetch('/users/', {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit: Can we move this to a hook instead of fetching inline?

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

I think I need to match this to Figma

@tsudhakar87 i think it's ok to do this in a separate pr but totally up to you

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

@tsudhakar87

tsudhakar87 commented Aug 13, 2026

Copy link
Copy Markdown
ContributorAuthor

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

ok sounds good I can fix that spacing in this PR I don't see it in Figma so I will just leave it as is lol

@github-actions

Copy link
Copy Markdown
Contributor

Terraform Plan 📖 infrastructure/aws

Terraform Initialization ⚙️success

Terraform Validation 🤖success

Terraform Plan 📖success

Show Plan
data.archive_file.lambda_placeholder: Reading...
data.archive_file.lambda_placeholder: Read complete after 0s [id=96878a51e358033297a32b882fd5223cc95fb8a7]
data.aws_vpc.default: Reading...
aws_cloudfront_function.rewrite_index: Refreshing state... [id=branch-frontend-rewrite-index]
data.aws_caller_identity.current: Reading...
aws_cloudfront_origin_access_control.frontend: Refreshing state... [id=E2T090T8V5CDLN]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::489881683177:oidc-provider/token.actions.githubusercontent.com]
aws_api_gateway_rest_api.branch_api: Refreshing state... [id=2apxzxb0r8]
aws_iam_role.lambda_role: Refreshing state... [id=branch-lambda-role]
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_cognito_user_pool.branch_user_pool: Refreshing state... [id=us-east-2_CxTueqe6g]
data.aws_caller_identity.current: Read complete after 0s [id=489881683177]
aws_s3_bucket.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_s3_bucket.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
data.infisical_secrets.github_folder: Reading...
data.infisical_secrets.rds_folder: Reading...
data.infisical_secrets.github_folder: Read complete after 0s
data.aws_iam_policy_document.ci_plan_assume: Reading...
data.aws_iam_policy_document.ci_plan_assume: Read complete after 0s [id=3057813384]
data.aws_iam_policy_document.ci_apply_assume: Reading...
data.aws_iam_policy_document.ci_apply_assume: Read complete after 0s [id=813913]
data.aws_iam_policy_document.ci_migrate_assume: Reading...
data.aws_iam_policy_document.ci_migrate_assume: Read complete after 0s [id=3474878989]
data.aws_iam_policy_document.ci_preview_assume: Reading...
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
data.aws_iam_policy_document.ci_preview_assume: Read complete after 0s [id=282080688]
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
data.infisical_secrets.rds_folder: Read complete after 0s
aws_iam_role.ci_migrate: Refreshing state... [id=branch-ci-migrate]
aws_api_gateway_gateway_response.cors["DEFAULT_5XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_5XX]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=wsnfk2]
aws_api_gateway_gateway_response.cors["DEFAULT_4XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_4XX]
aws_api_gateway_resource.lambda_resources["users"]: Refreshing state... [id=0dkbds]
aws_api_gateway_resource.lambda_resources["auth"]: Refreshing state... [id=u8unad]
aws_api_gateway_resource.lambda_resources["donors"]: Refreshing state... [id=hybur2]
aws_api_gateway_resource.lambda_resources["expenditures"]: Refreshing state... [id=6sdj3w]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=chhy2i]
aws_iam_role.ci_preview: Refreshing state... [id=branch-ci-preview]
aws_cognito_user_pool_client.branch_client: Refreshing state... [id=570i6ocj0882qu0ditm4vrr60f]
aws_iam_role_policy.lambda_cognito_admin: Refreshing state... [id=branch-lambda-role:branch-lambda-cognito-admin]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
data.aws_vpc.default: Read complete after 1s [id=vpc-0a9ccfb59c8918ce9]
aws_iam_role_policy.ci_plan_state_lock: Refreshing state... [id=branch-ci-plan:tfstate-lock]
aws_iam_role_policy_attachment.ci_plan_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/ReadOnlyAccess]
aws_api_gateway_resource.lambda_proxy["auth"]: Refreshing state... [id=srhf9j]
aws_api_gateway_resource.lambda_proxy["donors"]: Refreshing state... [id=xkazax]
aws_api_gateway_resource.lambda_proxy["expenditures"]: Refreshing state... [id=14khv0]
aws_api_gateway_resource.lambda_proxy["users"]: Refreshing state... [id=4sjlu3]
aws_api_gateway_resource.lambda_proxy["reports"]: Refreshing state... [id=elsvn3]
aws_api_gateway_resource.lambda_proxy["projects"]: Refreshing state... [id=kmwcxq]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_method.lambda_methods["projects-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_method.lambda_methods["users-GET"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_method.lambda_methods["auth-POST"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-POST]
aws_api_gateway_method.lambda_methods["auth-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_method.lambda_methods["reports-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-GET]
aws_api_gateway_method.lambda_methods["expenditures-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_method.lambda_methods["users-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-GET]
aws_api_gateway_method.lambda_methods["donors-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-GET]
aws_security_group.rds: Refreshing state... [id=sg-0c8a1ff1676a14edb]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_iam_role_policy.lambda_s3_objects: Refreshing state... [id=branch-lambda-role:branch-lambda-s3-objects]
aws_cloudfront_distribution.frontend: Refreshing state... [id=E37FDHRYNZNF4R]
aws_s3_bucket_public_access_block.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_api_gateway_method.lambda_proxy_any["reports"]: Refreshing state... [id=agm-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_method.lambda_proxy_any["auth"]: Refreshing state... [id=agm-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_method.lambda_proxy_any["users"]: Refreshing state... [id=agm-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_method.lambda_proxy_any["expenditures"]: Refreshing state... [id=agm-2apxzxb0r8-14khv0-ANY]
aws_api_gateway_method.lambda_proxy_any["donors"]: Refreshing state... [id=agm-2apxzxb0r8-xkazax-ANY]
aws_api_gateway_method.lambda_proxy_any["projects"]: Refreshing state... [id=agm-2apxzxb0r8-kmwcxq-ANY]
aws_iam_role_policy.ci_preview: Refreshing state... [id=branch-ci-preview:preview-env]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-489881683177/donors/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-489881683177/reports/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-489881683177/users/initial.zip]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-489881683177/expenditures/initial.zip]
aws_s3_object.lambda_placeholder["projects"]: Refreshing state... [id=branch-lambda-deployments-489881683177/projects/initial.zip]
aws_s3_object.lambda_placeholder["auth"]: Refreshing state... [id=branch-lambda-deployments-489881683177/auth/initial.zip]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_vpc_security_group_ingress_rule.rds_postgres: Refreshing state... [id=sgr-0594341dc6234d55c]
aws_db_instance.branch_rds: Refreshing state... [id=db-AMMYFTORW6XJGRELV7WQZCNHQI]
aws_vpc_security_group_egress_rule.rds_all: Refreshing state... [id=sgr-099fe0d98d4b3f3b5]
data.aws_iam_policy_document.frontend_bucket: Reading...
data.aws_iam_policy_document.frontend_bucket: Read complete after 0s [id=1471335443]
aws_s3_bucket_policy.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_iam_role_policy.ci_migrate: Refreshing state... [id=branch-ci-migrate:db-migrate]
aws_lambda_permission.api_gateway_permissions["users"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["expenditures"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["donors"]: Refreshing state... [id=agi-2apxzxb0r8-xkazax-ANY]
aws_lambda_permission.api_gateway_permissions["projects"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["reports"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["expenditures"]: Refreshing state... [id=agi-2apxzxb0r8-14khv0-ANY]
aws_lambda_permission.api_gateway_permissions["auth"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["projects"]: Refreshing state... [id=agi-2apxzxb0r8-kmwcxq-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["users"]: Refreshing state... [id=agi-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["reports"]: Refreshing state... [id=agi-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["auth"]: Refreshing state... [id=agi-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-POST"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-GET]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-GET]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_integration.lambda_integrations["reports-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_integration.lambda_integrations["users-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_integration.lambda_integrations["donors-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-GET"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_integration.lambda_integrations["users-GET"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_integration.lambda_integrations["projects-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_integration.lambda_integrations["auth-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_deployment.branch_deployment: Refreshing state... [id=od3a3y]
aws_api_gateway_stage.branch_stage: Refreshing state... [id=ags-2apxzxb0r8-prod]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:# aws_api_gateway_gateway_response.cors["DEFAULT_4XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_4XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
# aws_api_gateway_gateway_response.cors["DEFAULT_5XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_5XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
Plan:0 to add, 2 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"

Pushed by: @tsudhakar87, Action: pull_request

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 74d24d4 · logs

@nourshoreibah
nourshoreibah merged commit 81c19d7 into mainAug 16, 2026
18 checks passed
@nourshoreibah
nourshoreibah deleted the 243-admin-invite-user branch August 16, 2026 19:25
github-actionsBot added a commit that referenced this pull request Aug 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-environmentCreates a temporary (nearly free) test environment. Uses prod DB and cognito

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Backend] Admin-invite user provisioning via Cognito AdminCreateUser

2 participants

@tsudhakar87@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(auth): #243 Admin Invite User - #302

Merged
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user
Aug 16, 2026
Merged

feat(auth): #243 Admin Invite User#302
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user

Conversation

@tsudhakar87

@tsudhakar87tsudhakar87 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Issue

Closes#243

📝 Description

Here's what this PR adds on top of PR #290:

Backend (lambdas/users)

  • POST /users now calls AdminCreateUser before the DB insert - invited user gets an email with a temp password and their cognito_sub is written to the DB at creation time (fixes the orphaned-row bug where users could never authenticate)
  • Rollback: if the DB insert fails after a successful Cognito call, AdminDeleteUser is called to keep the two systems in sync
  • UsernameExistsException from Cognito → 409 (deduplication at both layers)

Infrastructure

  • cognito.tf: admin_create_user_config block - disables self-signup (allow_admin_create_user_only = true), sets invite email template
  • lambda.tf: IAM policy grants AdminCreateUser + AdminDeleteUser + AdminGetUser to the shared lambda role

Frontend

  • New AddUserModal component (email, name, is-admin checkbox) - calls POST /users with the admin's access token
  • "+ Add User" button on the Accounts page wired to the modal

✔️ Verification

What steps did you take to verify your changes work? These should be clear enough for someone to be able to clone the branch and follow the steps themselves.

Screenshot 2026-08-12 at 7 32 51 PMScreenshot 2026-08-12 at 7 33 08 PMScreenshot 2026-08-12 at 7 49 12 PMScreenshot 2026-08-12 at 7 49 58 PMScreenshot 2026-08-12 at 7 50 09 PMScreenshot 2026-08-12 at 7 50 28 PM

🏕️ (Optional) Future Work / Notes

Did you notice anything ugly during the course of this ticket? Any bugs, design challenges, or unexpected behavior? Write it down so we can clean it up in a future ticket!

@tsudhakar87tsudhakar87 changed the title 243 admin invite userfeat(auth): #243 Admin Invite UserAug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bd1htawwsa.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@nourshoreibahnourshoreibah added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 4, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bhctek80u7.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 5b652f0 · logs

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

tsudhakar87and others added 4 commits August 12, 2026 19:25
….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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ng shared/lambda-auth
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 <noreply@anthropic.com>
…ssage
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 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 097c0a5 · logs

@tsudhakar87
tsudhakar87 marked this pull request as ready for review August 12, 2026 23:52
github-actionsBot added a commit that referenced this pull request Aug 12, 2026
@tsudhakar87

Copy link
Copy Markdown
ContributorAuthor

I think I need to match this to Figma

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good, thank you! just one nit


setIsLoading(true);
try {
await apiFetch('/users/', {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit: Can we move this to a hook instead of fetching inline?

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

I think I need to match this to Figma

@tsudhakar87 i think it's ok to do this in a separate pr but totally up to you

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

@tsudhakar87

tsudhakar87 commented Aug 13, 2026

Copy link
Copy Markdown
ContributorAuthor

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

ok sounds good I can fix that spacing in this PR I don't see it in Figma so I will just leave it as is lol

@github-actions

Copy link
Copy Markdown
Contributor

Terraform Plan 📖 infrastructure/aws

Terraform Initialization ⚙️success

Terraform Validation 🤖success

Terraform Plan 📖success

Show Plan
data.archive_file.lambda_placeholder: Reading...
data.archive_file.lambda_placeholder: Read complete after 0s [id=96878a51e358033297a32b882fd5223cc95fb8a7]
data.aws_vpc.default: Reading...
aws_cloudfront_function.rewrite_index: Refreshing state... [id=branch-frontend-rewrite-index]
data.aws_caller_identity.current: Reading...
aws_cloudfront_origin_access_control.frontend: Refreshing state... [id=E2T090T8V5CDLN]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::489881683177:oidc-provider/token.actions.githubusercontent.com]
aws_api_gateway_rest_api.branch_api: Refreshing state... [id=2apxzxb0r8]
aws_iam_role.lambda_role: Refreshing state... [id=branch-lambda-role]
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_cognito_user_pool.branch_user_pool: Refreshing state... [id=us-east-2_CxTueqe6g]
data.aws_caller_identity.current: Read complete after 0s [id=489881683177]
aws_s3_bucket.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_s3_bucket.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
data.infisical_secrets.github_folder: Reading...
data.infisical_secrets.rds_folder: Reading...
data.infisical_secrets.github_folder: Read complete after 0s
data.aws_iam_policy_document.ci_plan_assume: Reading...
data.aws_iam_policy_document.ci_plan_assume: Read complete after 0s [id=3057813384]
data.aws_iam_policy_document.ci_apply_assume: Reading...
data.aws_iam_policy_document.ci_apply_assume: Read complete after 0s [id=813913]
data.aws_iam_policy_document.ci_migrate_assume: Reading...
data.aws_iam_policy_document.ci_migrate_assume: Read complete after 0s [id=3474878989]
data.aws_iam_policy_document.ci_preview_assume: Reading...
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
data.aws_iam_policy_document.ci_preview_assume: Read complete after 0s [id=282080688]
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
data.infisical_secrets.rds_folder: Read complete after 0s
aws_iam_role.ci_migrate: Refreshing state... [id=branch-ci-migrate]
aws_api_gateway_gateway_response.cors["DEFAULT_5XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_5XX]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=wsnfk2]
aws_api_gateway_gateway_response.cors["DEFAULT_4XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_4XX]
aws_api_gateway_resource.lambda_resources["users"]: Refreshing state... [id=0dkbds]
aws_api_gateway_resource.lambda_resources["auth"]: Refreshing state... [id=u8unad]
aws_api_gateway_resource.lambda_resources["donors"]: Refreshing state... [id=hybur2]
aws_api_gateway_resource.lambda_resources["expenditures"]: Refreshing state... [id=6sdj3w]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=chhy2i]
aws_iam_role.ci_preview: Refreshing state... [id=branch-ci-preview]
aws_cognito_user_pool_client.branch_client: Refreshing state... [id=570i6ocj0882qu0ditm4vrr60f]
aws_iam_role_policy.lambda_cognito_admin: Refreshing state... [id=branch-lambda-role:branch-lambda-cognito-admin]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
data.aws_vpc.default: Read complete after 1s [id=vpc-0a9ccfb59c8918ce9]
aws_iam_role_policy.ci_plan_state_lock: Refreshing state... [id=branch-ci-plan:tfstate-lock]
aws_iam_role_policy_attachment.ci_plan_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/ReadOnlyAccess]
aws_api_gateway_resource.lambda_proxy["auth"]: Refreshing state... [id=srhf9j]
aws_api_gateway_resource.lambda_proxy["donors"]: Refreshing state... [id=xkazax]
aws_api_gateway_resource.lambda_proxy["expenditures"]: Refreshing state... [id=14khv0]
aws_api_gateway_resource.lambda_proxy["users"]: Refreshing state... [id=4sjlu3]
aws_api_gateway_resource.lambda_proxy["reports"]: Refreshing state... [id=elsvn3]
aws_api_gateway_resource.lambda_proxy["projects"]: Refreshing state... [id=kmwcxq]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_method.lambda_methods["projects-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_method.lambda_methods["users-GET"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_method.lambda_methods["auth-POST"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-POST]
aws_api_gateway_method.lambda_methods["auth-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_method.lambda_methods["reports-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-GET]
aws_api_gateway_method.lambda_methods["expenditures-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_method.lambda_methods["users-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-GET]
aws_api_gateway_method.lambda_methods["donors-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-GET]
aws_security_group.rds: Refreshing state... [id=sg-0c8a1ff1676a14edb]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_iam_role_policy.lambda_s3_objects: Refreshing state... [id=branch-lambda-role:branch-lambda-s3-objects]
aws_cloudfront_distribution.frontend: Refreshing state... [id=E37FDHRYNZNF4R]
aws_s3_bucket_public_access_block.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_api_gateway_method.lambda_proxy_any["reports"]: Refreshing state... [id=agm-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_method.lambda_proxy_any["auth"]: Refreshing state... [id=agm-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_method.lambda_proxy_any["users"]: Refreshing state... [id=agm-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_method.lambda_proxy_any["expenditures"]: Refreshing state... [id=agm-2apxzxb0r8-14khv0-ANY]
aws_api_gateway_method.lambda_proxy_any["donors"]: Refreshing state... [id=agm-2apxzxb0r8-xkazax-ANY]
aws_api_gateway_method.lambda_proxy_any["projects"]: Refreshing state... [id=agm-2apxzxb0r8-kmwcxq-ANY]
aws_iam_role_policy.ci_preview: Refreshing state... [id=branch-ci-preview:preview-env]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-489881683177/donors/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-489881683177/reports/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-489881683177/users/initial.zip]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-489881683177/expenditures/initial.zip]
aws_s3_object.lambda_placeholder["projects"]: Refreshing state... [id=branch-lambda-deployments-489881683177/projects/initial.zip]
aws_s3_object.lambda_placeholder["auth"]: Refreshing state... [id=branch-lambda-deployments-489881683177/auth/initial.zip]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_vpc_security_group_ingress_rule.rds_postgres: Refreshing state... [id=sgr-0594341dc6234d55c]
aws_db_instance.branch_rds: Refreshing state... [id=db-AMMYFTORW6XJGRELV7WQZCNHQI]
aws_vpc_security_group_egress_rule.rds_all: Refreshing state... [id=sgr-099fe0d98d4b3f3b5]
data.aws_iam_policy_document.frontend_bucket: Reading...
data.aws_iam_policy_document.frontend_bucket: Read complete after 0s [id=1471335443]
aws_s3_bucket_policy.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_iam_role_policy.ci_migrate: Refreshing state... [id=branch-ci-migrate:db-migrate]
aws_lambda_permission.api_gateway_permissions["users"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["expenditures"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["donors"]: Refreshing state... [id=agi-2apxzxb0r8-xkazax-ANY]
aws_lambda_permission.api_gateway_permissions["projects"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["reports"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["expenditures"]: Refreshing state... [id=agi-2apxzxb0r8-14khv0-ANY]
aws_lambda_permission.api_gateway_permissions["auth"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["projects"]: Refreshing state... [id=agi-2apxzxb0r8-kmwcxq-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["users"]: Refreshing state... [id=agi-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["reports"]: Refreshing state... [id=agi-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["auth"]: Refreshing state... [id=agi-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-POST"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-GET]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-GET]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_integration.lambda_integrations["reports-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_integration.lambda_integrations["users-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_integration.lambda_integrations["donors-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-GET"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_integration.lambda_integrations["users-GET"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_integration.lambda_integrations["projects-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_integration.lambda_integrations["auth-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_deployment.branch_deployment: Refreshing state... [id=od3a3y]
aws_api_gateway_stage.branch_stage: Refreshing state... [id=ags-2apxzxb0r8-prod]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:# aws_api_gateway_gateway_response.cors["DEFAULT_4XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_4XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
# aws_api_gateway_gateway_response.cors["DEFAULT_5XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_5XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
Plan:0 to add, 2 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"

Pushed by: @tsudhakar87, Action: pull_request

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 74d24d4 · logs

@nourshoreibah
nourshoreibah merged commit 81c19d7 into mainAug 16, 2026
18 checks passed
@nourshoreibah
nourshoreibah deleted the 243-admin-invite-user branch August 16, 2026 19:25
github-actionsBot added a commit that referenced this pull request Aug 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-environmentCreates a temporary (nearly free) test environment. Uses prod DB and cognito

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Backend] Admin-invite user provisioning via Cognito AdminCreateUser

2 participants

@tsudhakar87@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(auth): #243 Admin Invite User - #302

Merged
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user
Aug 16, 2026
Merged

feat(auth): #243 Admin Invite User#302
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user

Conversation

@tsudhakar87

@tsudhakar87tsudhakar87 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Issue

Closes#243

📝 Description

Here's what this PR adds on top of PR #290:

Backend (lambdas/users)

  • POST /users now calls AdminCreateUser before the DB insert - invited user gets an email with a temp password and their cognito_sub is written to the DB at creation time (fixes the orphaned-row bug where users could never authenticate)
  • Rollback: if the DB insert fails after a successful Cognito call, AdminDeleteUser is called to keep the two systems in sync
  • UsernameExistsException from Cognito → 409 (deduplication at both layers)

Infrastructure

  • cognito.tf: admin_create_user_config block - disables self-signup (allow_admin_create_user_only = true), sets invite email template
  • lambda.tf: IAM policy grants AdminCreateUser + AdminDeleteUser + AdminGetUser to the shared lambda role

Frontend

  • New AddUserModal component (email, name, is-admin checkbox) - calls POST /users with the admin's access token
  • "+ Add User" button on the Accounts page wired to the modal

✔️ Verification

What steps did you take to verify your changes work? These should be clear enough for someone to be able to clone the branch and follow the steps themselves.

Screenshot 2026-08-12 at 7 32 51 PMScreenshot 2026-08-12 at 7 33 08 PMScreenshot 2026-08-12 at 7 49 12 PMScreenshot 2026-08-12 at 7 49 58 PMScreenshot 2026-08-12 at 7 50 09 PMScreenshot 2026-08-12 at 7 50 28 PM

🏕️ (Optional) Future Work / Notes

Did you notice anything ugly during the course of this ticket? Any bugs, design challenges, or unexpected behavior? Write it down so we can clean it up in a future ticket!

@tsudhakar87tsudhakar87 changed the title 243 admin invite userfeat(auth): #243 Admin Invite UserAug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bd1htawwsa.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@nourshoreibahnourshoreibah added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 4, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bhctek80u7.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 5b652f0 · logs

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

tsudhakar87and others added 4 commits August 12, 2026 19:25
….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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ng shared/lambda-auth
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 <noreply@anthropic.com>
…ssage
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 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 097c0a5 · logs

@tsudhakar87
tsudhakar87 marked this pull request as ready for review August 12, 2026 23:52
github-actionsBot added a commit that referenced this pull request Aug 12, 2026
@tsudhakar87

Copy link
Copy Markdown
ContributorAuthor

I think I need to match this to Figma

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good, thank you! just one nit


setIsLoading(true);
try {
await apiFetch('/users/', {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit: Can we move this to a hook instead of fetching inline?

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

I think I need to match this to Figma

@tsudhakar87 i think it's ok to do this in a separate pr but totally up to you

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

@tsudhakar87

tsudhakar87 commented Aug 13, 2026

Copy link
Copy Markdown
ContributorAuthor

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

ok sounds good I can fix that spacing in this PR I don't see it in Figma so I will just leave it as is lol

@github-actions

Copy link
Copy Markdown
Contributor

Terraform Plan 📖 infrastructure/aws

Terraform Initialization ⚙️success

Terraform Validation 🤖success

Terraform Plan 📖success

Show Plan
data.archive_file.lambda_placeholder: Reading...
data.archive_file.lambda_placeholder: Read complete after 0s [id=96878a51e358033297a32b882fd5223cc95fb8a7]
data.aws_vpc.default: Reading...
aws_cloudfront_function.rewrite_index: Refreshing state... [id=branch-frontend-rewrite-index]
data.aws_caller_identity.current: Reading...
aws_cloudfront_origin_access_control.frontend: Refreshing state... [id=E2T090T8V5CDLN]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::489881683177:oidc-provider/token.actions.githubusercontent.com]
aws_api_gateway_rest_api.branch_api: Refreshing state... [id=2apxzxb0r8]
aws_iam_role.lambda_role: Refreshing state... [id=branch-lambda-role]
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_cognito_user_pool.branch_user_pool: Refreshing state... [id=us-east-2_CxTueqe6g]
data.aws_caller_identity.current: Read complete after 0s [id=489881683177]
aws_s3_bucket.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_s3_bucket.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
data.infisical_secrets.github_folder: Reading...
data.infisical_secrets.rds_folder: Reading...
data.infisical_secrets.github_folder: Read complete after 0s
data.aws_iam_policy_document.ci_plan_assume: Reading...
data.aws_iam_policy_document.ci_plan_assume: Read complete after 0s [id=3057813384]
data.aws_iam_policy_document.ci_apply_assume: Reading...
data.aws_iam_policy_document.ci_apply_assume: Read complete after 0s [id=813913]
data.aws_iam_policy_document.ci_migrate_assume: Reading...
data.aws_iam_policy_document.ci_migrate_assume: Read complete after 0s [id=3474878989]
data.aws_iam_policy_document.ci_preview_assume: Reading...
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
data.aws_iam_policy_document.ci_preview_assume: Read complete after 0s [id=282080688]
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
data.infisical_secrets.rds_folder: Read complete after 0s
aws_iam_role.ci_migrate: Refreshing state... [id=branch-ci-migrate]
aws_api_gateway_gateway_response.cors["DEFAULT_5XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_5XX]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=wsnfk2]
aws_api_gateway_gateway_response.cors["DEFAULT_4XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_4XX]
aws_api_gateway_resource.lambda_resources["users"]: Refreshing state... [id=0dkbds]
aws_api_gateway_resource.lambda_resources["auth"]: Refreshing state... [id=u8unad]
aws_api_gateway_resource.lambda_resources["donors"]: Refreshing state... [id=hybur2]
aws_api_gateway_resource.lambda_resources["expenditures"]: Refreshing state... [id=6sdj3w]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=chhy2i]
aws_iam_role.ci_preview: Refreshing state... [id=branch-ci-preview]
aws_cognito_user_pool_client.branch_client: Refreshing state... [id=570i6ocj0882qu0ditm4vrr60f]
aws_iam_role_policy.lambda_cognito_admin: Refreshing state... [id=branch-lambda-role:branch-lambda-cognito-admin]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
data.aws_vpc.default: Read complete after 1s [id=vpc-0a9ccfb59c8918ce9]
aws_iam_role_policy.ci_plan_state_lock: Refreshing state... [id=branch-ci-plan:tfstate-lock]
aws_iam_role_policy_attachment.ci_plan_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/ReadOnlyAccess]
aws_api_gateway_resource.lambda_proxy["auth"]: Refreshing state... [id=srhf9j]
aws_api_gateway_resource.lambda_proxy["donors"]: Refreshing state... [id=xkazax]
aws_api_gateway_resource.lambda_proxy["expenditures"]: Refreshing state... [id=14khv0]
aws_api_gateway_resource.lambda_proxy["users"]: Refreshing state... [id=4sjlu3]
aws_api_gateway_resource.lambda_proxy["reports"]: Refreshing state... [id=elsvn3]
aws_api_gateway_resource.lambda_proxy["projects"]: Refreshing state... [id=kmwcxq]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_method.lambda_methods["projects-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_method.lambda_methods["users-GET"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_method.lambda_methods["auth-POST"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-POST]
aws_api_gateway_method.lambda_methods["auth-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_method.lambda_methods["reports-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-GET]
aws_api_gateway_method.lambda_methods["expenditures-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_method.lambda_methods["users-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-GET]
aws_api_gateway_method.lambda_methods["donors-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-GET]
aws_security_group.rds: Refreshing state... [id=sg-0c8a1ff1676a14edb]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_iam_role_policy.lambda_s3_objects: Refreshing state... [id=branch-lambda-role:branch-lambda-s3-objects]
aws_cloudfront_distribution.frontend: Refreshing state... [id=E37FDHRYNZNF4R]
aws_s3_bucket_public_access_block.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_api_gateway_method.lambda_proxy_any["reports"]: Refreshing state... [id=agm-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_method.lambda_proxy_any["auth"]: Refreshing state... [id=agm-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_method.lambda_proxy_any["users"]: Refreshing state... [id=agm-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_method.lambda_proxy_any["expenditures"]: Refreshing state... [id=agm-2apxzxb0r8-14khv0-ANY]
aws_api_gateway_method.lambda_proxy_any["donors"]: Refreshing state... [id=agm-2apxzxb0r8-xkazax-ANY]
aws_api_gateway_method.lambda_proxy_any["projects"]: Refreshing state... [id=agm-2apxzxb0r8-kmwcxq-ANY]
aws_iam_role_policy.ci_preview: Refreshing state... [id=branch-ci-preview:preview-env]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-489881683177/donors/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-489881683177/reports/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-489881683177/users/initial.zip]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-489881683177/expenditures/initial.zip]
aws_s3_object.lambda_placeholder["projects"]: Refreshing state... [id=branch-lambda-deployments-489881683177/projects/initial.zip]
aws_s3_object.lambda_placeholder["auth"]: Refreshing state... [id=branch-lambda-deployments-489881683177/auth/initial.zip]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_vpc_security_group_ingress_rule.rds_postgres: Refreshing state... [id=sgr-0594341dc6234d55c]
aws_db_instance.branch_rds: Refreshing state... [id=db-AMMYFTORW6XJGRELV7WQZCNHQI]
aws_vpc_security_group_egress_rule.rds_all: Refreshing state... [id=sgr-099fe0d98d4b3f3b5]
data.aws_iam_policy_document.frontend_bucket: Reading...
data.aws_iam_policy_document.frontend_bucket: Read complete after 0s [id=1471335443]
aws_s3_bucket_policy.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_iam_role_policy.ci_migrate: Refreshing state... [id=branch-ci-migrate:db-migrate]
aws_lambda_permission.api_gateway_permissions["users"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["expenditures"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["donors"]: Refreshing state... [id=agi-2apxzxb0r8-xkazax-ANY]
aws_lambda_permission.api_gateway_permissions["projects"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["reports"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["expenditures"]: Refreshing state... [id=agi-2apxzxb0r8-14khv0-ANY]
aws_lambda_permission.api_gateway_permissions["auth"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["projects"]: Refreshing state... [id=agi-2apxzxb0r8-kmwcxq-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["users"]: Refreshing state... [id=agi-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["reports"]: Refreshing state... [id=agi-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["auth"]: Refreshing state... [id=agi-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-POST"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-GET]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-GET]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_integration.lambda_integrations["reports-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_integration.lambda_integrations["users-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_integration.lambda_integrations["donors-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-GET"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_integration.lambda_integrations["users-GET"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_integration.lambda_integrations["projects-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_integration.lambda_integrations["auth-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_deployment.branch_deployment: Refreshing state... [id=od3a3y]
aws_api_gateway_stage.branch_stage: Refreshing state... [id=ags-2apxzxb0r8-prod]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:# aws_api_gateway_gateway_response.cors["DEFAULT_4XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_4XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
# aws_api_gateway_gateway_response.cors["DEFAULT_5XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_5XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
Plan:0 to add, 2 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"

Pushed by: @tsudhakar87, Action: pull_request

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 74d24d4 · logs

@nourshoreibah
nourshoreibah merged commit 81c19d7 into mainAug 16, 2026
18 checks passed
@nourshoreibah
nourshoreibah deleted the 243-admin-invite-user branch August 16, 2026 19:25
github-actionsBot added a commit that referenced this pull request Aug 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-environmentCreates a temporary (nearly free) test environment. Uses prod DB and cognito

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Backend] Admin-invite user provisioning via Cognito AdminCreateUser

2 participants

@tsudhakar87@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(auth): #243 Admin Invite User - #302

Merged
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user
Aug 16, 2026
Merged

feat(auth): #243 Admin Invite User#302
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user

Conversation

@tsudhakar87

@tsudhakar87tsudhakar87 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Issue

Closes#243

📝 Description

Here's what this PR adds on top of PR #290:

Backend (lambdas/users)

  • POST /users now calls AdminCreateUser before the DB insert - invited user gets an email with a temp password and their cognito_sub is written to the DB at creation time (fixes the orphaned-row bug where users could never authenticate)
  • Rollback: if the DB insert fails after a successful Cognito call, AdminDeleteUser is called to keep the two systems in sync
  • UsernameExistsException from Cognito → 409 (deduplication at both layers)

Infrastructure

  • cognito.tf: admin_create_user_config block - disables self-signup (allow_admin_create_user_only = true), sets invite email template
  • lambda.tf: IAM policy grants AdminCreateUser + AdminDeleteUser + AdminGetUser to the shared lambda role

Frontend

  • New AddUserModal component (email, name, is-admin checkbox) - calls POST /users with the admin's access token
  • "+ Add User" button on the Accounts page wired to the modal

✔️ Verification

What steps did you take to verify your changes work? These should be clear enough for someone to be able to clone the branch and follow the steps themselves.

Screenshot 2026-08-12 at 7 32 51 PMScreenshot 2026-08-12 at 7 33 08 PMScreenshot 2026-08-12 at 7 49 12 PMScreenshot 2026-08-12 at 7 49 58 PMScreenshot 2026-08-12 at 7 50 09 PMScreenshot 2026-08-12 at 7 50 28 PM

🏕️ (Optional) Future Work / Notes

Did you notice anything ugly during the course of this ticket? Any bugs, design challenges, or unexpected behavior? Write it down so we can clean it up in a future ticket!

@tsudhakar87tsudhakar87 changed the title 243 admin invite userfeat(auth): #243 Admin Invite UserAug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bd1htawwsa.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@nourshoreibahnourshoreibah added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 4, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bhctek80u7.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 5b652f0 · logs

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

tsudhakar87and others added 4 commits August 12, 2026 19:25
….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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ng shared/lambda-auth
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 <noreply@anthropic.com>
…ssage
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 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 097c0a5 · logs

@tsudhakar87
tsudhakar87 marked this pull request as ready for review August 12, 2026 23:52
github-actionsBot added a commit that referenced this pull request Aug 12, 2026
@tsudhakar87

Copy link
Copy Markdown
ContributorAuthor

I think I need to match this to Figma

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good, thank you! just one nit


setIsLoading(true);
try {
await apiFetch('/users/', {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit: Can we move this to a hook instead of fetching inline?

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

I think I need to match this to Figma

@tsudhakar87 i think it's ok to do this in a separate pr but totally up to you

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

@tsudhakar87

tsudhakar87 commented Aug 13, 2026

Copy link
Copy Markdown
ContributorAuthor

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

ok sounds good I can fix that spacing in this PR I don't see it in Figma so I will just leave it as is lol

@github-actions

Copy link
Copy Markdown
Contributor

Terraform Plan 📖 infrastructure/aws

Terraform Initialization ⚙️success

Terraform Validation 🤖success

Terraform Plan 📖success

Show Plan
data.archive_file.lambda_placeholder: Reading...
data.archive_file.lambda_placeholder: Read complete after 0s [id=96878a51e358033297a32b882fd5223cc95fb8a7]
data.aws_vpc.default: Reading...
aws_cloudfront_function.rewrite_index: Refreshing state... [id=branch-frontend-rewrite-index]
data.aws_caller_identity.current: Reading...
aws_cloudfront_origin_access_control.frontend: Refreshing state... [id=E2T090T8V5CDLN]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::489881683177:oidc-provider/token.actions.githubusercontent.com]
aws_api_gateway_rest_api.branch_api: Refreshing state... [id=2apxzxb0r8]
aws_iam_role.lambda_role: Refreshing state... [id=branch-lambda-role]
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_cognito_user_pool.branch_user_pool: Refreshing state... [id=us-east-2_CxTueqe6g]
data.aws_caller_identity.current: Read complete after 0s [id=489881683177]
aws_s3_bucket.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_s3_bucket.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
data.infisical_secrets.github_folder: Reading...
data.infisical_secrets.rds_folder: Reading...
data.infisical_secrets.github_folder: Read complete after 0s
data.aws_iam_policy_document.ci_plan_assume: Reading...
data.aws_iam_policy_document.ci_plan_assume: Read complete after 0s [id=3057813384]
data.aws_iam_policy_document.ci_apply_assume: Reading...
data.aws_iam_policy_document.ci_apply_assume: Read complete after 0s [id=813913]
data.aws_iam_policy_document.ci_migrate_assume: Reading...
data.aws_iam_policy_document.ci_migrate_assume: Read complete after 0s [id=3474878989]
data.aws_iam_policy_document.ci_preview_assume: Reading...
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
data.aws_iam_policy_document.ci_preview_assume: Read complete after 0s [id=282080688]
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
data.infisical_secrets.rds_folder: Read complete after 0s
aws_iam_role.ci_migrate: Refreshing state... [id=branch-ci-migrate]
aws_api_gateway_gateway_response.cors["DEFAULT_5XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_5XX]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=wsnfk2]
aws_api_gateway_gateway_response.cors["DEFAULT_4XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_4XX]
aws_api_gateway_resource.lambda_resources["users"]: Refreshing state... [id=0dkbds]
aws_api_gateway_resource.lambda_resources["auth"]: Refreshing state... [id=u8unad]
aws_api_gateway_resource.lambda_resources["donors"]: Refreshing state... [id=hybur2]
aws_api_gateway_resource.lambda_resources["expenditures"]: Refreshing state... [id=6sdj3w]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=chhy2i]
aws_iam_role.ci_preview: Refreshing state... [id=branch-ci-preview]
aws_cognito_user_pool_client.branch_client: Refreshing state... [id=570i6ocj0882qu0ditm4vrr60f]
aws_iam_role_policy.lambda_cognito_admin: Refreshing state... [id=branch-lambda-role:branch-lambda-cognito-admin]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
data.aws_vpc.default: Read complete after 1s [id=vpc-0a9ccfb59c8918ce9]
aws_iam_role_policy.ci_plan_state_lock: Refreshing state... [id=branch-ci-plan:tfstate-lock]
aws_iam_role_policy_attachment.ci_plan_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/ReadOnlyAccess]
aws_api_gateway_resource.lambda_proxy["auth"]: Refreshing state... [id=srhf9j]
aws_api_gateway_resource.lambda_proxy["donors"]: Refreshing state... [id=xkazax]
aws_api_gateway_resource.lambda_proxy["expenditures"]: Refreshing state... [id=14khv0]
aws_api_gateway_resource.lambda_proxy["users"]: Refreshing state... [id=4sjlu3]
aws_api_gateway_resource.lambda_proxy["reports"]: Refreshing state... [id=elsvn3]
aws_api_gateway_resource.lambda_proxy["projects"]: Refreshing state... [id=kmwcxq]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_method.lambda_methods["projects-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_method.lambda_methods["users-GET"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_method.lambda_methods["auth-POST"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-POST]
aws_api_gateway_method.lambda_methods["auth-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_method.lambda_methods["reports-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-GET]
aws_api_gateway_method.lambda_methods["expenditures-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_method.lambda_methods["users-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-GET]
aws_api_gateway_method.lambda_methods["donors-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-GET]
aws_security_group.rds: Refreshing state... [id=sg-0c8a1ff1676a14edb]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_iam_role_policy.lambda_s3_objects: Refreshing state... [id=branch-lambda-role:branch-lambda-s3-objects]
aws_cloudfront_distribution.frontend: Refreshing state... [id=E37FDHRYNZNF4R]
aws_s3_bucket_public_access_block.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_api_gateway_method.lambda_proxy_any["reports"]: Refreshing state... [id=agm-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_method.lambda_proxy_any["auth"]: Refreshing state... [id=agm-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_method.lambda_proxy_any["users"]: Refreshing state... [id=agm-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_method.lambda_proxy_any["expenditures"]: Refreshing state... [id=agm-2apxzxb0r8-14khv0-ANY]
aws_api_gateway_method.lambda_proxy_any["donors"]: Refreshing state... [id=agm-2apxzxb0r8-xkazax-ANY]
aws_api_gateway_method.lambda_proxy_any["projects"]: Refreshing state... [id=agm-2apxzxb0r8-kmwcxq-ANY]
aws_iam_role_policy.ci_preview: Refreshing state... [id=branch-ci-preview:preview-env]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-489881683177/donors/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-489881683177/reports/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-489881683177/users/initial.zip]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-489881683177/expenditures/initial.zip]
aws_s3_object.lambda_placeholder["projects"]: Refreshing state... [id=branch-lambda-deployments-489881683177/projects/initial.zip]
aws_s3_object.lambda_placeholder["auth"]: Refreshing state... [id=branch-lambda-deployments-489881683177/auth/initial.zip]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_vpc_security_group_ingress_rule.rds_postgres: Refreshing state... [id=sgr-0594341dc6234d55c]
aws_db_instance.branch_rds: Refreshing state... [id=db-AMMYFTORW6XJGRELV7WQZCNHQI]
aws_vpc_security_group_egress_rule.rds_all: Refreshing state... [id=sgr-099fe0d98d4b3f3b5]
data.aws_iam_policy_document.frontend_bucket: Reading...
data.aws_iam_policy_document.frontend_bucket: Read complete after 0s [id=1471335443]
aws_s3_bucket_policy.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_iam_role_policy.ci_migrate: Refreshing state... [id=branch-ci-migrate:db-migrate]
aws_lambda_permission.api_gateway_permissions["users"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["expenditures"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["donors"]: Refreshing state... [id=agi-2apxzxb0r8-xkazax-ANY]
aws_lambda_permission.api_gateway_permissions["projects"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["reports"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["expenditures"]: Refreshing state... [id=agi-2apxzxb0r8-14khv0-ANY]
aws_lambda_permission.api_gateway_permissions["auth"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["projects"]: Refreshing state... [id=agi-2apxzxb0r8-kmwcxq-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["users"]: Refreshing state... [id=agi-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["reports"]: Refreshing state... [id=agi-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["auth"]: Refreshing state... [id=agi-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-POST"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-GET]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-GET]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_integration.lambda_integrations["reports-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_integration.lambda_integrations["users-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_integration.lambda_integrations["donors-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-GET"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_integration.lambda_integrations["users-GET"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_integration.lambda_integrations["projects-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_integration.lambda_integrations["auth-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_deployment.branch_deployment: Refreshing state... [id=od3a3y]
aws_api_gateway_stage.branch_stage: Refreshing state... [id=ags-2apxzxb0r8-prod]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:# aws_api_gateway_gateway_response.cors["DEFAULT_4XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_4XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
# aws_api_gateway_gateway_response.cors["DEFAULT_5XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_5XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
Plan:0 to add, 2 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"

Pushed by: @tsudhakar87, Action: pull_request

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 74d24d4 · logs

@nourshoreibah
nourshoreibah merged commit 81c19d7 into mainAug 16, 2026
18 checks passed
@nourshoreibah
nourshoreibah deleted the 243-admin-invite-user branch August 16, 2026 19:25
github-actionsBot added a commit that referenced this pull request Aug 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-environmentCreates a temporary (nearly free) test environment. Uses prod DB and cognito

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Backend] Admin-invite user provisioning via Cognito AdminCreateUser

2 participants

@tsudhakar87@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(auth): #243 Admin Invite User - #302

Merged
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user
Aug 16, 2026
Merged

feat(auth): #243 Admin Invite User#302
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user

Conversation

@tsudhakar87

@tsudhakar87tsudhakar87 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Issue

Closes#243

📝 Description

Here's what this PR adds on top of PR #290:

Backend (lambdas/users)

  • POST /users now calls AdminCreateUser before the DB insert - invited user gets an email with a temp password and their cognito_sub is written to the DB at creation time (fixes the orphaned-row bug where users could never authenticate)
  • Rollback: if the DB insert fails after a successful Cognito call, AdminDeleteUser is called to keep the two systems in sync
  • UsernameExistsException from Cognito → 409 (deduplication at both layers)

Infrastructure

  • cognito.tf: admin_create_user_config block - disables self-signup (allow_admin_create_user_only = true), sets invite email template
  • lambda.tf: IAM policy grants AdminCreateUser + AdminDeleteUser + AdminGetUser to the shared lambda role

Frontend

  • New AddUserModal component (email, name, is-admin checkbox) - calls POST /users with the admin's access token
  • "+ Add User" button on the Accounts page wired to the modal

✔️ Verification

What steps did you take to verify your changes work? These should be clear enough for someone to be able to clone the branch and follow the steps themselves.

Screenshot 2026-08-12 at 7 32 51 PMScreenshot 2026-08-12 at 7 33 08 PMScreenshot 2026-08-12 at 7 49 12 PMScreenshot 2026-08-12 at 7 49 58 PMScreenshot 2026-08-12 at 7 50 09 PMScreenshot 2026-08-12 at 7 50 28 PM

🏕️ (Optional) Future Work / Notes

Did you notice anything ugly during the course of this ticket? Any bugs, design challenges, or unexpected behavior? Write it down so we can clean it up in a future ticket!

@tsudhakar87tsudhakar87 changed the title 243 admin invite userfeat(auth): #243 Admin Invite UserAug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bd1htawwsa.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@nourshoreibahnourshoreibah added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 4, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bhctek80u7.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 5b652f0 · logs

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

tsudhakar87and others added 4 commits August 12, 2026 19:25
….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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ng shared/lambda-auth
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 <noreply@anthropic.com>
…ssage
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 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 097c0a5 · logs

@tsudhakar87
tsudhakar87 marked this pull request as ready for review August 12, 2026 23:52
github-actionsBot added a commit that referenced this pull request Aug 12, 2026
@tsudhakar87

Copy link
Copy Markdown
ContributorAuthor

I think I need to match this to Figma

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good, thank you! just one nit


setIsLoading(true);
try {
await apiFetch('/users/', {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit: Can we move this to a hook instead of fetching inline?

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

I think I need to match this to Figma

@tsudhakar87 i think it's ok to do this in a separate pr but totally up to you

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

@tsudhakar87

tsudhakar87 commented Aug 13, 2026

Copy link
Copy Markdown
ContributorAuthor

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

ok sounds good I can fix that spacing in this PR I don't see it in Figma so I will just leave it as is lol

@github-actions

Copy link
Copy Markdown
Contributor

Terraform Plan 📖 infrastructure/aws

Terraform Initialization ⚙️success

Terraform Validation 🤖success

Terraform Plan 📖success

Show Plan
data.archive_file.lambda_placeholder: Reading...
data.archive_file.lambda_placeholder: Read complete after 0s [id=96878a51e358033297a32b882fd5223cc95fb8a7]
data.aws_vpc.default: Reading...
aws_cloudfront_function.rewrite_index: Refreshing state... [id=branch-frontend-rewrite-index]
data.aws_caller_identity.current: Reading...
aws_cloudfront_origin_access_control.frontend: Refreshing state... [id=E2T090T8V5CDLN]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::489881683177:oidc-provider/token.actions.githubusercontent.com]
aws_api_gateway_rest_api.branch_api: Refreshing state... [id=2apxzxb0r8]
aws_iam_role.lambda_role: Refreshing state... [id=branch-lambda-role]
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_cognito_user_pool.branch_user_pool: Refreshing state... [id=us-east-2_CxTueqe6g]
data.aws_caller_identity.current: Read complete after 0s [id=489881683177]
aws_s3_bucket.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_s3_bucket.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
data.infisical_secrets.github_folder: Reading...
data.infisical_secrets.rds_folder: Reading...
data.infisical_secrets.github_folder: Read complete after 0s
data.aws_iam_policy_document.ci_plan_assume: Reading...
data.aws_iam_policy_document.ci_plan_assume: Read complete after 0s [id=3057813384]
data.aws_iam_policy_document.ci_apply_assume: Reading...
data.aws_iam_policy_document.ci_apply_assume: Read complete after 0s [id=813913]
data.aws_iam_policy_document.ci_migrate_assume: Reading...
data.aws_iam_policy_document.ci_migrate_assume: Read complete after 0s [id=3474878989]
data.aws_iam_policy_document.ci_preview_assume: Reading...
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
data.aws_iam_policy_document.ci_preview_assume: Read complete after 0s [id=282080688]
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
data.infisical_secrets.rds_folder: Read complete after 0s
aws_iam_role.ci_migrate: Refreshing state... [id=branch-ci-migrate]
aws_api_gateway_gateway_response.cors["DEFAULT_5XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_5XX]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=wsnfk2]
aws_api_gateway_gateway_response.cors["DEFAULT_4XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_4XX]
aws_api_gateway_resource.lambda_resources["users"]: Refreshing state... [id=0dkbds]
aws_api_gateway_resource.lambda_resources["auth"]: Refreshing state... [id=u8unad]
aws_api_gateway_resource.lambda_resources["donors"]: Refreshing state... [id=hybur2]
aws_api_gateway_resource.lambda_resources["expenditures"]: Refreshing state... [id=6sdj3w]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=chhy2i]
aws_iam_role.ci_preview: Refreshing state... [id=branch-ci-preview]
aws_cognito_user_pool_client.branch_client: Refreshing state... [id=570i6ocj0882qu0ditm4vrr60f]
aws_iam_role_policy.lambda_cognito_admin: Refreshing state... [id=branch-lambda-role:branch-lambda-cognito-admin]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
data.aws_vpc.default: Read complete after 1s [id=vpc-0a9ccfb59c8918ce9]
aws_iam_role_policy.ci_plan_state_lock: Refreshing state... [id=branch-ci-plan:tfstate-lock]
aws_iam_role_policy_attachment.ci_plan_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/ReadOnlyAccess]
aws_api_gateway_resource.lambda_proxy["auth"]: Refreshing state... [id=srhf9j]
aws_api_gateway_resource.lambda_proxy["donors"]: Refreshing state... [id=xkazax]
aws_api_gateway_resource.lambda_proxy["expenditures"]: Refreshing state... [id=14khv0]
aws_api_gateway_resource.lambda_proxy["users"]: Refreshing state... [id=4sjlu3]
aws_api_gateway_resource.lambda_proxy["reports"]: Refreshing state... [id=elsvn3]
aws_api_gateway_resource.lambda_proxy["projects"]: Refreshing state... [id=kmwcxq]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_method.lambda_methods["projects-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_method.lambda_methods["users-GET"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_method.lambda_methods["auth-POST"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-POST]
aws_api_gateway_method.lambda_methods["auth-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_method.lambda_methods["reports-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-GET]
aws_api_gateway_method.lambda_methods["expenditures-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_method.lambda_methods["users-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-GET]
aws_api_gateway_method.lambda_methods["donors-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-GET]
aws_security_group.rds: Refreshing state... [id=sg-0c8a1ff1676a14edb]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_iam_role_policy.lambda_s3_objects: Refreshing state... [id=branch-lambda-role:branch-lambda-s3-objects]
aws_cloudfront_distribution.frontend: Refreshing state... [id=E37FDHRYNZNF4R]
aws_s3_bucket_public_access_block.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_api_gateway_method.lambda_proxy_any["reports"]: Refreshing state... [id=agm-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_method.lambda_proxy_any["auth"]: Refreshing state... [id=agm-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_method.lambda_proxy_any["users"]: Refreshing state... [id=agm-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_method.lambda_proxy_any["expenditures"]: Refreshing state... [id=agm-2apxzxb0r8-14khv0-ANY]
aws_api_gateway_method.lambda_proxy_any["donors"]: Refreshing state... [id=agm-2apxzxb0r8-xkazax-ANY]
aws_api_gateway_method.lambda_proxy_any["projects"]: Refreshing state... [id=agm-2apxzxb0r8-kmwcxq-ANY]
aws_iam_role_policy.ci_preview: Refreshing state... [id=branch-ci-preview:preview-env]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-489881683177/donors/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-489881683177/reports/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-489881683177/users/initial.zip]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-489881683177/expenditures/initial.zip]
aws_s3_object.lambda_placeholder["projects"]: Refreshing state... [id=branch-lambda-deployments-489881683177/projects/initial.zip]
aws_s3_object.lambda_placeholder["auth"]: Refreshing state... [id=branch-lambda-deployments-489881683177/auth/initial.zip]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_vpc_security_group_ingress_rule.rds_postgres: Refreshing state... [id=sgr-0594341dc6234d55c]
aws_db_instance.branch_rds: Refreshing state... [id=db-AMMYFTORW6XJGRELV7WQZCNHQI]
aws_vpc_security_group_egress_rule.rds_all: Refreshing state... [id=sgr-099fe0d98d4b3f3b5]
data.aws_iam_policy_document.frontend_bucket: Reading...
data.aws_iam_policy_document.frontend_bucket: Read complete after 0s [id=1471335443]
aws_s3_bucket_policy.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_iam_role_policy.ci_migrate: Refreshing state... [id=branch-ci-migrate:db-migrate]
aws_lambda_permission.api_gateway_permissions["users"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["expenditures"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["donors"]: Refreshing state... [id=agi-2apxzxb0r8-xkazax-ANY]
aws_lambda_permission.api_gateway_permissions["projects"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["reports"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["expenditures"]: Refreshing state... [id=agi-2apxzxb0r8-14khv0-ANY]
aws_lambda_permission.api_gateway_permissions["auth"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["projects"]: Refreshing state... [id=agi-2apxzxb0r8-kmwcxq-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["users"]: Refreshing state... [id=agi-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["reports"]: Refreshing state... [id=agi-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["auth"]: Refreshing state... [id=agi-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-POST"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-GET]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-GET]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_integration.lambda_integrations["reports-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_integration.lambda_integrations["users-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_integration.lambda_integrations["donors-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-GET"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_integration.lambda_integrations["users-GET"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_integration.lambda_integrations["projects-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_integration.lambda_integrations["auth-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_deployment.branch_deployment: Refreshing state... [id=od3a3y]
aws_api_gateway_stage.branch_stage: Refreshing state... [id=ags-2apxzxb0r8-prod]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:# aws_api_gateway_gateway_response.cors["DEFAULT_4XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_4XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
# aws_api_gateway_gateway_response.cors["DEFAULT_5XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_5XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
Plan:0 to add, 2 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"

Pushed by: @tsudhakar87, Action: pull_request

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 74d24d4 · logs

@nourshoreibah
nourshoreibah merged commit 81c19d7 into mainAug 16, 2026
18 checks passed
@nourshoreibah
nourshoreibah deleted the 243-admin-invite-user branch August 16, 2026 19:25
github-actionsBot added a commit that referenced this pull request Aug 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-environmentCreates a temporary (nearly free) test environment. Uses prod DB and cognito

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Backend] Admin-invite user provisioning via Cognito AdminCreateUser

2 participants

@tsudhakar87@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(auth): #243 Admin Invite User - #302

Merged
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user
Aug 16, 2026
Merged

feat(auth): #243 Admin Invite User#302
nourshoreibah merged 15 commits into
mainfrom
243-admin-invite-user

Conversation

@tsudhakar87

@tsudhakar87tsudhakar87 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Issue

Closes#243

📝 Description

Here's what this PR adds on top of PR #290:

Backend (lambdas/users)

  • POST /users now calls AdminCreateUser before the DB insert - invited user gets an email with a temp password and their cognito_sub is written to the DB at creation time (fixes the orphaned-row bug where users could never authenticate)
  • Rollback: if the DB insert fails after a successful Cognito call, AdminDeleteUser is called to keep the two systems in sync
  • UsernameExistsException from Cognito → 409 (deduplication at both layers)

Infrastructure

  • cognito.tf: admin_create_user_config block - disables self-signup (allow_admin_create_user_only = true), sets invite email template
  • lambda.tf: IAM policy grants AdminCreateUser + AdminDeleteUser + AdminGetUser to the shared lambda role

Frontend

  • New AddUserModal component (email, name, is-admin checkbox) - calls POST /users with the admin's access token
  • "+ Add User" button on the Accounts page wired to the modal

✔️ Verification

What steps did you take to verify your changes work? These should be clear enough for someone to be able to clone the branch and follow the steps themselves.

Screenshot 2026-08-12 at 7 32 51 PMScreenshot 2026-08-12 at 7 33 08 PMScreenshot 2026-08-12 at 7 49 12 PMScreenshot 2026-08-12 at 7 49 58 PMScreenshot 2026-08-12 at 7 50 09 PMScreenshot 2026-08-12 at 7 50 28 PM

🏕️ (Optional) Future Work / Notes

Did you notice anything ugly during the course of this ticket? Any bugs, design challenges, or unexpected behavior? Write it down so we can clean it up in a future ticket!

@tsudhakar87tsudhakar87 changed the title 243 admin invite userfeat(auth): #243 Admin Invite UserAug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@tsudhakar87tsudhakar87 added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bd1htawwsa.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@tsudhakar87tsudhakar87 removed the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

@nourshoreibahnourshoreibah added the test-environment Creates a temporary (nearly free) test environment. Uses prod DB and cognito label Aug 4, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open:https://d3nmtjoh6ir9ym.cloudfront.net/pr-302/
API:https://bhctek80u7.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here — if this PR adds a migration, endpoints using the new columns will fail until it merges. New commits update this environment in place — a note is posted here on each update. Remove the test-environment label or close the PR to tear it down.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 5b652f0 · logs

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — failed ❌ to update. See the workflow logs.

tsudhakar87and others added 4 commits August 12, 2026 19:25
….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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…ng shared/lambda-auth
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 <noreply@anthropic.com>
…ssage
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 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 097c0a5 · logs

@tsudhakar87
tsudhakar87 marked this pull request as ready for review August 12, 2026 23:52
github-actionsBot added a commit that referenced this pull request Aug 12, 2026
@tsudhakar87

Copy link
Copy Markdown
ContributorAuthor

I think I need to match this to Figma

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good, thank you! just one nit


setIsLoading(true);
try {
await apiFetch('/users/', {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit: Can we move this to a hook instead of fetching inline?

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

I think I need to match this to Figma

@tsudhakar87 i think it's ok to do this in a separate pr but totally up to you

@nourshoreibah

nourshoreibah commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

@tsudhakar87

tsudhakar87 commented Aug 13, 2026

Copy link
Copy Markdown
ContributorAuthor

Oh also @shreeyaadhikari will work on the accounts page if that's what you're talking about (but i think the modal spacing also needs to be adjusted)

ok sounds good I can fix that spacing in this PR I don't see it in Figma so I will just leave it as is lol

@github-actions

Copy link
Copy Markdown
Contributor

Terraform Plan 📖 infrastructure/aws

Terraform Initialization ⚙️success

Terraform Validation 🤖success

Terraform Plan 📖success

Show Plan
data.archive_file.lambda_placeholder: Reading...
data.archive_file.lambda_placeholder: Read complete after 0s [id=96878a51e358033297a32b882fd5223cc95fb8a7]
data.aws_vpc.default: Reading...
aws_cloudfront_function.rewrite_index: Refreshing state... [id=branch-frontend-rewrite-index]
data.aws_caller_identity.current: Reading...
aws_cloudfront_origin_access_control.frontend: Refreshing state... [id=E2T090T8V5CDLN]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::489881683177:oidc-provider/token.actions.githubusercontent.com]
aws_api_gateway_rest_api.branch_api: Refreshing state... [id=2apxzxb0r8]
aws_iam_role.lambda_role: Refreshing state... [id=branch-lambda-role]
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_cognito_user_pool.branch_user_pool: Refreshing state... [id=us-east-2_CxTueqe6g]
data.aws_caller_identity.current: Read complete after 0s [id=489881683177]
aws_s3_bucket.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_s3_bucket.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
data.infisical_secrets.github_folder: Reading...
data.infisical_secrets.rds_folder: Reading...
data.infisical_secrets.github_folder: Read complete after 0s
data.aws_iam_policy_document.ci_plan_assume: Reading...
data.aws_iam_policy_document.ci_plan_assume: Read complete after 0s [id=3057813384]
data.aws_iam_policy_document.ci_apply_assume: Reading...
data.aws_iam_policy_document.ci_apply_assume: Read complete after 0s [id=813913]
data.aws_iam_policy_document.ci_migrate_assume: Reading...
data.aws_iam_policy_document.ci_migrate_assume: Read complete after 0s [id=3474878989]
data.aws_iam_policy_document.ci_preview_assume: Reading...
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
data.aws_iam_policy_document.ci_preview_assume: Read complete after 0s [id=282080688]
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
data.infisical_secrets.rds_folder: Read complete after 0s
aws_iam_role.ci_migrate: Refreshing state... [id=branch-ci-migrate]
aws_api_gateway_gateway_response.cors["DEFAULT_5XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_5XX]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=wsnfk2]
aws_api_gateway_gateway_response.cors["DEFAULT_4XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_4XX]
aws_api_gateway_resource.lambda_resources["users"]: Refreshing state... [id=0dkbds]
aws_api_gateway_resource.lambda_resources["auth"]: Refreshing state... [id=u8unad]
aws_api_gateway_resource.lambda_resources["donors"]: Refreshing state... [id=hybur2]
aws_api_gateway_resource.lambda_resources["expenditures"]: Refreshing state... [id=6sdj3w]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=chhy2i]
aws_iam_role.ci_preview: Refreshing state... [id=branch-ci-preview]
aws_cognito_user_pool_client.branch_client: Refreshing state... [id=570i6ocj0882qu0ditm4vrr60f]
aws_iam_role_policy.lambda_cognito_admin: Refreshing state... [id=branch-lambda-role:branch-lambda-cognito-admin]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
data.aws_vpc.default: Read complete after 1s [id=vpc-0a9ccfb59c8918ce9]
aws_iam_role_policy.ci_plan_state_lock: Refreshing state... [id=branch-ci-plan:tfstate-lock]
aws_iam_role_policy_attachment.ci_plan_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/ReadOnlyAccess]
aws_api_gateway_resource.lambda_proxy["auth"]: Refreshing state... [id=srhf9j]
aws_api_gateway_resource.lambda_proxy["donors"]: Refreshing state... [id=xkazax]
aws_api_gateway_resource.lambda_proxy["expenditures"]: Refreshing state... [id=14khv0]
aws_api_gateway_resource.lambda_proxy["users"]: Refreshing state... [id=4sjlu3]
aws_api_gateway_resource.lambda_proxy["reports"]: Refreshing state... [id=elsvn3]
aws_api_gateway_resource.lambda_proxy["projects"]: Refreshing state... [id=kmwcxq]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_method.lambda_methods["projects-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_method.lambda_methods["users-GET"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_method.lambda_methods["auth-POST"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-POST]
aws_api_gateway_method.lambda_methods["auth-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_method.lambda_methods["reports-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-GET]
aws_api_gateway_method.lambda_methods["expenditures-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_method.lambda_methods["users-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-GET]
aws_api_gateway_method.lambda_methods["donors-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-GET]
aws_security_group.rds: Refreshing state... [id=sg-0c8a1ff1676a14edb]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_iam_role_policy.lambda_s3_objects: Refreshing state... [id=branch-lambda-role:branch-lambda-s3-objects]
aws_cloudfront_distribution.frontend: Refreshing state... [id=E37FDHRYNZNF4R]
aws_s3_bucket_public_access_block.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_api_gateway_method.lambda_proxy_any["reports"]: Refreshing state... [id=agm-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_method.lambda_proxy_any["auth"]: Refreshing state... [id=agm-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_method.lambda_proxy_any["users"]: Refreshing state... [id=agm-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_method.lambda_proxy_any["expenditures"]: Refreshing state... [id=agm-2apxzxb0r8-14khv0-ANY]
aws_api_gateway_method.lambda_proxy_any["donors"]: Refreshing state... [id=agm-2apxzxb0r8-xkazax-ANY]
aws_api_gateway_method.lambda_proxy_any["projects"]: Refreshing state... [id=agm-2apxzxb0r8-kmwcxq-ANY]
aws_iam_role_policy.ci_preview: Refreshing state... [id=branch-ci-preview:preview-env]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-489881683177/donors/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-489881683177/reports/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-489881683177/users/initial.zip]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-489881683177/expenditures/initial.zip]
aws_s3_object.lambda_placeholder["projects"]: Refreshing state... [id=branch-lambda-deployments-489881683177/projects/initial.zip]
aws_s3_object.lambda_placeholder["auth"]: Refreshing state... [id=branch-lambda-deployments-489881683177/auth/initial.zip]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_vpc_security_group_ingress_rule.rds_postgres: Refreshing state... [id=sgr-0594341dc6234d55c]
aws_db_instance.branch_rds: Refreshing state... [id=db-AMMYFTORW6XJGRELV7WQZCNHQI]
aws_vpc_security_group_egress_rule.rds_all: Refreshing state... [id=sgr-099fe0d98d4b3f3b5]
data.aws_iam_policy_document.frontend_bucket: Reading...
data.aws_iam_policy_document.frontend_bucket: Read complete after 0s [id=1471335443]
aws_s3_bucket_policy.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_iam_role_policy.ci_migrate: Refreshing state... [id=branch-ci-migrate:db-migrate]
aws_lambda_permission.api_gateway_permissions["users"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["expenditures"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["donors"]: Refreshing state... [id=agi-2apxzxb0r8-xkazax-ANY]
aws_lambda_permission.api_gateway_permissions["projects"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["reports"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["expenditures"]: Refreshing state... [id=agi-2apxzxb0r8-14khv0-ANY]
aws_lambda_permission.api_gateway_permissions["auth"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_proxy_integrations["projects"]: Refreshing state... [id=agi-2apxzxb0r8-kmwcxq-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["users"]: Refreshing state... [id=agi-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["reports"]: Refreshing state... [id=agi-2apxzxb0r8-elsvn3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["auth"]: Refreshing state... [id=agi-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-POST"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-GET]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-GET]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_integration.lambda_integrations["reports-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_integration.lambda_integrations["users-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_integration.lambda_integrations["donors-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["expenditures-GET"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_integration.lambda_integrations["users-GET"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_integration.lambda_integrations["projects-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_integration.lambda_integrations["auth-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_deployment.branch_deployment: Refreshing state... [id=od3a3y]
aws_api_gateway_stage.branch_stage: Refreshing state... [id=ags-2apxzxb0r8-prod]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:# aws_api_gateway_gateway_response.cors["DEFAULT_4XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_4XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
# aws_api_gateway_gateway_response.cors["DEFAULT_5XX"] will be updated in-place
~ resource"aws_api_gateway_gateway_response""cors" {
id="aggr-2apxzxb0r8-DEFAULT_5XX"
~ response_templates={
-"application/json" = "{\"message\":$context.error.messageString}"->null
}
# (5 unchanged attributes hidden)
}
Plan:0 to add, 2 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"

Pushed by: @tsudhakar87, Action: pull_request

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Test environment updated in place ✅ — Click here to open. updated for 74d24d4 · logs

@nourshoreibah
nourshoreibah merged commit 81c19d7 into mainAug 16, 2026
18 checks passed
@nourshoreibah
nourshoreibah deleted the 243-admin-invite-user branch August 16, 2026 19:25
github-actionsBot added a commit that referenced this pull request Aug 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment torn down 🧹 — the stack for this PR has been destroyed.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-environmentCreates a temporary (nearly free) test environment. Uses prod DB and cognito

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Backend] Admin-invite user provisioning via Cognito AdminCreateUser

2 participants

@tsudhakar87@nourshoreibah