Skip to content

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

Draft
tsudhakar87 wants to merge 8 commits into
mainfrom
243-admin-invite-user
Draft

feat(auth): #243 Admin Invite User#302
tsudhakar87 wants to merge 8 commits into
mainfrom
243-admin-invite-user

Conversation

@tsudhakar87

@tsudhakar87 tsudhakar87 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.

Provide screenshots of any new components, styling changes, or pages.

🏕️ (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!

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

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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

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

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Aug 3, 2026

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.

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

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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

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

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🌿 ⏳ Creating preview environment… (logs)

@github-actions

github-actions Bot commented Aug 4, 2026

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

github-actions Bot commented Aug 4, 2026

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]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::489881683177:oidc-provider/token.actions.githubusercontent.com]
aws_s3_bucket.reports_bucket: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_iam_role.lambda_role: Refreshing state... [id=branch-lambda-role]
aws_api_gateway_rest_api.branch_api: Refreshing state... [id=2apxzxb0r8]
data.aws_caller_identity.current: Reading...
aws_cloudfront_origin_access_control.frontend: Refreshing state... [id=E2T090T8V5CDLN]
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]
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
data.aws_iam_policy_document.ci_preview_assume: Reading...
data.aws_iam_policy_document.ci_migrate_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_migrate_assume: Read complete after 0s [id=3474878989]
data.aws_iam_policy_document.ci_apply_assume: Read complete after 0s [id=813913]
data.aws_iam_policy_document.ci_plan_assume: Reading...
aws_api_gateway_gateway_response.cors["DEFAULT_4XX"]: Refreshing state... [id=aggr-2apxzxb0r8-DEFAULT_4XX]
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_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["donors"]: Refreshing state... [id=hybur2]
aws_api_gateway_resource.lambda_resources["reports"]: Refreshing state... [id=wsnfk2]
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["expenditures"]: Refreshing state... [id=6sdj3w]
aws_api_gateway_resource.lambda_resources["projects"]: Refreshing state... [id=chhy2i]
aws_iam_role.ci_apply: Refreshing state... [id=branch-ci-apply]
data.aws_vpc.default: Read complete after 1s [id=vpc-0a9ccfb59c8918ce9]
aws_iam_role.ci_plan: Refreshing state... [id=branch-ci-plan]
aws_iam_role_policy_attachment.lambda_basic: Refreshing state... [id=branch-lambda-role/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]
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_security_group.rds: Refreshing state... [id=sg-0c8a1ff1676a14edb]
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["auth"]: Refreshing state... [id=srhf9j]
aws_api_gateway_resource.lambda_proxy["donors"]: Refreshing state... [id=xkazax]
aws_api_gateway_resource.lambda_proxy["projects"]: Refreshing state... [id=kmwcxq]
aws_api_gateway_method.lambda_methods["users-DELETE"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_method.lambda_methods["expenditures-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-GET"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-GET]
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["users-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_method.lambda_methods["expenditures-POST"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-POST]
aws_api_gateway_method.lambda_methods["projects-POST"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_method.lambda_methods["expenditures-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_method.lambda_methods["auth-GET"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-GET]
aws_api_gateway_method.lambda_methods["donors-GET"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-GET]
aws_api_gateway_method.lambda_methods["users-POST"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_method.lambda_methods["projects-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_method.lambda_methods["reports-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_method.lambda_methods["auth-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_method.lambda_methods["reports-GET"]: Refreshing state... [id=agm-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_method.lambda_methods["projects-GET"]: Refreshing state... [id=agm-2apxzxb0r8-chhy2i-GET]
aws_api_gateway_method.lambda_methods["users-PATCH"]: Refreshing state... [id=agm-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_method.lambda_methods["donors-OPTIONS"]: Refreshing state... [id=agm-2apxzxb0r8-hybur2-OPTIONS]
aws_iam_role_policy.ci_preview: Refreshing state... [id=branch-ci-preview:preview-env]
aws_s3_bucket_public_access_block.frontend: Refreshing state... [id=branch-frontend-489881683177]
aws_vpc_security_group_egress_rule.rds_all: Refreshing state... [id=sgr-099fe0d98d4b3f3b5]
aws_db_instance.branch_rds: Refreshing state... [id=db-AMMYFTORW6XJGRELV7WQZCNHQI]
aws_vpc_security_group_ingress_rule.rds_postgres: Refreshing state... [id=sgr-0594341dc6234d55c]
aws_iam_role_policy_attachment.ci_apply_admin: Refreshing state... [id=branch-ci-apply/arn:aws:iam::aws:policy/AdministratorAccess]
aws_s3_bucket_public_access_block.reports_bucket_public_access: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
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_api_gateway_method.lambda_proxy_any["donors"]: Refreshing state... [id=agm-2apxzxb0r8-xkazax-ANY]
aws_api_gateway_method.lambda_proxy_any["users"]: Refreshing state... [id=agm-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_method.lambda_proxy_any["projects"]: Refreshing state... [id=agm-2apxzxb0r8-kmwcxq-ANY]
aws_api_gateway_method.lambda_proxy_any["expenditures"]: Refreshing state... [id=agm-2apxzxb0r8-14khv0-ANY]
aws_iam_role_policy_attachment.ci_plan_readonly: Refreshing state... [id=branch-ci-plan/arn:aws:iam::aws:policy/ReadOnlyAccess]
aws_iam_role_policy.ci_plan_state_lock: Refreshing state... [id=branch-ci-plan:tfstate-lock]
aws_s3_bucket_policy.reports_bucket_policy: Refreshing state... [id=c4c-branch-generated-reports20251030194253425700000001]
aws_cloudfront_distribution.frontend: Refreshing state... [id=E37FDHRYNZNF4R]
aws_s3_bucket_versioning.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
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_object.lambda_placeholder["donors"]: Refreshing state... [id=branch-lambda-deployments-489881683177/donors/initial.zip]
aws_s3_object.lambda_placeholder["users"]: Refreshing state... [id=branch-lambda-deployments-489881683177/users/initial.zip]
aws_s3_object.lambda_placeholder["reports"]: Refreshing state... [id=branch-lambda-deployments-489881683177/reports/initial.zip]
aws_s3_object.lambda_placeholder["expenditures"]: Refreshing state... [id=branch-lambda-deployments-489881683177/expenditures/initial.zip]
aws_s3_bucket_server_side_encryption_configuration.lambda_deployments: Refreshing state... [id=branch-lambda-deployments-489881683177]
aws_lambda_function.functions["expenditures"]: Refreshing state... [id=branch-expenditures]
aws_lambda_function.functions["users"]: Refreshing state... [id=branch-users]
aws_lambda_function.functions["reports"]: Refreshing state... [id=branch-reports]
aws_lambda_function.functions["donors"]: Refreshing state... [id=branch-donors]
aws_lambda_function.functions["projects"]: Refreshing state... [id=branch-projects]
aws_lambda_function.functions["auth"]: Refreshing state... [id=branch-auth]
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_iam_role_policy.ci_migrate: Refreshing state... [id=branch-ci-migrate:db-migrate]
aws_api_gateway_integration.lambda_integrations["auth-POST"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-POST]
aws_api_gateway_integration.lambda_integrations["auth-GET"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-GET]
aws_api_gateway_integration.lambda_integrations["auth-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-u8unad-OPTIONS]
aws_api_gateway_integration.lambda_integrations["projects-POST"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-POST]
aws_api_gateway_integration.lambda_integrations["users-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-PATCH]
aws_api_gateway_integration.lambda_integrations["reports-GET"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-GET]
aws_api_gateway_integration.lambda_integrations["users-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-OPTIONS]
aws_api_gateway_integration.lambda_integrations["donors-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["reports-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-wsnfk2-OPTIONS]
aws_api_gateway_integration.lambda_integrations["users-POST"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-POST]
aws_api_gateway_integration.lambda_integrations["projects-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-OPTIONS]
aws_api_gateway_integration.lambda_integrations["donors-GET"]: Refreshing state... [id=agi-2apxzxb0r8-hybur2-GET]
aws_api_gateway_integration.lambda_integrations["expenditures-PATCH"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-PATCH]
aws_api_gateway_integration.lambda_integrations["users-DELETE"]: Refreshing state... [id=agi-2apxzxb0r8-0dkbds-DELETE]
aws_api_gateway_integration.lambda_integrations["expenditures-OPTIONS"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-OPTIONS]
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["expenditures-GET"]: Refreshing state... [id=agi-2apxzxb0r8-6sdj3w-GET]
aws_api_gateway_integration.lambda_integrations["projects-GET"]: Refreshing state... [id=agi-2apxzxb0r8-chhy2i-GET]
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["users"]: Refreshing state... [id=agi-2apxzxb0r8-4sjlu3-ANY]
aws_api_gateway_integration.lambda_proxy_integrations["auth"]: Refreshing state... [id=agi-2apxzxb0r8-srhf9j-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]
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["reports"]: 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_lambda_permission.api_gateway_permissions["donors"]: Refreshing state... [id=AllowAPIGatewayInvoke]
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)
    }

  # 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)

      ~ admin_create_user_config {
          ~ allow_admin_create_user_only = false -> true

          + invite_message_template {
              + email_message = "You have been invited to the BRANCH Accounting Platform. Your username is {username} and temporary password is {####}. Log in and set a new password to get started."
              + email_subject = "Your BRANCH Accounting Platform Invitation"
              + sms_message   = "BRANCH invite: username {username}, temp password {####}."
            }
        }

        # (8 unchanged blocks hidden)
    }

  # aws_iam_role_policy.lambda_cognito_admin will be updated in-place
  ~ resource "aws_iam_role_policy" "lambda_cognito_admin" {
        id          = "branch-lambda-role:branch-lambda-cognito-admin"
        name        = "branch-lambda-cognito-admin"
      ~ policy      = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Action   = [
                          + "cognito-idp:AdminCreateUser",
                            "cognito-idp:AdminDeleteUser",
                            # (1 unchanged element hidden)
                        ]
                        # (3 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (2 unchanged attributes hidden)
    }

Plan: 0 to add, 4 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

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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

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.

[Backend] Admin-invite user provisioning via Cognito AdminCreateUser

2 participants