Skip to content

feat(auth): wire auth end to end across frontend, backend and Cognito - #289

Merged
nourshoreibah merged 3 commits into
mainfrom
feat/auth-end-to-end
Jul 28, 2026
Merged

feat(auth): wire auth end to end across frontend, backend and Cognito#289
nourshoreibah merged 3 commits into
mainfrom
feat/auth-end-to-end

Conversation

@nourshoreibah

@nourshoreibah nourshoreibah commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Context

Auth was half-built on both sides of the wire and the halves didn't meet.

The reported symptom — opening the site lands you on a dashboard as if logged in — was apps/frontend/src/app/page.tsx, which rendered <NavBar role="admin" /> with no auth check. It wasn't a faulty redirect; no redirect was ever written. There were zero route guards in the entire frontend — no middleware.ts, no ProtectedRoute, and not one page read isAuthenticated. AuthContext was fully implemented and consumed by nothing, which is exactly why CI stayed green.

Login could hang. handleLogin drove amazon-cognito-identity-js with only onSuccess/onFailure/newPasswordRequired callbacks. Any other challenge (SOFTWARE_TOKEN_MFA, SMS_MFA, MFA_SETUP, SELECT_MFA_TYPE) resolved no promise at all, so the lambda ran to its 30s timeout. NEW_PASSWORD_REQUIRED returned 403 without a Session and had no follow-up endpoint — an unrecoverable dead end. Turning MFA on would have broken login.

Sessions died silently at one hour. No /auth/refresh existed; InitiateAuthCommand was imported and never called. The frontend stored branch_refresh_token and never read it. Nothing handled a 401.

Anyone could create an account. POST /auth/register is public and unauthenticated, and when no branch.users row matched the email it inserted one. This predates the branch, but it mattered more than is_admin: false suggests: four list endpoints authorize on isAuthenticated alone with no project scoping, so a self-registered stranger could read the full donor list and financial history.

⚠️ Merge and apply the Terraform change first

infrastructure/aws/lambda.tf declared a complete environment block containing only NODE_ENV + DB_*, with ignore_changes listing just s3_key. The Cognito IDs exist in production solely as hand-set console values (proven by preview-env.yml:120, which reads them off the live branch-auth function). The next terraform apply of infrastructure/aws would have deleted them — and because authenticate.ts throws inside a try, that degrades to blanket silent 401s across all six lambdas, not a loud failure.

Before applying, record the live values and confirm the plan matches:

aws lambda get-function-configuration --function-name branch-auth --query 'Environment.Variables'

If they differ, the pool in state is not the pool in the console — stop and reconcile.

Two facts that shaped the design

  • is_admin is not a JWT claim. It lives only in Postgres; there is no pre-token-generation trigger, and a Cognito access token carries no email or name either. Hence the new GET /auth/me — the frontend cannot decode its way to identity or role. This is why Navbar.tsx hardcoded role="admin".
  • Static export on S3+CloudFront means no server, so middleware.ts would never run. All guarding is client-side.

Account provisioning is now invitation-only

Registration can no longer create a branch.users row, only claim one an admin already approved:

  1. Admin creates the invitation via the ADMIN-gated POST /users — a row with cognito_sub = NULL.
  2. The invitee POST /auth/registers with that email, which claims the row (setting cognito_sub, never touching is_admin).
  3. POST /auth/verify-email with the emailed code, then POST /auth/login.

An email with no pending invitation gets 403 INVITATION_REQUIRED; an already-claimed one gets 409. The 403 is deliberately identical whether or not the address exists, so the endpoint can't be used to enumerate staff emails.

The DB row is the real control rather than the pool configuration — authenticateRequest rejects any Cognito identity whose sub has no row, so an identity created out of band stays inert. AdminCreateUser also works as an invitation path: it yields NEW_PASSWORD_REQUIRED, which the login page now handles.

Changes

FrontendAuthGate mounted once in providers.tsx; lib/routes.ts is protected-by-default, so a new page is gated without anyone opting in. New /dashboard and /projects pages the Navbar always linked to. ApiError carrying status, authTokens.ts as sole owner of storage, authClient.ts with single-flight refresh + one 401 retry, useApi() replacing token-through-props. Session bootstraps from /auth/me; refresh is scheduled from token exp.

Backend — SRP library replaced with InitiateAuth/RespondToAuthChallenge on USER_PASSWORD_AUTH (the SDK's Session is an opaque string that survives across invocations, which is what makes a stateless /auth/respond-challenge possible). New /auth/refresh, /auth/respond-challenge, /auth/me. Claim-on-register so the seeded admins can finally sign in. Adding a challenge type is now one row in CHALLENGE_SPECS.

Authorization fixesPATCH /users/{userId} self-escalation (any user could POST {isAdmin: true} to their own id); DELETE /projects/{id} had no check at all behind a stale TODO citing an already-merged ticket; PATCH missing from Allow-Methods, which made PATCH /users/{userId} unreachable from a browser.

Infra — Cognito IDs into the lambda env, pool-scoped cognito-idp policy for the registration-rollback path, advanced_security_modeAUDIT (all sign-ins arrive from one Lambda ENI, so adaptive auth was risk-scoring a shared address), and the two outputs the docs already told operators to use but which didn't exist.

Decisions worth a look

  • MFA-safe now, TOTP later. mfa_configuration stays OFF, but login can no longer hang and every challenge name is plumbed through. Enabling MFA becomes a Terraform change.
  • Cognito Admins group promotion dropped. Postgres is_admin is now the single source of truth — two sources would make demotion via PATCH /users/{userId} silently ineffective. The branch was 100% dead (no such group exists).
  • No self-serve signup UI. register/verifyEmail/resendCode removed from the context; the backend routes stay for the invitation-claim flow. Rationale is recorded in a comment so nobody re-adds them.

Verification

Frontend 24 suites / 249 tests, tsc clean, eslint clean, production build emits /dashboard + /projects
shared/lambda-auth 42 tests — it had zero before, and now runs in CI via a new job
Auth lambda 66 unit tests, covering every Cognito SDK interaction (none were covered before)
users / projects 52 / 5 tests
Terraform fmt -check and validate pass

Regression guards worth calling out: SOFTWARE_TOKEN_MFA returns 200 under an explicit 2s jest timeout so the hang fails fast rather than stalling the suite; an invented challenge name proves an unknown challenge can't hang; a seeded ID token claiming is_admin: true against /auth/me saying false must yield false; five concurrent 401s trigger exactly one /auth/refresh; an uninvited registration creates neither a Cognito user nor a DB row.

Not run locally: the auth e2e suite. It hardcodes localhost:3000 and that port is occupied on my machine, so it returns 404 rather than connecting. Its three original cases were invalidated by invitation-only (two inserted rows without a cognito_sub, which now reads as a pending invitation rather than a conflict, and one created an account from nothing); all four have been rewritten and typecheck, but please confirm they pass in CI, which runs on a clean runner.

Still needs a real Cognito user, run manually once: NEW_PASSWORD_REQUIRED end to end (admin-create-user --temporary-password … --message-action SUPPRESS), AdminDeleteUser IAM post-apply, and refresh across the real one-hour boundary. Verification emails are capped at 50/day pool-wide — don't loop registration tests.

Out of scope (worth tickets)

  • GET /expenditures, /reports, /donors, /donations have no project scoping — any authenticated user reads every row. Invitation-only removes the anonymous path to that data, but this should still be fixed; deliberately left to its own PR.
  • POST /donors returns 201 without writing anything.
  • api_gateway.tf grants only GET on bare /donors and /reports while the handlers serve POST there.
  • branch-lambda-role has no s3:PutObject, so report generation is likely failing AccessDenied — same root-cause class as the Cognito IAM gap fixed here.
  • lambda.zip build artifacts aren't gitignored; worth adding *.zip.

🤖 Generated with Claude Code

nourshoreibah and others added 3 commits July 27, 2026 21:36
Auth was half-built on both sides of the wire and the halves did not meet.

Frontend had no route guard of any kind -- no middleware, no wrapper, and not
one page read `isAuthenticated`. The root route rendered `<NavBar role="admin">`
unconditionally, so opening the site showed the admin shell to anonymous
visitors. `AuthContext` was fully implemented and consumed by nothing, which is
why CI stayed green.

Login could also hang. `handleLogin` drove `amazon-cognito-identity-js` with
only onSuccess/onFailure/newPasswordRequired callbacks, so any other Cognito
challenge resolved no promise and the lambda ran to its 30s timeout.
NEW_PASSWORD_REQUIRED returned 403 without a Session and had no follow-up
endpoint, making it a dead end.

Frontend
- Add AuthGate, mounted once in providers.tsx. Static export means middleware
  would never run, so guarding is client-side. lib/routes.ts is
  protected-by-default: a new page is gated without opting in.
- Rewrite the root route to route by session, and absorb the CloudFront SPA
  fallback (403/404 -> /index.html) with a sessionStorage loop breaker.
- Add the /dashboard and /projects pages the Navbar always linked to, and drop
  the /profile and /logout links that pointed nowhere.
- Bootstrap the session from GET /auth/me instead of trusting a decoded ID
  token, so a revoked session no longer looks signed in.
- Add ApiError (carrying status), lib/authTokens.ts as the sole owner of token
  storage, and lib/authClient.ts with single-flight refresh plus one 401 retry.
  Components now use useApi() rather than threading a token through props; the
  old `localStorage.getItem(...) ?? ''` pattern silently sent unauthenticated
  requests.
- Schedule refresh from the token exp, so sessions stop breaking at one hour.
- Derive the Navbar role and Header identity from the real session.
- Handle NEW_PASSWORD_REQUIRED at login, and stop reporting all failures as bad
  credentials. Throw instead of persisting `undefined` when a response carries
  no tokens.
- Stop showing "Password Changed" when the reset request failed.

Backend
- Replace the SRP library with InitiateAuth/RespondToAuthChallenge on
  USER_PASSWORD_AUTH. The SDK returns Session as an opaque string, which is what
  makes a stateless /auth/respond-challenge possible, and an unrecognised
  ChallengeName becomes a value rather than a hang.
- Add POST /auth/refresh, POST /auth/respond-challenge and GET /auth/me. Adding
  a challenge type is now a row in CHALLENGE_SPECS, so enabling MFA is a
  configuration change rather than a code change.
- Treat a branch.users row with a NULL cognito_sub as a pending invitation and
  claim it on register, never touching is_admin. The seeded admins could not
  previously sign in at all.
- Make branch.users.is_admin the single source of truth by dropping the dead
  Cognito "Admins" group promotion.
- Fix PATCH /users/{userId} self-escalation: is_admin is admin-only.
- Gate DELETE /projects/{id}, which had no authorization check behind a stale
  TODO referencing an already-merged ticket.
- Advertise PATCH in Allow-Methods, without which PATCH /users/{userId} was
  unreachable from a browser.

Infra
- Declare COGNITO_USER_POOL_ID, COGNITO_CLIENT_ID and REPORTS_BUCKET_NAME in the
  lambda environment block. They existed only as hand-set console values, so the
  next apply would have removed them and silently 401'd every authenticated
  request across all six lambdas.
- Add a pool-scoped cognito-idp policy for the registration-rollback path.
- Set advanced_security_mode to AUDIT: every sign-in is proxied through the auth
  lambda, so adaptive auth was risk-scoring one shared ENI address.
- Add the cognito_user_pool_id and cognito_client_id outputs the docs referenced
  but which did not exist.

Tests
- shared/lambda-auth had none; add 42, including guards against re-adding the
  group promotion and against a missing pool id throwing rather than degrading.
- Cover every Cognito SDK interaction in the auth lambda, including a
  SOFTWARE_TOKEN_MFA case with a short timeout so the hang cannot come back.
- Add frontend coverage for the guard, refresh, route policy and reset flow, and
  run the shared package in CI, which previously never executed its tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
  - Auto-formatted .tf files with terraform fmt
  - Updated README.md with terraform-docs

  Co-authored-by: nourshoreibah <nourshoreibah@users.noreply.github.com>
  Co-authored-by: nourshoreibah <nourshoreibah@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

Database Types Auto-Regenerated

The database schema has changed and the shared TypeScript definitions were regenerated:

  • shared/types/db-types.d.ts

All lambdas consume these types through the @branch/types package, so they are now in sync with the schema changes.

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

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

Copy link
Copy Markdown
Contributor

Database Types Check Complete

The database schema files were modified, but the regenerated TypeScript types are identical to the existing ones.

No changes were needed and the type definitions are already up to date.

@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.infisical_secrets.github_folder: Reading...
data.infisical_secrets.rds_folder: Reading...
data.infisical_secrets.github_folder: Read complete after 0s
data.infisical_secrets.rds_folder: Read complete after 0s
aws_cloudfront_function.rewrite_index: Refreshing state... [id=branch-frontend-rewrite-index]
data.aws_caller_identity.current: Reading...
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_db_instance.branch_rds: Refreshing state... [id=db-AMMYFTORW6XJGRELV7WQZCNHQI]
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_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.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_bucket.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_api_gateway_resource.lambda_resources["donors"]: Refreshing state... [id=hybur2]
aws_api_gateway_gateway_response.cors["DEFAULT_5XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_5XX]
aws_api_gateway_gateway_response.cors["DEFAULT_4XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_4XX]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=chhy2i]
aws_api_gateway_resource.lambda_resources["users"]: Refreshing state... [id=0dkbds]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=wsnfk2]
aws_api_gateway_resource.lambda_resources["expenditures"]: Refreshing state... [id=6sdj3w]
aws_api_gateway_resource.lambda_resources["auth"]: Refreshing state... [id=u8unad]
data.aws_iam_policy_document.ci_preview_assume: Reading...
data.aws_iam_policy_document.ci_preview_assume: Read complete after 0s [id=282080688]
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_plan_assume: Reading...
aws_iam_role.ci_preview: Refreshing state... [id=branch-ci-preview]
data.aws_iam_policy_document.ci_plan_assume: Read complete after 0s [id=3057813384]
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
aws_cognito_user_pool_client.branch_client: Refreshing state... [id=570i6ocj0882qu0ditm4vrr60f]
aws_api_gateway_resource.lambda_proxy["users"]: Refreshing state... [id=4sjlu3]
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["projects"]: Refreshing state... [id=kmwcxq]
aws_api_gateway_resource.lambda_proxy["reports"]: Refreshing state... [id=elsvn3]
aws_api_gateway_method.lambda_methods["reports-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_method.lambda_methods["auth-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-GET]
aws_api_gateway_method.lambda_methods["projects-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_method.lambda_methods["expenditures-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_method.lambda_methods["users-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_method.lambda_methods["donors-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_method.lambda_methods["users-GET"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-GET]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_method.lambda_methods["auth-POST"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-POST]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-GET]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_s3_bucket_public_access_block.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-489881683177/expenditures/initial.zip]
aws_s3_object.lambda_placeholder["auth"]: Refreshing state... [id=branch-lambda-deployments-489881683177/auth/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-489881683177/users/initial.zip]
aws_s3_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-489881683177/donors/initial.zip]
aws_api_gateway_method.lambda_proxy_any["users"]: Refreshing state... [id=agm-2apxzxb0r8-4sjlu3-ANY]
aws_s3_object.lambda_placeholder["projects"]: Refreshing state... [id=branch-lambda-deployments-489881683177/projects/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-489881683177/reports/initial.zip]
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_api_gateway_method.lambda_proxy_any["auth"]: Refreshing state... [id=agm-2apxzxb0r8-srhf9j-ANY]
aws_api_gateway_method.lambda_proxy_any["reports"]: Refreshing state... [id=agm-2apxzxb0r8-elsvn3-ANY]
aws_iam_role_policy.ci_preview: Refreshing state... [id=branch-ci-preview:preview-env]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
aws_s3_bucket_policy.reports_bucket_policy: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
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_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_cloudfront_distribution.frontend: Refreshing state... [id=E37FDHRYNZNF4R]
aws_lambda_permission.api_gateway_permissions["reports"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_integrations["donors-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-OPTIONS]
aws_lambda_permission.api_gateway_permissions["expenditures"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["projects"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["users"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_lambda_permission.api_gateway_permissions["auth"]: Refreshing state... [id=AllowAPIGatewayInvoke]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-GET]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_integration.lambda_integrations["expenditures-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_integration.lambda_integrations["expenditures-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_integration.lambda_integrations["auth-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-DELETE]
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["expenditures-POST"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-POST]
aws_api_gateway_integration.lambda_integrations["reports-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["users-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-GET]
aws_api_gateway_integration.lambda_integrations["projects-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_integration.lambda_proxy_integrations["projects"]: Refreshing state... [id=agi-2apxzxb0r8-kmwcxq-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_proxy_integrations["users"]: Refreshing state... [id=agi-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["donors"]: Refreshing state... [id=agi-2apxzxb0r8-xkazax-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["expenditures"]: Refreshing state... [id=agi-2apxzxb0r8-14khv0-ANY]
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_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:
  + create
  ~ 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_parameters = {
          ~ "gatewayresponse.header.Access-Control-Allow-Methods" = "'GET,POST,PUT,DELETE,OPTIONS'" -> "'GET,POST,PUT,PATCH,DELETE,OPTIONS'"
            # (2 unchanged elements hidden)
        }
      ~ response_templates  = {
          - "application/json" = "{\"message\":$context.error.messageString}" -> null
        }
        # (4 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_parameters = {
          ~ "gatewayresponse.header.Access-Control-Allow-Methods" = "'GET,POST,PUT,DELETE,OPTIONS'" -> "'GET,POST,PUT,PATCH,DELETE,OPTIONS'"
            # (2 unchanged elements hidden)
        }
      ~ response_templates  = {
          - "application/json" = "{\"message\":$context.error.messageString}" -> null
        }
        # (4 unchanged attributes hidden)
    }

  # aws_cognito_user_pool.branch_user_pool will be updated in-place
  ~ resource "aws_cognito_user_pool" "branch_user_pool" {
        id                         = "us-east-2_CxTueqe6g"
        name                       = "branch-user-pool"
        tags                       = {
            "Environment" = "development"
            "ManagedBy"   = "terraform"
            "Project"     = "branch"
        }
        # (18 unchanged attributes hidden)

      ~ user_pool_add_ons {
          ~ advanced_security_mode = "ENFORCED" -> "AUDIT"

            # (1 unchanged block hidden)
        }

        # (8 unchanged blocks hidden)
    }

  # aws_iam_role_policy.lambda_cognito_admin will be created
  + resource "aws_iam_role_policy" "lambda_cognito_admin" {
      + id          = (known after apply)
      + name        = "branch-lambda-cognito-admin"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = [
                          + "cognito-idp:AdminDeleteUser",
                          + "cognito-idp:AdminGetUser",
                        ]
                      + Effect   = "Allow"
                      + Resource = "arn:aws:cognito-idp:us-east-2:489881683177:userpool/us-east-2_CxTueqe6g"
                      + Sid      = "AuthLambdaUserPoolAdmin"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = "branch-lambda-role"
    }

  # aws_lambda_function.functions["auth"] will be updated in-place
  ~ resource "aws_lambda_function" "functions" {
        id                             = "branch-auth"
        tags                           = {}
        # (32 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "COGNITO_CLIENT_ID"    = "570i6ocj0882qu0ditm4vrr60f"
              + "COGNITO_USER_POOL_ID" = "us-east-2_CxTueqe6g"
              + "REPORTS_BUCKET_NAME"  = "c4c-branch-generated-reports20251030194253425700000001"
                # (6 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # aws_lambda_function.functions["donors"] will be updated in-place
  ~ resource "aws_lambda_function" "functions" {
        id                             = "branch-donors"
        tags                           = {}
        # (32 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "COGNITO_CLIENT_ID"    = "570i6ocj0882qu0ditm4vrr60f"
              + "COGNITO_USER_POOL_ID" = "us-east-2_CxTueqe6g"
              + "REPORTS_BUCKET_NAME"  = "c4c-branch-generated-reports20251030194253425700000001"
                # (6 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # aws_lambda_function.functions["expenditures"] will be updated in-place
  ~ resource "aws_lambda_function" "functions" {
        id                             = "branch-expenditures"
        tags                           = {}
        # (32 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "COGNITO_CLIENT_ID"    = "570i6ocj0882qu0ditm4vrr60f"
              + "COGNITO_USER_POOL_ID" = "us-east-2_CxTueqe6g"
              + "REPORTS_BUCKET_NAME"  = "c4c-branch-generated-reports20251030194253425700000001"
                # (6 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # aws_lambda_function.functions["projects"] will be updated in-place
  ~ resource "aws_lambda_function" "functions" {
        id                             = "branch-projects"
        tags                           = {}
        # (32 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "COGNITO_CLIENT_ID"    = "570i6ocj0882qu0ditm4vrr60f"
              + "COGNITO_USER_POOL_ID" = "us-east-2_CxTueqe6g"
              + "REPORTS_BUCKET_NAME"  = "c4c-branch-generated-reports20251030194253425700000001"
                # (6 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # aws_lambda_function.functions["reports"] will be updated in-place
  ~ resource "aws_lambda_function" "functions" {
        id                             = "branch-reports"
        tags                           = {}
        # (32 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "COGNITO_CLIENT_ID"    = "570i6ocj0882qu0ditm4vrr60f"
              + "COGNITO_USER_POOL_ID" = "us-east-2_CxTueqe6g"
              + "REPORTS_BUCKET_NAME"  = "c4c-branch-generated-reports20251030194253425700000001"
                # (6 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # aws_lambda_function.functions["users"] will be updated in-place
  ~ resource "aws_lambda_function" "functions" {
        id                             = "branch-users"
        tags                           = {}
        # (32 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              + "COGNITO_CLIENT_ID"    = "570i6ocj0882qu0ditm4vrr60f"
              + "COGNITO_USER_POOL_ID" = "us-east-2_CxTueqe6g"
              + "REPORTS_BUCKET_NAME"  = "c4c-branch-generated-reports20251030194253425700000001"
                # (6 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

Plan: 1 to add, 9 to change, 0 to destroy.

Changes to Outputs:
  + cognito_client_id                   = "570i6ocj0882qu0ditm4vrr60f"
  + cognito_user_pool_id                = "us-east-2_CxTueqe6g"

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

Pushed by: @github-actions[bot], Action: pull_request

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview environment — ready ✅

Open: https://d3nmtjoh6ir9ym.cloudfront.net/pr-289/
API: https://5jvj7rt5p1.execute-api.us-east-2.amazonaws.com/prod

Shared RDS + Cognito (prod data); DB migrations are not applied here. 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.

@nourshoreibah
nourshoreibah marked this pull request as ready for review July 28, 2026 01:45
@nourshoreibah
nourshoreibah merged commit f5e8b48 into main Jul 28, 2026
20 checks passed
@nourshoreibah
nourshoreibah deleted the feat/auth-end-to-end branch July 28, 2026 01:45
@github-actions
github-actions Bot requested review from mehanana and tsudhakar87 July 28, 2026 01:45
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant