Repository: aws-samples/aws-serverless-saas-workshop Branch: main Commit: 9d72a781db91 Files: 2379 Total size: 4.4 MB Directory structure: gitextract_xdujmuk7/ ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cloud9Setup/ │ ├── .gitignore │ ├── README.md │ ├── increase-disk-size.sh │ ├── pre-requisites-versions-check.sh │ ├── pre-requisites.sh │ └── samconfig.toml ├── LICENSE ├── LICENSE-SAMPLECODE ├── LICENSE-SUMMARY ├── Lab1/ │ ├── client/ │ │ └── Application/ │ │ ├── .browserslistrc │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── README.md │ │ ├── angular.json │ │ ├── karma.conf.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── _nav.ts │ │ │ │ ├── app-routing.module.ts │ │ │ │ ├── app.component.scss │ │ │ │ ├── app.component.ts │ │ │ │ ├── app.module.ts │ │ │ │ ├── models/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── interfaces.ts │ │ │ │ ├── nav/ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ ├── nav.component.scss │ │ │ │ │ └── nav.component.ts │ │ │ │ └── views/ │ │ │ │ ├── dashboard/ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ └── dashboard.module.ts │ │ │ │ ├── orders/ │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ ├── detail/ │ │ │ │ │ │ ├── detail.component.html │ │ │ │ │ │ ├── detail.component.scss │ │ │ │ │ │ └── detail.component.ts │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── order.interface.ts │ │ │ │ │ │ └── orderproduct.interface.ts │ │ │ │ │ ├── orders-routing.module.ts │ │ │ │ │ ├── orders.module.ts │ │ │ │ │ └── orders.service.ts │ │ │ │ └── products/ │ │ │ │ ├── create/ │ │ │ │ │ ├── create.component.html │ │ │ │ │ ├── create.component.scss │ │ │ │ │ └── create.component.ts │ │ │ │ ├── edit/ │ │ │ │ │ ├── edit.component.html │ │ │ │ │ ├── edit.component.scss │ │ │ │ │ └── edit.component.ts │ │ │ │ ├── list/ │ │ │ │ │ ├── list.component.html │ │ │ │ │ ├── list.component.scss │ │ │ │ │ └── list.component.ts │ │ │ │ ├── models/ │ │ │ │ │ └── product.interface.ts │ │ │ │ ├── product.service.ts │ │ │ │ ├── products-routing.module.ts │ │ │ │ └── products.module.ts │ │ │ ├── assets/ │ │ │ │ └── .gitkeep │ │ │ ├── custom-theme.scss │ │ │ ├── environments/ │ │ │ │ ├── environment.prod.ts │ │ │ │ └── environment.ts │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ ├── polyfills.ts │ │ │ ├── styles/ │ │ │ │ ├── _variables.scss │ │ │ │ └── reset.scss │ │ │ ├── styles.scss │ │ │ └── test.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ └── tsconfig.spec.json │ ├── scripts/ │ │ ├── deployment.sh │ │ └── geturl.sh │ └── server/ │ ├── .gitignore │ ├── OrderService/ │ │ ├── order_models.py │ │ ├── order_service.py │ │ ├── order_service_dal.py │ │ └── requirements.txt │ ├── ProductService/ │ │ ├── product_models.py │ │ ├── product_service.py │ │ ├── product_service_dal.py │ │ └── requirements.txt │ ├── README.md │ ├── layers/ │ │ ├── logger.py │ │ ├── requirements.txt │ │ └── utils.py │ ├── samconfig.toml │ └── template.yaml ├── Lab2/ │ ├── client/ │ │ ├── Admin/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ ├── nav/ │ │ │ │ │ │ ├── nav.component.css │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ ├── nav.component.spec.ts │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ └── auth.component.ts │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ ├── tenants/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── tenant.ts │ │ │ │ │ │ ├── tenants-routing.module.ts │ │ │ │ │ │ ├── tenants.module.ts │ │ │ │ │ │ └── tenants.service.ts │ │ │ │ │ └── users/ │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── user.ts │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ ├── users.module.ts │ │ │ │ │ └── users.service.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── aws-exports.ts │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ └── Landing/ │ │ ├── .browserslistrc │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── README.md │ │ ├── angular.json │ │ ├── karma.conf.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── app-routing.module.ts │ │ │ │ ├── app.component.scss │ │ │ │ ├── app.component.ts │ │ │ │ ├── app.module.ts │ │ │ │ └── views/ │ │ │ │ ├── landing/ │ │ │ │ │ ├── landing.component.html │ │ │ │ │ ├── landing.component.scss │ │ │ │ │ └── landing.component.ts │ │ │ │ └── register/ │ │ │ │ ├── register.component.html │ │ │ │ ├── register.component.scss │ │ │ │ └── register.component.ts │ │ │ ├── assets/ │ │ │ │ └── .gitkeep │ │ │ ├── custom-theme.scss │ │ │ ├── environments/ │ │ │ │ ├── environment.prod.ts │ │ │ │ └── environment.ts │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ ├── polyfills.ts │ │ │ ├── styles/ │ │ │ │ ├── _variables.scss │ │ │ │ └── reset.scss │ │ │ ├── styles.scss │ │ │ └── test.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ └── tsconfig.spec.json │ ├── scripts/ │ │ ├── deploy-updates.sh │ │ ├── deployment.sh │ │ └── geturl.sh │ └── server/ │ ├── .gitignore │ ├── OrderService/ │ │ ├── order_models.py │ │ ├── order_service.py │ │ ├── order_service_dal.py │ │ └── requirements.txt │ ├── ProductService/ │ │ ├── product_models.py │ │ ├── product_service.py │ │ ├── product_service_dal.py │ │ └── requirements.txt │ ├── README.md │ ├── Resources/ │ │ ├── requirements.txt │ │ └── shared_service_authorizer.py │ ├── TenantManagementService/ │ │ ├── requirements.txt │ │ ├── tenant-management.py │ │ ├── tenant-registration.py │ │ └── user-management.py │ ├── layers/ │ │ ├── logger.py │ │ ├── requirements.txt │ │ └── utils.py │ ├── nested_templates/ │ │ ├── apigateway.yaml │ │ ├── apigateway_lambdapermissions.yaml │ │ ├── cognito.yaml │ │ ├── lambdafunctions.yaml │ │ ├── tables.yaml │ │ └── userinterface.yaml │ ├── samconfig.toml │ └── template.yaml ├── Lab3/ │ ├── client/ │ │ ├── Admin/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ ├── nav/ │ │ │ │ │ │ ├── nav.component.css │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ ├── nav.component.spec.ts │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ └── auth.component.ts │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ ├── tenants/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── tenant.ts │ │ │ │ │ │ ├── tenants-routing.module.ts │ │ │ │ │ │ ├── tenants.module.ts │ │ │ │ │ │ └── tenants.service.ts │ │ │ │ │ └── users/ │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── user.ts │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ ├── users.module.ts │ │ │ │ │ └── users.service.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── aws-exports.ts │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ ├── Application/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── cypress/ │ │ │ │ ├── README.md │ │ │ │ └── e2e/ │ │ │ │ └── 1-getting-started/ │ │ │ │ ├── basic-access.cy.js │ │ │ │ └── product-testing.cy.js │ │ │ ├── cypress.config.ts │ │ │ ├── cypress.env.json.example │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ ├── cognito.guard.ts │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ ├── nav/ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ ├── nav.component.scss │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── auth-configuration.service.ts │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ ├── auth.component.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ └── config-params.ts │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ ├── error/ │ │ │ │ │ │ ├── 404.component.html │ │ │ │ │ │ ├── 404.component.ts │ │ │ │ │ │ ├── 500.component.html │ │ │ │ │ │ ├── 500.component.ts │ │ │ │ │ │ ├── unauthorized.component.html │ │ │ │ │ │ ├── unauthorized.component.scss │ │ │ │ │ │ └── unauthorized.component.ts │ │ │ │ │ ├── orders/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── detail/ │ │ │ │ │ │ │ ├── detail.component.html │ │ │ │ │ │ │ ├── detail.component.scss │ │ │ │ │ │ │ └── detail.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── order.interface.ts │ │ │ │ │ │ │ └── orderproduct.interface.ts │ │ │ │ │ │ ├── orders-routing.module.ts │ │ │ │ │ │ ├── orders.module.ts │ │ │ │ │ │ └── orders.service.ts │ │ │ │ │ ├── products/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── edit/ │ │ │ │ │ │ │ ├── edit.component.html │ │ │ │ │ │ │ ├── edit.component.scss │ │ │ │ │ │ │ └── edit.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── product.interface.ts │ │ │ │ │ │ ├── product.service.ts │ │ │ │ │ │ ├── products-routing.module.ts │ │ │ │ │ │ └── products.module.ts │ │ │ │ │ └── users/ │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── user.ts │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ ├── users.module.ts │ │ │ │ │ └── users.service.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ ├── Landing/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── landing/ │ │ │ │ │ │ ├── landing.component.html │ │ │ │ │ │ ├── landing.component.scss │ │ │ │ │ │ └── landing.component.ts │ │ │ │ │ └── register/ │ │ │ │ │ ├── register.component.html │ │ │ │ │ ├── register.component.scss │ │ │ │ │ └── register.component.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ └── dummy.txt │ ├── scripts/ │ │ ├── deploy-updates.sh │ │ ├── deployment.sh │ │ └── geturl.sh │ └── server/ │ ├── .gitignore │ ├── OrderService/ │ │ ├── order_models.py │ │ ├── order_service.py │ │ ├── order_service_dal.py │ │ └── requirements.txt │ ├── ProductService/ │ │ ├── product_models.py │ │ ├── product_service.py │ │ ├── product_service_dal.py │ │ └── requirements.txt │ ├── README.md │ ├── Resources/ │ │ ├── requirements.txt │ │ ├── shared_service_authorizer.py │ │ └── tenant_authorizer.py │ ├── TenantManagementService/ │ │ ├── requirements.txt │ │ ├── tenant-management.py │ │ ├── tenant-registration.py │ │ └── user-management.py │ ├── layers/ │ │ ├── auth_manager.py │ │ ├── logger.py │ │ ├── metrics_manager.py │ │ ├── requirements.txt │ │ └── utils.py │ ├── nested_templates/ │ │ ├── apigateway.yaml │ │ ├── apigateway_lambdapermissions.yaml │ │ ├── cognito.yaml │ │ ├── lambdafunctions.yaml │ │ ├── tables.yaml │ │ └── userinterface.yaml │ ├── shared-samconfig.toml │ ├── shared-template.yaml │ ├── tenant-samconfig.toml │ └── tenant-template.yaml ├── Lab4/ │ ├── client/ │ │ ├── Admin/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ ├── nav/ │ │ │ │ │ │ ├── nav.component.css │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ ├── nav.component.spec.ts │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ └── auth.component.ts │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ ├── tenants/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── tenant.ts │ │ │ │ │ │ ├── tenants-routing.module.ts │ │ │ │ │ │ ├── tenants.module.ts │ │ │ │ │ │ └── tenants.service.ts │ │ │ │ │ └── users/ │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── user.ts │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ ├── users.module.ts │ │ │ │ │ └── users.service.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── aws-exports.ts │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ ├── Application/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── cypress/ │ │ │ │ ├── README.md │ │ │ │ └── e2e/ │ │ │ │ └── 1-getting-started/ │ │ │ │ ├── basic-access.cy.js │ │ │ │ └── product-testing.cy.js │ │ │ ├── cypress.config.ts │ │ │ ├── cypress.env.json.example │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ ├── cognito.guard.ts │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ ├── nav/ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ ├── nav.component.scss │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── auth-configuration.service.ts │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ ├── auth.component.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ └── config-params.ts │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ ├── error/ │ │ │ │ │ │ ├── 404.component.html │ │ │ │ │ │ ├── 404.component.ts │ │ │ │ │ │ ├── 500.component.html │ │ │ │ │ │ ├── 500.component.ts │ │ │ │ │ │ ├── unauthorized.component.html │ │ │ │ │ │ ├── unauthorized.component.scss │ │ │ │ │ │ └── unauthorized.component.ts │ │ │ │ │ ├── orders/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── detail/ │ │ │ │ │ │ │ ├── detail.component.html │ │ │ │ │ │ │ ├── detail.component.scss │ │ │ │ │ │ │ └── detail.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── order.interface.ts │ │ │ │ │ │ │ └── orderproduct.interface.ts │ │ │ │ │ │ ├── orders-routing.module.ts │ │ │ │ │ │ ├── orders.module.ts │ │ │ │ │ │ └── orders.service.ts │ │ │ │ │ ├── products/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── edit/ │ │ │ │ │ │ │ ├── edit.component.html │ │ │ │ │ │ │ ├── edit.component.scss │ │ │ │ │ │ │ └── edit.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── product.interface.ts │ │ │ │ │ │ ├── product.service.ts │ │ │ │ │ │ ├── products-routing.module.ts │ │ │ │ │ │ └── products.module.ts │ │ │ │ │ └── users/ │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── user.ts │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ ├── users.module.ts │ │ │ │ │ └── users.service.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ ├── Landing/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── landing/ │ │ │ │ │ │ ├── landing.component.html │ │ │ │ │ │ ├── landing.component.scss │ │ │ │ │ │ └── landing.component.ts │ │ │ │ │ └── register/ │ │ │ │ │ ├── register.component.html │ │ │ │ │ ├── register.component.scss │ │ │ │ │ └── register.component.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ └── dummy.txt │ ├── scripts/ │ │ ├── deployment.sh │ │ └── geturl.sh │ └── server/ │ ├── .gitignore │ ├── OrderService/ │ │ ├── order_models.py │ │ ├── order_service.py │ │ ├── order_service_dal.py │ │ └── requirements.txt │ ├── ProductService/ │ │ ├── product_models.py │ │ ├── product_service.py │ │ ├── product_service_dal.py │ │ └── requirements.txt │ ├── README.md │ ├── Resources/ │ │ ├── requirements.txt │ │ ├── shared_service_authorizer.py │ │ └── tenant_authorizer.py │ ├── TenantManagementService/ │ │ ├── requirements.txt │ │ ├── tenant-management.py │ │ ├── tenant-registration.py │ │ └── user-management.py │ ├── layers/ │ │ ├── auth_manager.py │ │ ├── logger.py │ │ ├── metrics_manager.py │ │ ├── requirements.txt │ │ └── utils.py │ ├── nested_templates/ │ │ ├── apigateway.yaml │ │ ├── apigateway_lambdapermissions.yaml │ │ ├── cognito.yaml │ │ ├── lambdafunctions.yaml │ │ ├── tables.yaml │ │ └── userinterface.yaml │ ├── shared-samconfig.toml │ ├── shared-template.yaml │ ├── tenant-samconfig.toml │ └── tenant-template.yaml ├── Lab5/ │ ├── client/ │ │ ├── Admin/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ ├── nav/ │ │ │ │ │ │ ├── nav.component.css │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ ├── nav.component.spec.ts │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ └── auth.component.ts │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ ├── tenants/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── tenant.ts │ │ │ │ │ │ ├── tenants-routing.module.ts │ │ │ │ │ │ ├── tenants.module.ts │ │ │ │ │ │ └── tenants.service.ts │ │ │ │ │ └── users/ │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── user.ts │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ ├── users.module.ts │ │ │ │ │ └── users.service.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── aws-exports.ts │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ ├── Application/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── cypress/ │ │ │ │ ├── README.md │ │ │ │ └── e2e/ │ │ │ │ └── 1-getting-started/ │ │ │ │ ├── basic-access.cy.js │ │ │ │ └── product-testing.cy.js │ │ │ ├── cypress.config.ts │ │ │ ├── cypress.env.json.example │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ ├── cognito.guard.ts │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ ├── nav/ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ ├── nav.component.scss │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── auth-configuration.service.ts │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ ├── auth.component.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ └── config-params.ts │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ ├── error/ │ │ │ │ │ │ ├── 404.component.html │ │ │ │ │ │ ├── 404.component.ts │ │ │ │ │ │ ├── 500.component.html │ │ │ │ │ │ ├── 500.component.ts │ │ │ │ │ │ ├── unauthorized.component.html │ │ │ │ │ │ ├── unauthorized.component.scss │ │ │ │ │ │ └── unauthorized.component.ts │ │ │ │ │ ├── orders/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── detail/ │ │ │ │ │ │ │ ├── detail.component.html │ │ │ │ │ │ │ ├── detail.component.scss │ │ │ │ │ │ │ └── detail.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── order.interface.ts │ │ │ │ │ │ │ └── orderproduct.interface.ts │ │ │ │ │ │ ├── orders-routing.module.ts │ │ │ │ │ │ ├── orders.module.ts │ │ │ │ │ │ └── orders.service.ts │ │ │ │ │ ├── products/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── edit/ │ │ │ │ │ │ │ ├── edit.component.html │ │ │ │ │ │ │ ├── edit.component.scss │ │ │ │ │ │ │ └── edit.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── product.interface.ts │ │ │ │ │ │ ├── product.service.ts │ │ │ │ │ │ ├── products-routing.module.ts │ │ │ │ │ │ └── products.module.ts │ │ │ │ │ └── users/ │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── user.ts │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ ├── users.module.ts │ │ │ │ │ └── users.service.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ └── Landing/ │ │ ├── .browserslistrc │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── README.md │ │ ├── angular.json │ │ ├── karma.conf.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── app-routing.module.ts │ │ │ │ ├── app.component.scss │ │ │ │ ├── app.component.ts │ │ │ │ ├── app.module.ts │ │ │ │ └── views/ │ │ │ │ ├── landing/ │ │ │ │ │ ├── landing.component.html │ │ │ │ │ ├── landing.component.scss │ │ │ │ │ └── landing.component.ts │ │ │ │ └── register/ │ │ │ │ ├── register.component.html │ │ │ │ ├── register.component.scss │ │ │ │ └── register.component.ts │ │ │ ├── assets/ │ │ │ │ └── .gitkeep │ │ │ ├── custom-theme.scss │ │ │ ├── environments/ │ │ │ │ ├── environment.prod.ts │ │ │ │ └── environment.ts │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ ├── polyfills.ts │ │ │ ├── styles/ │ │ │ │ ├── _variables.scss │ │ │ │ └── reset.scss │ │ │ ├── styles.scss │ │ │ └── test.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ └── tsconfig.spec.json │ ├── scripts/ │ │ ├── deploy-updates.sh │ │ ├── deployment.sh │ │ └── geturl.sh │ └── server/ │ ├── .gitignore │ ├── OrderService/ │ │ ├── order_models.py │ │ ├── order_service.py │ │ ├── order_service_dal.py │ │ └── requirements.txt │ ├── ProductService/ │ │ ├── product_models.py │ │ ├── product_service.py │ │ ├── product_service_dal.py │ │ └── requirements.txt │ ├── README.md │ ├── Resources/ │ │ ├── requirements.txt │ │ ├── shared_service_authorizer.py │ │ └── tenant_authorizer.py │ ├── TenantManagementService/ │ │ ├── requirements.txt │ │ ├── tenant-management.py │ │ ├── tenant-provisioning.py │ │ ├── tenant-registration.py │ │ └── user-management.py │ ├── TenantPipeline/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── bin/ │ │ │ └── pipeline.ts │ │ ├── cdk.json │ │ ├── jest.config.js │ │ ├── lib/ │ │ │ └── serverless-saas-stack.ts │ │ ├── package.json │ │ ├── resources/ │ │ │ └── lambda-deploy-tenant-stack.py │ │ ├── test/ │ │ │ └── pipeline.test.ts │ │ └── tsconfig.json │ ├── custom_resources/ │ │ ├── requirements.txt │ │ ├── update_settings_table.py │ │ ├── update_tenant_apigatewayurl.py │ │ └── update_tenantstackmap_table.py │ ├── layers/ │ │ ├── auth_manager.py │ │ ├── logger.py │ │ ├── metrics_manager.py │ │ ├── requirements.txt │ │ └── utils.py │ ├── nested_templates/ │ │ ├── apigateway.yaml │ │ ├── apigateway_lambdapermissions.yaml │ │ ├── cognito.yaml │ │ ├── custom_resources.yaml │ │ ├── lambdafunctions.yaml │ │ ├── tables.yaml │ │ └── userinterface.yaml │ ├── shared-samconfig.toml │ ├── shared-template.yaml │ ├── tenant-buildspec.yml │ ├── tenant-samconfig.toml │ └── tenant-template.yaml ├── Lab6/ │ ├── client/ │ │ ├── Admin/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ ├── nav/ │ │ │ │ │ │ ├── nav.component.css │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ ├── nav.component.spec.ts │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ └── auth.component.ts │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ ├── tenants/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── tenant.ts │ │ │ │ │ │ ├── tenants-routing.module.ts │ │ │ │ │ │ ├── tenants.module.ts │ │ │ │ │ │ └── tenants.service.ts │ │ │ │ │ └── users/ │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── user.ts │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ ├── users.module.ts │ │ │ │ │ └── users.service.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── aws-exports.ts │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ ├── Application/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── cypress/ │ │ │ │ ├── README.md │ │ │ │ └── e2e/ │ │ │ │ └── 1-getting-started/ │ │ │ │ ├── basic-access.cy.js │ │ │ │ └── product-testing.cy.js │ │ │ ├── cypress.config.ts │ │ │ ├── cypress.env.json.example │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ ├── cognito.guard.ts │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ ├── nav/ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ ├── nav.component.scss │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── auth-configuration.service.ts │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ ├── auth.component.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ └── config-params.ts │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ ├── error/ │ │ │ │ │ │ ├── 404.component.html │ │ │ │ │ │ ├── 404.component.ts │ │ │ │ │ │ ├── 500.component.html │ │ │ │ │ │ ├── 500.component.ts │ │ │ │ │ │ ├── unauthorized.component.html │ │ │ │ │ │ ├── unauthorized.component.scss │ │ │ │ │ │ └── unauthorized.component.ts │ │ │ │ │ ├── orders/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── detail/ │ │ │ │ │ │ │ ├── detail.component.html │ │ │ │ │ │ │ ├── detail.component.scss │ │ │ │ │ │ │ └── detail.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── order.interface.ts │ │ │ │ │ │ │ └── orderproduct.interface.ts │ │ │ │ │ │ ├── orders-routing.module.ts │ │ │ │ │ │ ├── orders.module.ts │ │ │ │ │ │ └── orders.service.ts │ │ │ │ │ ├── products/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── edit/ │ │ │ │ │ │ │ ├── edit.component.html │ │ │ │ │ │ │ ├── edit.component.scss │ │ │ │ │ │ │ └── edit.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── product.interface.ts │ │ │ │ │ │ ├── product.service.ts │ │ │ │ │ │ ├── products-routing.module.ts │ │ │ │ │ │ └── products.module.ts │ │ │ │ │ └── users/ │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── user.ts │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ ├── users.module.ts │ │ │ │ │ └── users.service.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ └── Landing/ │ │ ├── .browserslistrc │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── README.md │ │ ├── angular.json │ │ ├── karma.conf.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── app-routing.module.ts │ │ │ │ ├── app.component.scss │ │ │ │ ├── app.component.ts │ │ │ │ ├── app.module.ts │ │ │ │ └── views/ │ │ │ │ ├── landing/ │ │ │ │ │ ├── landing.component.html │ │ │ │ │ ├── landing.component.scss │ │ │ │ │ └── landing.component.ts │ │ │ │ └── register/ │ │ │ │ ├── register.component.html │ │ │ │ ├── register.component.scss │ │ │ │ └── register.component.ts │ │ │ ├── assets/ │ │ │ │ └── .gitkeep │ │ │ ├── custom-theme.scss │ │ │ ├── environments/ │ │ │ │ ├── environment.prod.ts │ │ │ │ └── environment.ts │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ ├── polyfills.ts │ │ │ ├── styles/ │ │ │ │ ├── _variables.scss │ │ │ │ └── reset.scss │ │ │ ├── styles.scss │ │ │ └── test.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ └── tsconfig.spec.json │ ├── scripts/ │ │ ├── deployment.sh │ │ ├── geturl.sh │ │ └── test-basic-tier-throttling.sh │ └── server/ │ ├── .gitignore │ ├── OrderService/ │ │ ├── order_models.py │ │ ├── order_service.py │ │ ├── order_service_dal.py │ │ └── requirements.txt │ ├── ProductService/ │ │ ├── product_models.py │ │ ├── product_service.py │ │ ├── product_service_dal.py │ │ └── requirements.txt │ ├── README.md │ ├── Resources/ │ │ ├── requirements.txt │ │ ├── shared_service_authorizer.py │ │ └── tenant_authorizer.py │ ├── TenantManagementService/ │ │ ├── requirements.txt │ │ ├── tenant-management.py │ │ ├── tenant-provisioning.py │ │ ├── tenant-registration.py │ │ └── user-management.py │ ├── TenantPipeline/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── bin/ │ │ │ └── pipeline.ts │ │ ├── cdk.json │ │ ├── jest.config.js │ │ ├── lib/ │ │ │ └── serverless-saas-stack.ts │ │ ├── package.json │ │ ├── resources/ │ │ │ └── lambda-deploy-tenant-stack.py │ │ ├── test/ │ │ │ └── pipeline.test.ts │ │ └── tsconfig.json │ ├── custom_resources/ │ │ ├── requirements.txt │ │ ├── update_settings_table.py │ │ ├── update_tenant_apigatewayurl.py │ │ ├── update_tenantstackmap_table.py │ │ └── update_usage_plan.py │ ├── layers/ │ │ ├── auth_manager.py │ │ ├── logger.py │ │ ├── metrics_manager.py │ │ ├── requirements.txt │ │ └── utils.py │ ├── nested_templates/ │ │ ├── apigateway.yaml │ │ ├── apigateway_lambdapermissions.yaml │ │ ├── cognito.yaml │ │ ├── custom_resources.yaml │ │ ├── lambdafunctions.yaml │ │ ├── tables.yaml │ │ └── userinterface.yaml │ ├── shared-samconfig.toml │ ├── shared-template.yaml │ ├── tenant-buildspec.yml │ ├── tenant-samconfig.toml │ └── tenant-template.yaml ├── Lab7/ │ ├── .aws-sam/ │ │ ├── build/ │ │ │ ├── GetDynamoDBUsageAndCostByTenant/ │ │ │ │ ├── requirements.txt │ │ │ │ └── tenant_usage_and_cost.py │ │ │ ├── GetLambdaUsageAndCostByTenant/ │ │ │ │ ├── requirements.txt │ │ │ │ └── tenant_usage_and_cost.py │ │ │ └── template.yaml │ │ └── build.toml │ ├── SampleCUR/ │ │ ├── 20221011_211731_00058_ff5sr_094c8542-2cfd-459d-96e1-10e3bc2ac7a6 │ │ ├── 20221011_211731_00058_ff5sr_1ded6019-91c5-470a-9533-5a63ff447fa4 │ │ ├── 20221011_211731_00058_ff5sr_265b33b8-c229-4a42-9c51-e54380ec03b0 │ │ ├── 20221011_211731_00058_ff5sr_36154ed1-7420-43dc-9bce-e30ff784b7fc │ │ ├── 20221011_211731_00058_ff5sr_4644e2bd-8b84-42df-bce7-35b29a0d3b83 │ │ ├── 20221011_211731_00058_ff5sr_47129e77-847c-420a-acac-3ce962bad5af │ │ ├── 20221011_211731_00058_ff5sr_4f484d50-53e1-40f6-84d3-1495af1981e7 │ │ ├── 20221011_211731_00058_ff5sr_501fe43e-84a3-4665-8ae6-e81ac0a8c024 │ │ ├── 20221011_211731_00058_ff5sr_586d15f5-4ff9-4231-9693-d601f8a96386 │ │ ├── 20221011_211731_00058_ff5sr_5cf01031-5bec-496a-9003-354edff50dcc │ │ ├── 20221011_211731_00058_ff5sr_74023afa-6ec8-4281-a5ce-b4771aac2ae0 │ │ ├── 20221011_211731_00058_ff5sr_7fb5dd51-6c56-41be-94fc-0cb6fc122cfd │ │ ├── 20221011_211731_00058_ff5sr_93372514-9952-4af6-a6f3-396921248c28 │ │ ├── 20221011_211731_00058_ff5sr_952352a7-a59a-4f6c-b359-aa2251d1f6a8 │ │ ├── 20221011_211731_00058_ff5sr_9ab989ba-acce-479c-bc60-c33acffa4c79 │ │ ├── 20221011_211731_00058_ff5sr_9b9fe19e-b145-49d1-9aed-135fec199529 │ │ ├── 20221011_211731_00058_ff5sr_a35b0a43-ebb6-44e1-949b-14d3e0472842 │ │ ├── 20221011_211731_00058_ff5sr_a6fa2ec0-8151-41a2-b6ef-f0f630ff956f │ │ ├── 20221011_211731_00058_ff5sr_aa65c209-65b9-4c95-873a-7c863ea5e0a7 │ │ ├── 20221011_211731_00058_ff5sr_b0cfe354-83a9-403b-9371-477dc4f4e8ef │ │ ├── 20221011_211731_00058_ff5sr_b6f519bf-0afa-480a-b5ab-5d75bec1490d │ │ ├── 20221011_211731_00058_ff5sr_c0ad9ac0-db23-4466-9eb2-d59b532bf15a │ │ ├── 20221011_211731_00058_ff5sr_c3499311-c8db-4272-875a-280a7473d9ec │ │ ├── 20221011_211731_00058_ff5sr_dc8883cc-c8fd-49e7-acd1-8d08867e157d │ │ ├── 20221011_211731_00058_ff5sr_e02f4092-0266-4104-9978-d5bcb8138da3 │ │ ├── 20221011_211731_00058_ff5sr_e7dfb4a0-e8eb-438b-96c6-16023f5a131b │ │ ├── 20221011_211731_00058_ff5sr_ebfd0670-22dd-40cb-a63d-61ebb593982a │ │ ├── 20221011_211731_00058_ff5sr_ecb0667a-7aa5-4fd0-8e85-8c3a5701dd14 │ │ ├── 20221011_211731_00058_ff5sr_f8c1b718-c6dc-4ee0-874d-3d41f6b12732 │ │ └── 20221011_211731_00058_ff5sr_faaa1338-ae7a-4565-ab3c-3dd6b1d63837 │ ├── TenantUsageAndCost/ │ │ ├── requirements.txt │ │ └── tenant_usage_and_cost.py │ ├── deployment.sh │ ├── lambdaoutput.json │ ├── samconfig.toml │ └── template.yaml ├── README.md ├── Solution/ │ ├── Lab1/ │ │ ├── client/ │ │ │ └── Application/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ ├── nav/ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ ├── nav.component.scss │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ ├── orders/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── detail/ │ │ │ │ │ │ │ ├── detail.component.html │ │ │ │ │ │ │ ├── detail.component.scss │ │ │ │ │ │ │ └── detail.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── order.interface.ts │ │ │ │ │ │ │ └── orderproduct.interface.ts │ │ │ │ │ │ ├── orders-routing.module.ts │ │ │ │ │ │ ├── orders.module.ts │ │ │ │ │ │ └── orders.service.ts │ │ │ │ │ └── products/ │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ ├── edit/ │ │ │ │ │ │ ├── edit.component.html │ │ │ │ │ │ ├── edit.component.scss │ │ │ │ │ │ └── edit.component.ts │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── product.interface.ts │ │ │ │ │ ├── product.service.ts │ │ │ │ │ ├── products-routing.module.ts │ │ │ │ │ └── products.module.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ ├── scripts/ │ │ │ ├── deployment.sh │ │ │ └── geturl.sh │ │ └── server/ │ │ ├── .gitignore │ │ ├── OrderService/ │ │ │ ├── order_models.py │ │ │ ├── order_service.py │ │ │ ├── order_service_dal.py │ │ │ └── requirements.txt │ │ ├── ProductService/ │ │ │ ├── product_models.py │ │ │ ├── product_service.py │ │ │ ├── product_service_dal.py │ │ │ └── requirements.txt │ │ ├── README.md │ │ ├── layers/ │ │ │ ├── logger.py │ │ │ ├── requirements.txt │ │ │ └── utils.py │ │ ├── samconfig.toml │ │ └── template.yaml │ ├── Lab2/ │ │ ├── client/ │ │ │ ├── Admin/ │ │ │ │ ├── .browserslistrc │ │ │ │ ├── .editorconfig │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── angular.json │ │ │ │ ├── karma.conf.js │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ │ ├── app.component.scss │ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ │ ├── app.module.ts │ │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ │ ├── nav/ │ │ │ │ │ │ │ ├── nav.component.css │ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ │ ├── nav.component.spec.ts │ │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ │ └── views/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ │ └── auth.component.ts │ │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ │ ├── tenants/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ └── tenant.ts │ │ │ │ │ │ │ ├── tenants-routing.module.ts │ │ │ │ │ │ │ ├── tenants.module.ts │ │ │ │ │ │ │ └── tenants.service.ts │ │ │ │ │ │ └── users/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── user.ts │ │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ │ ├── users.module.ts │ │ │ │ │ │ └── users.service.ts │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── aws-exports.ts │ │ │ │ │ ├── custom-theme.scss │ │ │ │ │ ├── environments/ │ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ │ └── environment.ts │ │ │ │ │ ├── index.html │ │ │ │ │ ├── main.ts │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ ├── styles/ │ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ │ └── reset.scss │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── test.ts │ │ │ │ ├── tsconfig.app.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── tsconfig.spec.json │ │ │ └── Landing/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── landing/ │ │ │ │ │ │ ├── landing.component.html │ │ │ │ │ │ ├── landing.component.scss │ │ │ │ │ │ └── landing.component.ts │ │ │ │ │ └── register/ │ │ │ │ │ ├── register.component.html │ │ │ │ │ ├── register.component.scss │ │ │ │ │ └── register.component.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ ├── scripts/ │ │ │ ├── deploy-updates.sh │ │ │ ├── deployment.sh │ │ │ └── geturl.sh │ │ └── server/ │ │ ├── .gitignore │ │ ├── OrderService/ │ │ │ ├── order_models.py │ │ │ ├── order_service.py │ │ │ ├── order_service_dal.py │ │ │ └── requirements.txt │ │ ├── ProductService/ │ │ │ ├── product_models.py │ │ │ ├── product_service.py │ │ │ ├── product_service_dal.py │ │ │ └── requirements.txt │ │ ├── README.md │ │ ├── Resources/ │ │ │ ├── requirements.txt │ │ │ └── shared_service_authorizer.py │ │ ├── TenantManagementService/ │ │ │ ├── requirements.txt │ │ │ ├── tenant-management.py │ │ │ ├── tenant-registration.py │ │ │ └── user-management.py │ │ ├── layers/ │ │ │ ├── logger.py │ │ │ ├── requirements.txt │ │ │ └── utils.py │ │ ├── nested_templates/ │ │ │ ├── apigateway.yaml │ │ │ ├── apigateway_lambdapermissions.yaml │ │ │ ├── cognito.yaml │ │ │ ├── lambdafunctions.yaml │ │ │ ├── tables.yaml │ │ │ └── userinterface.yaml │ │ ├── samconfig.toml │ │ └── template.yaml │ ├── Lab3/ │ │ ├── client/ │ │ │ ├── Admin/ │ │ │ │ ├── .browserslistrc │ │ │ │ ├── .editorconfig │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── angular.json │ │ │ │ ├── karma.conf.js │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ │ ├── app.component.scss │ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ │ ├── app.module.ts │ │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ │ ├── nav/ │ │ │ │ │ │ │ ├── nav.component.css │ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ │ ├── nav.component.spec.ts │ │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ │ └── views/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ │ └── auth.component.ts │ │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ │ ├── tenants/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ └── tenant.ts │ │ │ │ │ │ │ ├── tenants-routing.module.ts │ │ │ │ │ │ │ ├── tenants.module.ts │ │ │ │ │ │ │ └── tenants.service.ts │ │ │ │ │ │ └── users/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── user.ts │ │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ │ ├── users.module.ts │ │ │ │ │ │ └── users.service.ts │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── aws-exports.ts │ │ │ │ │ ├── custom-theme.scss │ │ │ │ │ ├── environments/ │ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ │ └── environment.ts │ │ │ │ │ ├── index.html │ │ │ │ │ ├── main.ts │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ ├── styles/ │ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ │ └── reset.scss │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── test.ts │ │ │ │ ├── tsconfig.app.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── tsconfig.spec.json │ │ │ ├── Application/ │ │ │ │ ├── .browserslistrc │ │ │ │ ├── .editorconfig │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── angular.json │ │ │ │ ├── cypress/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── e2e/ │ │ │ │ │ └── 1-getting-started/ │ │ │ │ │ ├── basic-access.cy.js │ │ │ │ │ └── product-testing.cy.js │ │ │ │ ├── cypress.config.ts │ │ │ │ ├── cypress.env.json.example │ │ │ │ ├── karma.conf.js │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ │ ├── app.component.scss │ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ │ ├── app.module.ts │ │ │ │ │ │ ├── cognito.guard.ts │ │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ │ ├── nav/ │ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ │ ├── nav.component.scss │ │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ │ └── views/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── auth-configuration.service.ts │ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ │ ├── auth.component.ts │ │ │ │ │ │ │ └── models/ │ │ │ │ │ │ │ └── config-params.ts │ │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ │ ├── error/ │ │ │ │ │ │ │ ├── 404.component.html │ │ │ │ │ │ │ ├── 404.component.ts │ │ │ │ │ │ │ ├── 500.component.html │ │ │ │ │ │ │ ├── 500.component.ts │ │ │ │ │ │ │ ├── unauthorized.component.html │ │ │ │ │ │ │ ├── unauthorized.component.scss │ │ │ │ │ │ │ └── unauthorized.component.ts │ │ │ │ │ │ ├── orders/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── detail/ │ │ │ │ │ │ │ │ ├── detail.component.html │ │ │ │ │ │ │ │ ├── detail.component.scss │ │ │ │ │ │ │ │ └── detail.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ ├── order.interface.ts │ │ │ │ │ │ │ │ └── orderproduct.interface.ts │ │ │ │ │ │ │ ├── orders-routing.module.ts │ │ │ │ │ │ │ ├── orders.module.ts │ │ │ │ │ │ │ └── orders.service.ts │ │ │ │ │ │ ├── products/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── edit/ │ │ │ │ │ │ │ │ ├── edit.component.html │ │ │ │ │ │ │ │ ├── edit.component.scss │ │ │ │ │ │ │ │ └── edit.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ └── product.interface.ts │ │ │ │ │ │ │ ├── product.service.ts │ │ │ │ │ │ │ ├── products-routing.module.ts │ │ │ │ │ │ │ └── products.module.ts │ │ │ │ │ │ └── users/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── user.ts │ │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ │ ├── users.module.ts │ │ │ │ │ │ └── users.service.ts │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── custom-theme.scss │ │ │ │ │ ├── environments/ │ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ │ └── environment.ts │ │ │ │ │ ├── index.html │ │ │ │ │ ├── main.ts │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ ├── styles/ │ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ │ └── reset.scss │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── test.ts │ │ │ │ ├── tsconfig.app.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── tsconfig.spec.json │ │ │ ├── Landing/ │ │ │ │ ├── .browserslistrc │ │ │ │ ├── .editorconfig │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── angular.json │ │ │ │ ├── karma.conf.js │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ │ ├── app.component.scss │ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ │ ├── app.module.ts │ │ │ │ │ │ └── views/ │ │ │ │ │ │ ├── landing/ │ │ │ │ │ │ │ ├── landing.component.html │ │ │ │ │ │ │ ├── landing.component.scss │ │ │ │ │ │ │ └── landing.component.ts │ │ │ │ │ │ └── register/ │ │ │ │ │ │ ├── register.component.html │ │ │ │ │ │ ├── register.component.scss │ │ │ │ │ │ └── register.component.ts │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── custom-theme.scss │ │ │ │ │ ├── environments/ │ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ │ └── environment.ts │ │ │ │ │ ├── index.html │ │ │ │ │ ├── main.ts │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ ├── styles/ │ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ │ └── reset.scss │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── test.ts │ │ │ │ ├── tsconfig.app.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── tsconfig.spec.json │ │ │ └── dummy.txt │ │ ├── scripts/ │ │ │ ├── deploy-updates.sh │ │ │ ├── deployment.sh │ │ │ └── geturl.sh │ │ └── server/ │ │ ├── .gitignore │ │ ├── OrderService/ │ │ │ ├── order_models.py │ │ │ ├── order_service.py │ │ │ ├── order_service_dal.py │ │ │ └── requirements.txt │ │ ├── ProductService/ │ │ │ ├── product_models.py │ │ │ ├── product_service.py │ │ │ ├── product_service_dal.py │ │ │ └── requirements.txt │ │ ├── README.md │ │ ├── Resources/ │ │ │ ├── requirements.txt │ │ │ ├── shared_service_authorizer.py │ │ │ └── tenant_authorizer.py │ │ ├── TenantManagementService/ │ │ │ ├── requirements.txt │ │ │ ├── tenant-management.py │ │ │ ├── tenant-registration.py │ │ │ └── user-management.py │ │ ├── layers/ │ │ │ ├── auth_manager.py │ │ │ ├── logger.py │ │ │ ├── metrics_manager.py │ │ │ ├── requirements.txt │ │ │ └── utils.py │ │ ├── nested_templates/ │ │ │ ├── apigateway.yaml │ │ │ ├── apigateway_lambdapermissions.yaml │ │ │ ├── cognito.yaml │ │ │ ├── lambdafunctions.yaml │ │ │ ├── tables.yaml │ │ │ └── userinterface.yaml │ │ ├── shared-samconfig.toml │ │ ├── shared-template.yaml │ │ ├── tenant-samconfig.toml │ │ └── tenant-template.yaml │ ├── Lab4/ │ │ ├── client/ │ │ │ ├── Admin/ │ │ │ │ ├── .browserslistrc │ │ │ │ ├── .editorconfig │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── angular.json │ │ │ │ ├── karma.conf.js │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ │ ├── app.component.scss │ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ │ ├── app.module.ts │ │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ │ ├── nav/ │ │ │ │ │ │ │ ├── nav.component.css │ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ │ ├── nav.component.spec.ts │ │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ │ └── views/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ │ └── auth.component.ts │ │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ │ ├── tenants/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ └── tenant.ts │ │ │ │ │ │ │ ├── tenants-routing.module.ts │ │ │ │ │ │ │ ├── tenants.module.ts │ │ │ │ │ │ │ └── tenants.service.ts │ │ │ │ │ │ └── users/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── user.ts │ │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ │ ├── users.module.ts │ │ │ │ │ │ └── users.service.ts │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── aws-exports.ts │ │ │ │ │ ├── custom-theme.scss │ │ │ │ │ ├── environments/ │ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ │ └── environment.ts │ │ │ │ │ ├── index.html │ │ │ │ │ ├── main.ts │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ ├── styles/ │ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ │ └── reset.scss │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── test.ts │ │ │ │ ├── tsconfig.app.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── tsconfig.spec.json │ │ │ ├── Application/ │ │ │ │ ├── .browserslistrc │ │ │ │ ├── .editorconfig │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── angular.json │ │ │ │ ├── cypress/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── e2e/ │ │ │ │ │ └── 1-getting-started/ │ │ │ │ │ ├── basic-access.cy.js │ │ │ │ │ └── product-testing.cy.js │ │ │ │ ├── cypress.config.ts │ │ │ │ ├── cypress.env.json.example │ │ │ │ ├── karma.conf.js │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ │ ├── app.component.scss │ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ │ ├── app.module.ts │ │ │ │ │ │ ├── cognito.guard.ts │ │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ │ ├── nav/ │ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ │ ├── nav.component.scss │ │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ │ └── views/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── auth-configuration.service.ts │ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ │ ├── auth.component.ts │ │ │ │ │ │ │ └── models/ │ │ │ │ │ │ │ └── config-params.ts │ │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ │ ├── error/ │ │ │ │ │ │ │ ├── 404.component.html │ │ │ │ │ │ │ ├── 404.component.ts │ │ │ │ │ │ │ ├── 500.component.html │ │ │ │ │ │ │ ├── 500.component.ts │ │ │ │ │ │ │ ├── unauthorized.component.html │ │ │ │ │ │ │ ├── unauthorized.component.scss │ │ │ │ │ │ │ └── unauthorized.component.ts │ │ │ │ │ │ ├── orders/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── detail/ │ │ │ │ │ │ │ │ ├── detail.component.html │ │ │ │ │ │ │ │ ├── detail.component.scss │ │ │ │ │ │ │ │ └── detail.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ ├── order.interface.ts │ │ │ │ │ │ │ │ └── orderproduct.interface.ts │ │ │ │ │ │ │ ├── orders-routing.module.ts │ │ │ │ │ │ │ ├── orders.module.ts │ │ │ │ │ │ │ └── orders.service.ts │ │ │ │ │ │ ├── products/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── edit/ │ │ │ │ │ │ │ │ ├── edit.component.html │ │ │ │ │ │ │ │ ├── edit.component.scss │ │ │ │ │ │ │ │ └── edit.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ └── product.interface.ts │ │ │ │ │ │ │ ├── product.service.ts │ │ │ │ │ │ │ ├── products-routing.module.ts │ │ │ │ │ │ │ └── products.module.ts │ │ │ │ │ │ └── users/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── user.ts │ │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ │ ├── users.module.ts │ │ │ │ │ │ └── users.service.ts │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── custom-theme.scss │ │ │ │ │ ├── environments/ │ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ │ └── environment.ts │ │ │ │ │ ├── index.html │ │ │ │ │ ├── main.ts │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ ├── styles/ │ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ │ └── reset.scss │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── test.ts │ │ │ │ ├── tsconfig.app.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── tsconfig.spec.json │ │ │ ├── Landing/ │ │ │ │ ├── .browserslistrc │ │ │ │ ├── .editorconfig │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── angular.json │ │ │ │ ├── karma.conf.js │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ │ ├── app.component.scss │ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ │ ├── app.module.ts │ │ │ │ │ │ └── views/ │ │ │ │ │ │ ├── landing/ │ │ │ │ │ │ │ ├── landing.component.html │ │ │ │ │ │ │ ├── landing.component.scss │ │ │ │ │ │ │ └── landing.component.ts │ │ │ │ │ │ └── register/ │ │ │ │ │ │ ├── register.component.html │ │ │ │ │ │ ├── register.component.scss │ │ │ │ │ │ └── register.component.ts │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── custom-theme.scss │ │ │ │ │ ├── environments/ │ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ │ └── environment.ts │ │ │ │ │ ├── index.html │ │ │ │ │ ├── main.ts │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ ├── styles/ │ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ │ └── reset.scss │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── test.ts │ │ │ │ ├── tsconfig.app.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── tsconfig.spec.json │ │ │ └── dummy.txt │ │ ├── scripts/ │ │ │ ├── deployment.sh │ │ │ └── geturl.sh │ │ └── server/ │ │ ├── .gitignore │ │ ├── OrderService/ │ │ │ ├── order_models.py │ │ │ ├── order_service.py │ │ │ ├── order_service_dal.py │ │ │ └── requirements.txt │ │ ├── ProductService/ │ │ │ ├── product_models.py │ │ │ ├── product_service.py │ │ │ ├── product_service_dal.py │ │ │ └── requirements.txt │ │ ├── README.md │ │ ├── Resources/ │ │ │ ├── requirements.txt │ │ │ ├── shared_service_authorizer.py │ │ │ └── tenant_authorizer.py │ │ ├── TenantManagementService/ │ │ │ ├── requirements.txt │ │ │ ├── tenant-management.py │ │ │ ├── tenant-registration.py │ │ │ └── user-management.py │ │ ├── layers/ │ │ │ ├── auth_manager.py │ │ │ ├── logger.py │ │ │ ├── metrics_manager.py │ │ │ ├── requirements.txt │ │ │ └── utils.py │ │ ├── nested_templates/ │ │ │ ├── apigateway.yaml │ │ │ ├── apigateway_lambdapermissions.yaml │ │ │ ├── cognito.yaml │ │ │ ├── lambdafunctions.yaml │ │ │ ├── tables.yaml │ │ │ └── userinterface.yaml │ │ ├── shared-samconfig.toml │ │ ├── shared-template.yaml │ │ ├── tenant-samconfig.toml │ │ └── tenant-template.yaml │ ├── Lab5/ │ │ ├── client/ │ │ │ ├── Admin/ │ │ │ │ ├── .browserslistrc │ │ │ │ ├── .editorconfig │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── angular.json │ │ │ │ ├── karma.conf.js │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ │ ├── app.component.scss │ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ │ ├── app.module.ts │ │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ │ ├── nav/ │ │ │ │ │ │ │ ├── nav.component.css │ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ │ ├── nav.component.spec.ts │ │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ │ └── views/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ │ └── auth.component.ts │ │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ │ ├── tenants/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ └── tenant.ts │ │ │ │ │ │ │ ├── tenants-routing.module.ts │ │ │ │ │ │ │ ├── tenants.module.ts │ │ │ │ │ │ │ └── tenants.service.ts │ │ │ │ │ │ └── users/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── user.ts │ │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ │ ├── users.module.ts │ │ │ │ │ │ └── users.service.ts │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── aws-exports.ts │ │ │ │ │ ├── custom-theme.scss │ │ │ │ │ ├── environments/ │ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ │ └── environment.ts │ │ │ │ │ ├── index.html │ │ │ │ │ ├── main.ts │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ ├── styles/ │ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ │ └── reset.scss │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── test.ts │ │ │ │ ├── tsconfig.app.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── tsconfig.spec.json │ │ │ ├── Application/ │ │ │ │ ├── .browserslistrc │ │ │ │ ├── .editorconfig │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── angular.json │ │ │ │ ├── cypress/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── e2e/ │ │ │ │ │ └── 1-getting-started/ │ │ │ │ │ ├── basic-access.cy.js │ │ │ │ │ └── product-testing.cy.js │ │ │ │ ├── cypress.config.ts │ │ │ │ ├── cypress.env.json.example │ │ │ │ ├── karma.conf.js │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ │ ├── app.component.scss │ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ │ ├── app.module.ts │ │ │ │ │ │ ├── cognito.guard.ts │ │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ │ ├── nav/ │ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ │ ├── nav.component.scss │ │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ │ └── views/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── auth-configuration.service.ts │ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ │ ├── auth.component.ts │ │ │ │ │ │ │ └── models/ │ │ │ │ │ │ │ └── config-params.ts │ │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ │ ├── error/ │ │ │ │ │ │ │ ├── 404.component.html │ │ │ │ │ │ │ ├── 404.component.ts │ │ │ │ │ │ │ ├── 500.component.html │ │ │ │ │ │ │ ├── 500.component.ts │ │ │ │ │ │ │ ├── unauthorized.component.html │ │ │ │ │ │ │ ├── unauthorized.component.scss │ │ │ │ │ │ │ └── unauthorized.component.ts │ │ │ │ │ │ ├── orders/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── detail/ │ │ │ │ │ │ │ │ ├── detail.component.html │ │ │ │ │ │ │ │ ├── detail.component.scss │ │ │ │ │ │ │ │ └── detail.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ ├── order.interface.ts │ │ │ │ │ │ │ │ └── orderproduct.interface.ts │ │ │ │ │ │ │ ├── orders-routing.module.ts │ │ │ │ │ │ │ ├── orders.module.ts │ │ │ │ │ │ │ └── orders.service.ts │ │ │ │ │ │ ├── products/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── edit/ │ │ │ │ │ │ │ │ ├── edit.component.html │ │ │ │ │ │ │ │ ├── edit.component.scss │ │ │ │ │ │ │ │ └── edit.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ └── product.interface.ts │ │ │ │ │ │ │ ├── product.service.ts │ │ │ │ │ │ │ ├── products-routing.module.ts │ │ │ │ │ │ │ └── products.module.ts │ │ │ │ │ │ └── users/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── user.ts │ │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ │ ├── users.module.ts │ │ │ │ │ │ └── users.service.ts │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── custom-theme.scss │ │ │ │ │ ├── environments/ │ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ │ └── environment.ts │ │ │ │ │ ├── index.html │ │ │ │ │ ├── main.ts │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ ├── styles/ │ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ │ └── reset.scss │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── test.ts │ │ │ │ ├── tsconfig.app.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── tsconfig.spec.json │ │ │ └── Landing/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── landing/ │ │ │ │ │ │ ├── landing.component.html │ │ │ │ │ │ ├── landing.component.scss │ │ │ │ │ │ └── landing.component.ts │ │ │ │ │ └── register/ │ │ │ │ │ ├── register.component.html │ │ │ │ │ ├── register.component.scss │ │ │ │ │ └── register.component.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ ├── scripts/ │ │ │ ├── deploy-updates.sh │ │ │ ├── deployment.sh │ │ │ └── geturl.sh │ │ └── server/ │ │ ├── .gitignore │ │ ├── OrderService/ │ │ │ ├── order_models.py │ │ │ ├── order_service.py │ │ │ ├── order_service_dal.py │ │ │ └── requirements.txt │ │ ├── ProductService/ │ │ │ ├── product_models.py │ │ │ ├── product_service.py │ │ │ ├── product_service_dal.py │ │ │ └── requirements.txt │ │ ├── README.md │ │ ├── Resources/ │ │ │ ├── requirements.txt │ │ │ ├── shared_service_authorizer.py │ │ │ └── tenant_authorizer.py │ │ ├── TenantManagementService/ │ │ │ ├── events/ │ │ │ │ ├── env.json │ │ │ │ ├── tenant-registration.json │ │ │ │ ├── update_users_apikey_by_tenant.json │ │ │ │ └── user-management.json │ │ │ ├── requirements.txt │ │ │ ├── tenant-management.py │ │ │ ├── tenant-provisioning.py │ │ │ ├── tenant-registration.py │ │ │ └── user-management.py │ │ ├── TenantPipeline/ │ │ │ ├── .gitignore │ │ │ ├── .npmignore │ │ │ ├── README.md │ │ │ ├── bin/ │ │ │ │ └── pipeline.ts │ │ │ ├── cdk.json │ │ │ ├── jest.config.js │ │ │ ├── lib/ │ │ │ │ └── serverless-saas-stack.ts │ │ │ ├── package.json │ │ │ ├── resources/ │ │ │ │ └── lambda-deploy-tenant-stack.py │ │ │ ├── test/ │ │ │ │ └── pipeline.test.ts │ │ │ └── tsconfig.json │ │ ├── custom_resources/ │ │ │ ├── requirements.txt │ │ │ ├── update_settings_table.py │ │ │ ├── update_tenant_apigatewayurl.py │ │ │ └── update_tenantstackmap_table.py │ │ ├── layers/ │ │ │ ├── auth_manager.py │ │ │ ├── logger.py │ │ │ ├── metrics_manager.py │ │ │ ├── requirements.txt │ │ │ └── utils.py │ │ ├── nested_templates/ │ │ │ ├── apigateway.yaml │ │ │ ├── apigateway_lambdapermissions.yaml │ │ │ ├── cognito.yaml │ │ │ ├── custom_resources.yaml │ │ │ ├── lambdafunctions.yaml │ │ │ ├── tables.yaml │ │ │ └── userinterface.yaml │ │ ├── shared-samconfig.toml │ │ ├── shared-template.yaml │ │ ├── tenant-buildspec.yml │ │ ├── tenant-samconfig.toml │ │ └── tenant-template.yaml │ ├── Lab6/ │ │ ├── client/ │ │ │ ├── Admin/ │ │ │ │ ├── .browserslistrc │ │ │ │ ├── .editorconfig │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── angular.json │ │ │ │ ├── karma.conf.js │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ │ ├── app.component.scss │ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ │ ├── app.module.ts │ │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ │ ├── nav/ │ │ │ │ │ │ │ ├── nav.component.css │ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ │ ├── nav.component.spec.ts │ │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ │ └── views/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ │ └── auth.component.ts │ │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ │ ├── tenants/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ └── tenant.ts │ │ │ │ │ │ │ ├── tenants-routing.module.ts │ │ │ │ │ │ │ ├── tenants.module.ts │ │ │ │ │ │ │ └── tenants.service.ts │ │ │ │ │ │ └── users/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── user.ts │ │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ │ ├── users.module.ts │ │ │ │ │ │ └── users.service.ts │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── aws-exports.ts │ │ │ │ │ ├── custom-theme.scss │ │ │ │ │ ├── environments/ │ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ │ └── environment.ts │ │ │ │ │ ├── index.html │ │ │ │ │ ├── main.ts │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ ├── styles/ │ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ │ └── reset.scss │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── test.ts │ │ │ │ ├── tsconfig.app.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── tsconfig.spec.json │ │ │ ├── Application/ │ │ │ │ ├── .browserslistrc │ │ │ │ ├── .editorconfig │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── angular.json │ │ │ │ ├── cypress/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── e2e/ │ │ │ │ │ └── 1-getting-started/ │ │ │ │ │ ├── basic-access.cy.js │ │ │ │ │ └── product-testing.cy.js │ │ │ │ ├── cypress.config.ts │ │ │ │ ├── cypress.env.json.example │ │ │ │ ├── karma.conf.js │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── _nav.ts │ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ │ ├── app.component.scss │ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ │ ├── app.module.ts │ │ │ │ │ │ ├── cognito.guard.ts │ │ │ │ │ │ ├── interceptors/ │ │ │ │ │ │ │ ├── auth.interceptor.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── interfaces.ts │ │ │ │ │ │ ├── nav/ │ │ │ │ │ │ │ ├── nav.component.html │ │ │ │ │ │ │ ├── nav.component.scss │ │ │ │ │ │ │ └── nav.component.ts │ │ │ │ │ │ └── views/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── auth-configuration.service.ts │ │ │ │ │ │ │ ├── auth.component.html │ │ │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ │ │ ├── auth.component.ts │ │ │ │ │ │ │ └── models/ │ │ │ │ │ │ │ └── config-params.ts │ │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ │ │ │ ├── dashboard.component.html │ │ │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ │ │ └── dashboard.module.ts │ │ │ │ │ │ ├── error/ │ │ │ │ │ │ │ ├── 404.component.html │ │ │ │ │ │ │ ├── 404.component.ts │ │ │ │ │ │ │ ├── 500.component.html │ │ │ │ │ │ │ ├── 500.component.ts │ │ │ │ │ │ │ ├── unauthorized.component.html │ │ │ │ │ │ │ ├── unauthorized.component.scss │ │ │ │ │ │ │ └── unauthorized.component.ts │ │ │ │ │ │ ├── orders/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── detail/ │ │ │ │ │ │ │ │ ├── detail.component.html │ │ │ │ │ │ │ │ ├── detail.component.scss │ │ │ │ │ │ │ │ └── detail.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ ├── order.interface.ts │ │ │ │ │ │ │ │ └── orderproduct.interface.ts │ │ │ │ │ │ │ ├── orders-routing.module.ts │ │ │ │ │ │ │ ├── orders.module.ts │ │ │ │ │ │ │ └── orders.service.ts │ │ │ │ │ │ ├── products/ │ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ │ ├── edit/ │ │ │ │ │ │ │ │ ├── edit.component.html │ │ │ │ │ │ │ │ ├── edit.component.scss │ │ │ │ │ │ │ │ └── edit.component.ts │ │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ │ └── product.interface.ts │ │ │ │ │ │ │ ├── product.service.ts │ │ │ │ │ │ │ ├── products-routing.module.ts │ │ │ │ │ │ │ └── products.module.ts │ │ │ │ │ │ └── users/ │ │ │ │ │ │ ├── create/ │ │ │ │ │ │ │ ├── create.component.html │ │ │ │ │ │ │ ├── create.component.scss │ │ │ │ │ │ │ └── create.component.ts │ │ │ │ │ │ ├── list/ │ │ │ │ │ │ │ ├── list.component.html │ │ │ │ │ │ │ ├── list.component.scss │ │ │ │ │ │ │ └── list.component.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ └── user.ts │ │ │ │ │ │ ├── users-routing.module.ts │ │ │ │ │ │ ├── users.module.ts │ │ │ │ │ │ └── users.service.ts │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── custom-theme.scss │ │ │ │ │ ├── environments/ │ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ │ └── environment.ts │ │ │ │ │ ├── index.html │ │ │ │ │ ├── main.ts │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ ├── styles/ │ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ │ └── reset.scss │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── test.ts │ │ │ │ ├── tsconfig.app.json │ │ │ │ ├── tsconfig.json │ │ │ │ └── tsconfig.spec.json │ │ │ └── Landing/ │ │ │ ├── .browserslistrc │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── karma.conf.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app-routing.module.ts │ │ │ │ │ ├── app.component.scss │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.module.ts │ │ │ │ │ └── views/ │ │ │ │ │ ├── landing/ │ │ │ │ │ │ ├── landing.component.html │ │ │ │ │ │ ├── landing.component.scss │ │ │ │ │ │ └── landing.component.ts │ │ │ │ │ └── register/ │ │ │ │ │ ├── register.component.html │ │ │ │ │ ├── register.component.scss │ │ │ │ │ └── register.component.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── custom-theme.scss │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.prod.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── _variables.scss │ │ │ │ │ └── reset.scss │ │ │ │ ├── styles.scss │ │ │ │ └── test.ts │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ ├── scripts/ │ │ │ ├── deployment.sh │ │ │ ├── geturl.sh │ │ │ └── test-basic-tier-throttling.sh │ │ └── server/ │ │ ├── .gitignore │ │ ├── OrderService/ │ │ │ ├── order_models.py │ │ │ ├── order_service.py │ │ │ ├── order_service_dal.py │ │ │ └── requirements.txt │ │ ├── ProductService/ │ │ │ ├── product_models.py │ │ │ ├── product_service.py │ │ │ ├── product_service_dal.py │ │ │ └── requirements.txt │ │ ├── README.md │ │ ├── Resources/ │ │ │ ├── requirements.txt │ │ │ ├── shared_service_authorizer.py │ │ │ └── tenant_authorizer.py │ │ ├── TenantManagementService/ │ │ │ ├── events/ │ │ │ │ ├── env.json │ │ │ │ ├── tenant-registration.json │ │ │ │ ├── update_users_apikey_by_tenant.json │ │ │ │ └── user-management.json │ │ │ ├── requirements.txt │ │ │ ├── tenant-management.py │ │ │ ├── tenant-provisioning.py │ │ │ ├── tenant-registration.py │ │ │ └── user-management.py │ │ ├── TenantPipeline/ │ │ │ ├── .gitignore │ │ │ ├── .npmignore │ │ │ ├── README.md │ │ │ ├── bin/ │ │ │ │ └── pipeline.ts │ │ │ ├── cdk.json │ │ │ ├── jest.config.js │ │ │ ├── lib/ │ │ │ │ └── serverless-saas-stack.ts │ │ │ ├── package.json │ │ │ ├── resources/ │ │ │ │ └── lambda-deploy-tenant-stack.py │ │ │ ├── test/ │ │ │ │ └── pipeline.test.ts │ │ │ └── tsconfig.json │ │ ├── custom_resources/ │ │ │ ├── requirements.txt │ │ │ ├── update_settings_table.py │ │ │ ├── update_tenant_apigatewayurl.py │ │ │ ├── update_tenantstackmap_table.py │ │ │ └── update_usage_plan.py │ │ ├── layers/ │ │ │ ├── auth_manager.py │ │ │ ├── logger.py │ │ │ ├── metrics_manager.py │ │ │ ├── requirements.txt │ │ │ └── utils.py │ │ ├── nested_templates/ │ │ │ ├── apigateway.yaml │ │ │ ├── apigateway_lambdapermissions.yaml │ │ │ ├── cognito.yaml │ │ │ ├── custom_resources.yaml │ │ │ ├── lambdafunctions.yaml │ │ │ ├── tables.yaml │ │ │ └── userinterface.yaml │ │ ├── shared-samconfig.toml │ │ ├── shared-template.yaml │ │ ├── tenant-buildspec.yml │ │ ├── tenant-samconfig.toml │ │ └── tenant-template.yaml │ └── Lab7/ │ ├── .aws-sam/ │ │ ├── build/ │ │ │ ├── GetDynamoDBUsageAndCostByTenant/ │ │ │ │ ├── requirements.txt │ │ │ │ └── tenant_usage_and_cost.py │ │ │ ├── GetLambdaUsageAndCostByTenant/ │ │ │ │ ├── requirements.txt │ │ │ │ └── tenant_usage_and_cost.py │ │ │ └── template.yaml │ │ └── build.toml │ ├── SampleCUR/ │ │ ├── 20221011_211731_00058_ff5sr_094c8542-2cfd-459d-96e1-10e3bc2ac7a6 │ │ ├── 20221011_211731_00058_ff5sr_1ded6019-91c5-470a-9533-5a63ff447fa4 │ │ ├── 20221011_211731_00058_ff5sr_265b33b8-c229-4a42-9c51-e54380ec03b0 │ │ ├── 20221011_211731_00058_ff5sr_36154ed1-7420-43dc-9bce-e30ff784b7fc │ │ ├── 20221011_211731_00058_ff5sr_4644e2bd-8b84-42df-bce7-35b29a0d3b83 │ │ ├── 20221011_211731_00058_ff5sr_47129e77-847c-420a-acac-3ce962bad5af │ │ ├── 20221011_211731_00058_ff5sr_4f484d50-53e1-40f6-84d3-1495af1981e7 │ │ ├── 20221011_211731_00058_ff5sr_501fe43e-84a3-4665-8ae6-e81ac0a8c024 │ │ ├── 20221011_211731_00058_ff5sr_586d15f5-4ff9-4231-9693-d601f8a96386 │ │ ├── 20221011_211731_00058_ff5sr_5cf01031-5bec-496a-9003-354edff50dcc │ │ ├── 20221011_211731_00058_ff5sr_74023afa-6ec8-4281-a5ce-b4771aac2ae0 │ │ ├── 20221011_211731_00058_ff5sr_7fb5dd51-6c56-41be-94fc-0cb6fc122cfd │ │ ├── 20221011_211731_00058_ff5sr_93372514-9952-4af6-a6f3-396921248c28 │ │ ├── 20221011_211731_00058_ff5sr_952352a7-a59a-4f6c-b359-aa2251d1f6a8 │ │ ├── 20221011_211731_00058_ff5sr_9ab989ba-acce-479c-bc60-c33acffa4c79 │ │ ├── 20221011_211731_00058_ff5sr_9b9fe19e-b145-49d1-9aed-135fec199529 │ │ ├── 20221011_211731_00058_ff5sr_a35b0a43-ebb6-44e1-949b-14d3e0472842 │ │ ├── 20221011_211731_00058_ff5sr_a6fa2ec0-8151-41a2-b6ef-f0f630ff956f │ │ ├── 20221011_211731_00058_ff5sr_aa65c209-65b9-4c95-873a-7c863ea5e0a7 │ │ ├── 20221011_211731_00058_ff5sr_b0cfe354-83a9-403b-9371-477dc4f4e8ef │ │ ├── 20221011_211731_00058_ff5sr_b6f519bf-0afa-480a-b5ab-5d75bec1490d │ │ ├── 20221011_211731_00058_ff5sr_c0ad9ac0-db23-4466-9eb2-d59b532bf15a │ │ ├── 20221011_211731_00058_ff5sr_c3499311-c8db-4272-875a-280a7473d9ec │ │ ├── 20221011_211731_00058_ff5sr_dc8883cc-c8fd-49e7-acd1-8d08867e157d │ │ ├── 20221011_211731_00058_ff5sr_e02f4092-0266-4104-9978-d5bcb8138da3 │ │ ├── 20221011_211731_00058_ff5sr_e7dfb4a0-e8eb-438b-96c6-16023f5a131b │ │ ├── 20221011_211731_00058_ff5sr_ebfd0670-22dd-40cb-a63d-61ebb593982a │ │ ├── 20221011_211731_00058_ff5sr_ecb0667a-7aa5-4fd0-8e85-8c3a5701dd14 │ │ ├── 20221011_211731_00058_ff5sr_f8c1b718-c6dc-4ee0-874d-3d41f6b12732 │ │ └── 20221011_211731_00058_ff5sr_faaa1338-ae7a-4565-ab3c-3dd6b1d63837 │ ├── TenantUsageAndCost/ │ │ ├── requirements.txt │ │ └── tenant_usage_and_cost.py │ ├── deployment.sh │ ├── lambdaoutput.json │ ├── samconfig.toml │ └── template.yaml ├── THIRD-PARTY-LICENSES.txt ├── event-engine-assets/ │ ├── initialize-module-sam-template.yaml │ ├── lab1-module-sam-template.yaml │ ├── pre-requisites-event-engine.sh │ └── userinterface-module-sam-template.yaml └── scripts/ ├── cleanup.sh ├── create_tenants.sh ├── lab2_updates.py ├── lab3_updates.py ├── lab4_updates.py ├── lab5_updates.py ├── lab6_updates.py ├── replace_function.py ├── run_all_labs.sh └── run_workshop.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: CODE_OF_CONDUCT.md ================================================ ## Code of Conduct This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Guidelines Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional documentation, we greatly value feedback and contributions from our community. Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution. ## Reporting Bugs/Feature Requests We welcome you to use the GitHub issue tracker to report bugs or suggest features. When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: * A reproducible test case or series of steps * The version of our code being used * Any modifications you've made relevant to the bug * Anything unusual about your environment or deployment ## Contributing via Pull Requests Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 1. You are working against the latest source on the *main* branch. 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. To send us a pull request, please: 1. Fork the repository. 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 3. Ensure local tests pass. 4. Commit to your fork using clear commit messages. 5. Send us a pull request, answering any default questions in the pull request interface. 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). ## Finding contributions to work on Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. ## Code of Conduct This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. ## Security issue notifications If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. ## Licensing See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. ================================================ FILE: Cloud9Setup/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Cloud9Setup/README.md ================================================ # Setup Cloud9 sam build -t prereq-sam-template.yaml --use-container sam deploy ================================================ FILE: Cloud9Setup/increase-disk-size.sh ================================================ #!/bin/bash # Specify the desired volume size in GiB as a command line argument. If not specified, default to 50 GiB. SIZE=50 # Get the ID of the environment host Amazon EC2 instance. INSTANCEID=$(curl http://169.254.169.254/latest/meta-data/instance-id) REGION=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed 's/\(.*\)[a-z]/\1/') # Get the ID of the Amazon EBS volume associated with the instance. VOLUMEID=$(aws ec2 describe-instances \ --instance-id $INSTANCEID \ --query "Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId" \ --output text \ --region $REGION) # Resize the EBS volume. aws ec2 modify-volume --volume-id $VOLUMEID --size $SIZE # Wait for the resize to finish. while [ \ "$(aws ec2 describe-volumes-modifications \ --volume-id $VOLUMEID \ --filters Name=modification-state,Values="optimizing","completed" \ --query "length(VolumesModifications)"\ --output text)" != "1" ]; do sleep 1 done #Check if we're on an NVMe filesystem if [[ -e "/dev/xvda" && $(readlink -f /dev/xvda) = "/dev/xvda" ]] then # Rewrite the partition table so that the partition takes up all the space that it can. sudo growpart /dev/xvda 1 # Expand the size of the file system. # Check if we're on AL2 STR=$(cat /etc/os-release) SUB="VERSION_ID=\"2\"" if [[ "$STR" == *"$SUB"* ]] then sudo xfs_growfs -d / else sudo resize2fs /dev/xvda1 fi else # Rewrite the partition table so that the partition takes up all the space that it can. sudo growpart /dev/nvme0n1 1 # Expand the size of the file system. # Check if we're on AL2 STR=$(cat /etc/os-release) SUB="VERSION_ID=\"2\"" if [[ "$STR" == *"$SUB"* ]] then sudo xfs_growfs -d / else sudo resize2fs /dev/nvme0n1p1 fi fi ================================================ FILE: Cloud9Setup/pre-requisites-versions-check.sh ================================================ #!/bin/bash SUMMARY="Make sure all the pre-requisites checks PASS"$'\n' check_version() { retval=0 MIN_VERSION=$1 CURRENT_VERSION=$2 IFS='.' read -r -a minarr <<< "$MIN_VERSION" IFS='.' read -r -a currarr <<< "$CURRENT_VERSION" for ((i=0; i<${#minarr[@]}; i++)); do #echo "${currarr[$i]}, ${minarr[$i]}" if [[ ${currarr[$i]} -gt ${minarr[$i]} ]]; then break elif [[ ${currarr[$i]} -lt ${minarr[$i]} ]]; then retval=1 break else continue fi done return $retval } echo "Checking python version" python3 --version PYTHON_VERSION=$(python3 --version 2>&1 | cut -d'n' -f 2 | xargs) PYTHON_MIN_VERSION=3.8.0 check_version $PYTHON_MIN_VERSION $PYTHON_VERSION if [[ $? -eq 1 ]]; then echo "ACTION REQUIRED: Need to have python version greater than or equal to $PYTHON_MIN_VERSION" SUMMARY+="* ACTION REQUIRED: Need to have python version greater than or equal to $PYTHON_MIN_VERSION"$'\n' else SUMMARY+="* PASS : Python version $PYTHON_VERSION installed. The minimum required version $PYTHON_MIN_VERSION"$'\n' fi echo "" echo "Checking aws cli version" aws --version if [[ $? -ne 0 ]]; then echo "ACTION REQUIRED: aws cli is missing, please install !!" SUMMARY+="* ACTION REQUIRED: aws cli is missing, please install !!"$'\n' else AWS_VERSION=$(aws --version | cut -d'P' -f 1 | xargs) SUMMARY+="* PASS : $AWS_VERSION version installed"$'\n' jq --version if [[ $? -ne 0 ]]; then echo yes | sudo yum install jq fi CLOUD9_INSTANCE=$(aws ec2 describe-instances --filters Name=instance-type,Values=t3.large | jq -r '.Reservations[0].Instances[0].InstanceId') if [ -z $CLOUD9_INSTANCE ]; then echo "ACTION REQUIRED: Looks like Cloud9 instance with t3.large instance type is missing. Please create one!!" SUMMARY+="* ACTION REQUIRED: Looks like Cloud9 instance with t3.large instance type is missing. Please create one!!"$'\n' else SUMMARY+="* PASS : Has required t3.large instance type"$'\n' CLOUD9_INSTANCE_VOLUME_ID=$(aws ec2 describe-instances --filters Name=instance-type,Values=t3.large | jq -r '.Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId') VOLUME_SIZE=$(aws ec2 describe-volumes --volume-ids $CLOUD9_INSTANCE_VOLUME_ID | jq -r '.Volumes[0].Size') if [[ $VOLUME_SIZE -lt 50 ]]; then echo "ACTION REQUIRED: The volume size of cloud9 is less than 50GiB. Please update volume size to atleast 50GiB" SUMMARY+="* ACTION REQUIRED: The volume size of cloud9 is less than 50GiB. Please update volume size to atleast 50GiB"$'\n' else SUMMARY+="* PASS : Has minimum required 50GiB volume size"$'\n' fi fi fi echo "" echo "Checking sam cli version" sam --version SAM_VERSION=$(sam --version | cut -d'n' -f 2 | xargs) SAM_MIN_VERSION=1.53.0 check_version $SAM_MIN_VERSION $SAM_VERSION if [[ $? -eq 1 ]]; then echo "ACTION REQUIRED: Need to have SAM version greater than or equal to $SAM_MIN_VERSION" SUMMARY+="* ACTION REQUIRED: Need to have SAM version greater than or equal to $SAM_MIN_VERSION"$'\n' else SUMMARY+="* PASS : Sam cli version $SAM_VERSION installed. The minimum required version $SAM_MIN_VERSION"$'\n' fi echo "" echo "Checking git-remote-codecommit version" python3 -m pip show git-remote-codecommit if [[ $? -ne 0 ]]; then echo "ACTION REQUIRED: git-remote-codecommit is missing, please install" SUMMARY+="* ACTION REQUIRED: git-remote-codecommit is missing, please install !!"$'\n' else SUMMARY+="* PASS : Has git-remote-codecommit installed"$'\n' fi echo "" echo "Checking node version" node --version NODE_VERSION=$(node --version | cut -d'v' -f 2) NODE_MIN_VERSION=14.0.0 check_version $NODE_MIN_VERSION $NODE_VERSION if [[ $? -eq 1 ]]; then echo "ACTION REQUIRED: Need to have Node version greater than or equal to $NODE_MIN_VERSION" SUMMARY+="* ACTION REQUIRED: Need to have Node version greater than or equal to $NODE_MIN_VERSION"$'\n' else SUMMARY+="* PASS : Node version $NODE_VERSION installed. The minimum required version $NODE_MIN_VERSION"$'\n' fi echo "" echo "Checking cdk version" cdk --version CDK_VERSION=$(cdk --version | cut -d'(' -f 1| xargs) CDK_MIN_VERSION=2.40.0 check_version $CDK_MIN_VERSION $CDK_VERSION if [[ $? -eq 1 ]]; then echo "ACTION REQUIRED: Need to have CDK version greater than or equal to $CDK_MIN_VERSION" SUMMARY+="* ACTION REQUIRED: Need to have CDK version greater than or equal to $CDK_MIN_VERSION"$'\n' else SUMMARY+="* PASS : CDK version $CDK_VERSION installed. The minimum required version $CDK_MIN_VERSION"$'\n' fi echo "" echo "***************SUMMARY****************" echo "$SUMMARY" echo "***************END OF SUMMARY*********" ================================================ FILE: Cloud9Setup/pre-requisites.sh ================================================ #!/bin/bash -x #Installing NVM curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | sudo -u ec2-user bash . /home/ec2-user/.nvm/nvm.sh #Install python3.8 sudo yum install -y amazon-linux-extras sudo amazon-linux-extras enable python3.8 sudo yum install -y python3.8 sudo alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1 sudo alternatives --set python3 /usr/bin/python3.8 # Uninstall aws cli v1 and Install aws cli version-2.3.0 sudo pip uninstall awscli -y echo "Installing aws cli version-2.3.0" curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64-2.3.0.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install rm awscliv2.zip rm -rf aws # Install sam cli version 1.64.0 echo "Installing sam cli version 1.64.0" wget https://github.com/aws/aws-sam-cli/releases/download/v1.64.0/aws-sam-cli-linux-x86_64.zip unzip aws-sam-cli-linux-x86_64.zip -d sam-installation sudo ./sam-installation/install if [ $? -ne 0 ]; then echo "Sam cli is already present, so deleting existing version" sudo rm /usr/local/bin/sam sudo rm -rf /usr/local/aws-sam-cli echo "Now installing sam cli version 1.64.0" sudo ./sam-installation/install fi rm aws-sam-cli-linux-x86_64.zip rm -rf sam-installation # Install git-remote-codecommit version 1.15.1 echo "Installing git-remote-codecommit version 1.15.1" curl -O https://bootstrap.pypa.io/get-pip.py python3 get-pip.py --user rm get-pip.py python3 -m pip install git-remote-codecommit==1.15.1 # Install node v14.18.1 echo "Installing node v14.18.1" nvm deactivate nvm uninstall node nvm install v14.18.1 nvm use v14.18.1 nvm alias default v14.18.1 # Install cdk cli version ^2.40.0 echo "Installing cdk cli version ^2.40.0" npm uninstall -g aws-cdk npm install -g aws-cdk@"^2.40.0" #Install jq version 1.5 sudo yum -y install jq-1.5 #Install pylint version 2.11.1 python3 -m pip install pylint==2.11.1 python3 -m pip install boto3 ================================================ FILE: Cloud9Setup/samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas-init-environment" s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx" s3_prefix = "serverless-saas" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" ================================================ FILE: LICENSE ================================================ Creative Commons Attribution-ShareAlike 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 – Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike. h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. j. Licensor means the individual(s) or entity(ies) granting rights under this Public License. k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 – Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: A. reproduce and Share the Licensed Material, in whole or in part; and B. produce, reproduce, and Share Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 5. Downstream recipients. A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 – License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: A. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. b. ShareAlike.In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License. 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. Section 4 – Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 – Disclaimer of Warranties and Limitation of Liability. a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 – Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 – Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 – Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ================================================ FILE: LICENSE-SAMPLECODE ================================================ Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: LICENSE-SUMMARY ================================================ Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. The documentation is made available under the Creative Commons Attribution-ShareAlike 4.0 International License. See the LICENSE file. The sample code within this documentation is made available under the MIT-0 license. See the LICENSE-SAMPLECODE file. ================================================ FILE: Lab1/client/Application/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab1/client/Application/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab1/client/Application/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end #cypress cypress/videos/* cypress.env.json # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab1/client/Application/README.md ================================================ # Application This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab1/client/Application/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "application": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "2mb", "maximumError": "2mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "application:build:production" }, "development": { "browserTarget": "application:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "application:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab1/client/Application/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/Application'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab1/client/Application/package.json ================================================ { "name": "application", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "e2e": "npx cypress run" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "bootstrap": "~5.2.0", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "cypress": "~10.3.1", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab1/client/Application/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Products', url: '/products', icon: 'sell', }, { name: 'Orders', url: '/orders', icon: 'shopping_cart', }, ]; ================================================ FILE: Lab1/client/Application/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: '', component: NavComponent, data: { title: 'Home', }, children: [ { path: '', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'orders', loadChildren: () => import('./views/orders/orders.module').then((m) => m.OrdersModule), }, { path: 'products', loadChildren: () => import('./views/products/products.module').then( (m) => m.ProductsModule ), }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab1/client/Application/src/app/app.component.scss ================================================ ================================================ FILE: Lab1/client/Application/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'application'; } ================================================ FILE: Lab1/client/Application/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; @NgModule({ declarations: [AppComponent, NavComponent], imports: [ AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, MatSnackBarModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatIconModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab1/client/Application/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Lab1/client/Application/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Lab1/client/Application/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }}
================================================ FILE: Lab1/client/Application/src/app/nav/nav.component.scss ================================================ .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .sidebar-icon-container { height: 64px; background-color: whitesmoke; display: flex; align-items: center; justify-content: center; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .footer { position: fixed; bottom: 0; width: 100%; height: 40px; background-color: whitesmoke; color: #2d3337; // text-align: center; margin: 0px; } .footer-text { display: flex; } .content { position: absolute; width: 100%; height: 90%; max-height: 90%; overflow: auto; background-color: lightgray; } ================================================ FILE: Lab1/client/Application/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { navItems } from '../_nav'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.scss'], }) export class NavComponent implements OnInit { loading$: Observable = of(false); public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router ) { // this.configSvc.loadConfigurations().subscribe((val) => console.log(val)); this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void {} } ================================================ FILE: Lab1/client/Application/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Lab1/client/Application/src/app/views/dashboard/dashboard.component.html ================================================

Dashboard

{{ card.title }}
================================================ FILE: Lab1/client/Application/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } ================================================ FILE: Lab1/client/Application/src/app/views/dashboard/dashboard.component.ts ================================================ import { Component, ViewChild } from '@angular/core'; import { map } from 'rxjs/operators'; import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js'; import { BaseChartDirective } from 'ng2-charts'; import DataLabelsPlugin from 'chartjs-plugin-datalabels'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'], }) export class DashboardComponent { @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; public barChartOptions: ChartConfiguration['options'] = { responsive: true, maintainAspectRatio: false, // We use these empty structures as placeholders for dynamic theming. scales: { x: {}, y: { min: 10, }, }, plugins: { legend: { display: true, }, datalabels: { anchor: 'end', align: 'end', }, }, }; public barChartType: ChartType = 'bar'; public barChartPlugins = [DataLabelsPlugin]; public barChartData: ChartData<'bar'> = { labels: ['2006', '2007', '2008', '2009', '2010', '2011', '2012'], datasets: [ { data: [65, 59, 80, 81, 56, 55, 40], label: 'Series A' }, { data: [28, 48, 40, 19, 86, 27, 90], label: 'Series B' }, ], }; // events public chartClicked({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public chartHovered({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public randomize(): void { // Only Change 3 values this.barChartData.datasets[0].data = [ Math.round(Math.random() * 100), 59, 80, Math.round(Math.random() * 100), 56, Math.round(Math.random() * 100), 40, ]; this.chart?.update(); } /** Based on the screen size, switch from standard to one column per row */ cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe( map(({ matches }) => { if (matches) { return [ { title: 'Card 1', cols: 1, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 1 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; } return [ { title: 'Card 1', cols: 2, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 2 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; }) ); constructor(private breakpointObserver: BreakpointObserver) {} } ================================================ FILE: Lab1/client/Application/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Lab1/client/Application/src/app/views/orders/create/create.component.html ================================================
Create new order Enter order name Name is required
{{ op.product.name }}
{{ op.product.price | currency }}
{{ op.quantity || 0 }}
================================================ FILE: Lab1/client/Application/src/app/views/orders/create/create.component.scss ================================================ // .card { // margin: 20px; // display: flex; // flex-direction: column; // align-items: flex-start; // } .order-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab1/client/Application/src/app/views/orders/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { Product } from '../../products/models/product.interface'; import { ProductService } from '../../products/product.service'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; interface LineItem { product: Product; quantity?: number; } @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { displayedColumns = []; orderForm: FormGroup; orderProducts: LineItem[] = []; isLoadingProducts: boolean = true; error = ''; constructor( private fb: FormBuilder, private productSvc: ProductService, private orderSvc: OrdersService, private router: Router ) { this.orderForm = this.fb.group({ name: ['', Validators.required], }); } ngOnInit(): void { this.productSvc.fetch().subscribe((products) => { this.orderProducts = products.map((p) => ({ product: p })); this.isLoadingProducts = false; }); this.orderForm = this.fb.group({ orderName: ['', Validators.required], }); } get name() { return this.orderForm.get('name'); } get productQuantity() { return this.orderProducts .filter((p) => !!p.quantity).length > 0; } add(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity ? orderProduct.quantity + 1 : 1, }; } return p; }); } remove(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity && orderProduct.quantity > 1 ? orderProduct.quantity - 1 : undefined, }; } return p; }); } submit() { const val: Order = { ...this.orderForm?.value, orderProducts: this.orderProducts .filter((p) => !!p.quantity) .map((p) => ({ productId: p.product.productId, price: p.product.price, quantity: p.quantity, })), }; this.orderSvc.create(val).subscribe(() => this.router.navigate(['orders'])); } } ================================================ FILE: Lab1/client/Application/src/app/views/orders/detail/detail.component.html ================================================

Order Details mostly made up

NO: {{ orderId$ | async }} | Date: {{ today() | date }}

Services / Products

Item / Details Unit Cost Sum Cost Discount Tax Total
{{ op.productId }}
{{ op.price | currency }}
Before Tax
{{ sum(op) | currency }}
{{ op.quantity }} Units
$0.00
None
{{ tax(op) | currency }}
Sales Tax 8.9%
{{ total(op) | currency }}
Sub Total Discount Total Tax Final
{{ subTotal(order) | currency }} -$0.00 {{ subTotal(order) | currency }} {{ calcTax(order) | currency }} {{ final(order) | currency }}
Comments / Notes
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Odit repudiandae numquam sit facere blanditiis, quasi distinctio ipsam? Libero odit ex expedita, facere sunt, possimus consectetur dolore, nobis iure amet vero.
================================================ FILE: Lab1/client/Application/src/app/views/orders/detail/detail.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .no-bottom-margin { margin-bottom: 0; } .nowrap { white-space: nowrap; } .invoice { font-family: Arial, Helvetica, sans-serif; width: 970px !important; margin: 50px auto; .invoice-header { padding: 25px 25px 15px; h1 { margin: 0; } .media { .media-body { font-size: 0.9em; margin: 0; } } } .invoice-body { border-radius: 10px; padding: 25px; background: #fff; } .invoice-footer { padding: 15px; font-size: 0.9em; text-align: center; color: #999; } } .logo { max-height: 70px; border-radius: 10px; } .dl-horizontal { margin: 0; dt { float: left; width: 80px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } dd { margin-left: 90px; } } .rowamount { padding-top: 15px !important; } .rowtotal { font-size: 1.3em; } .colfix { width: 12%; } .mono { font-family: monospace; } ================================================ FILE: Lab1/client/Application/src/app/views/orders/detail/detail.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Order } from '../models/order.interface'; import { OrderProduct } from '../models/orderproduct.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-detail', templateUrl: './detail.component.html', styleUrls: ['./detail.component.scss'], }) export class DetailComponent implements OnInit { orderId$: Observable | undefined; order$: Observable | undefined; orderProducts$: Observable | undefined; taxRate = 0.0899; constructor(private route: ActivatedRoute, private orderSvc: OrdersService) {} ngOnInit(): void { this.orderId$ = this.route.params.pipe(map((o) => o['orderId'])); this.order$ = this.orderId$.pipe(switchMap((o) => this.orderSvc.get(o))); this.orderProducts$ = this.order$.pipe(map((o) => o.orderProducts)); } today() { return new Date(); } sum(op: OrderProduct) { return op.price * op.quantity; } tax(op: OrderProduct) { return this.sum(op) * this.taxRate; } total(op: OrderProduct) { return this.sum(op) + this.tax(op); } subTotal(order: Order) { return order.orderProducts .map((op) => op.price * op.quantity) .reduce((acc, curr) => acc + curr); } calcTax(order: Order) { return this.subTotal(order) * this.taxRate; } final(order: Order) { return this.subTotal(order) + this.calcTax(order); } } ================================================ FILE: Lab1/client/Application/src/app/views/orders/list/list.component.html ================================================

Order List

Name {{ element.orderName }} Line Items {{ element.orderProducts?.length }} Total {{ sum(element) | currency }}
================================================ FILE: Lab1/client/Application/src/app/views/orders/list/list.component.scss ================================================ .order-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Lab1/client/Application/src/app/views/orders/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { displayedColumns: string[] = ['name', 'lineItems', 'total']; orderData: Order[] = []; isLoading: boolean = true; constructor(private orderSvc: OrdersService, private router: Router) {} ngOnInit(): void { this.orderSvc.fetch().subscribe((data) => { this.isLoading = false; this.orderData = data; }); } sum(order: Order): number { return order.orderProducts .map((p) => p.price * p.quantity) .reduce((acc, curr) => acc + curr); } } ================================================ FILE: Lab1/client/Application/src/app/views/orders/models/order.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { OrderProduct } from './orderproduct.interface'; export interface Order { key: string; orderId: string; orderName: string; orderProducts: OrderProduct[]; } ================================================ FILE: Lab1/client/Application/src/app/views/orders/models/orderproduct.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface OrderProduct { productId: string; price: number; quantity: number; } ================================================ FILE: Lab1/client/Application/src/app/views/orders/orders-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', data: { title: 'All Orders', }, component: ListComponent, }, { path: 'create', data: { title: 'Create Order', }, component: CreateComponent, }, { path: 'detail/:orderId', data: { title: 'View Order Detail', }, component: DetailComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class OrdersRoutingModule {} ================================================ FILE: Lab1/client/Application/src/app/views/orders/orders.module.ts ================================================ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; import { OrdersRoutingModule } from './orders-routing.module'; @NgModule({ declarations: [CreateComponent, ListComponent, DetailComponent], imports: [ CommonModule, OrdersRoutingModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class OrdersModule {} ================================================ FILE: Lab1/client/Application/src/app/views/orders/orders.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Order } from './models/order.interface'; @Injectable({ providedIn: 'root', }) export class OrdersService { orders: Order[] = []; baseUrl = environment.apiGatewayUrl; constructor(private http: HttpClient) {} fetch(): Observable { return this.http.get(`${this.baseUrl}/orders`); } get(orderId: string): Observable { const url = `${this.baseUrl}/order/${orderId}`; return this.http.get(url); } create(order: Order): Observable { return this.http.post(`${this.baseUrl}/order`, order); } } ================================================ FILE: Lab1/client/Application/src/app/views/products/create/create.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required SKU Category {{category}}
================================================ FILE: Lab1/client/Application/src/app/views/products/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab1/client/Application/src/app/views/products/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ProductService } from '../product.service'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); } get name() { return this.productForm.get('name'); } get price() { return this.productForm.get('price'); } submit() { this.productSvc.post(this.productForm.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err.message); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Lab1/client/Application/src/app/views/products/edit/edit.component.html ================================================
Edit Product Product ID Enter product name Name is required Enter product price Price is required SKU Category {{ category }}
================================================ FILE: Lab1/client/Application/src/app/views/products/edit/edit.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab1/client/Application/src/app/views/products/edit/edit.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.scss'], }) export class EditComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; product$: Observable | undefined; productId$: Observable | undefined; productName$: Observable | undefined; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router, private route: ActivatedRoute ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ shardId: [], productId: [], name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); this.productId$ = this.route.params.pipe(map((p) => p['productId'])); this.product$ = this.productId$.pipe( switchMap((p) => this.productSvc.get(p)) ); this.productName$ = this.product$.pipe(map((p) => p?.name)); this.product$.subscribe((val) => { this.productForm?.patchValue({ ...val, }); }); } get name() { return this.productForm?.get('name'); } get price() { return this.productForm?.get('price'); } submit() { this.productSvc.put(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => console.error(err), }); } delete() { this.productSvc.delete(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Lab1/client/Application/src/app/views/products/list/list.component.html ================================================

Product List

Name {{ element.name }} Price {{ element.price | currency }} SKU {{ element.sku }}
================================================ FILE: Lab1/client/Application/src/app/views/products/list/list.component.scss ================================================ .product-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Lab1/client/Application/src/app/views/products/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { productData: Product[] = []; isLoading: boolean = true; displayedColumns: string[] = ['name', 'price', 'sku']; constructor(private productSvc: ProductService, private router: Router) {} ngOnInit(): void { this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onEdit(product: Product) { this.router.navigate(['products', 'edit', product.productId]); return false; } onRemove(product: Product) { this.productSvc.delete(product); this.isLoading = true; this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onCreate() { this.router.navigate(['products', 'create']); } } ================================================ FILE: Lab1/client/Application/src/app/views/products/models/product.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface Product { key: string; shardId: string; productId: string; name: string; price: number; sku: string; category: string; pictureUrl?: string; } ================================================ FILE: Lab1/client/Application/src/app/views/products/product.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Product } from './models/product.interface'; @Injectable({ providedIn: 'root', }) export class ProductService { constructor(private http: HttpClient) {} baseUrl = environment.apiGatewayUrl; fetch(): Observable { return this.http.get(`${this.baseUrl}/products`); } get(productId: string): Observable { const url = `${this.baseUrl}/product/${productId}`; return this.http.get(url); } delete(product: Product) { const url = `${this.baseUrl}/product/${product.productId}`; return this.http.delete(url); } put(product: Product) { const url = `${this.baseUrl}/product/${product.productId}`; return this.http.put(url, product); } post(product: Product) { return this.http.post(`${this.baseUrl}/product`, product); } } ================================================ FILE: Lab1/client/Application/src/app/views/products/products-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'Product List', }, component: ListComponent, }, { path: 'create', data: { title: 'Create new Product', }, component: CreateComponent, }, { path: 'edit/:productId', data: { title: 'Edit Product', }, component: EditComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class ProductsRoutingModule {} ================================================ FILE: Lab1/client/Application/src/app/views/products/products.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ProductsRoutingModule } from './products-routing.module'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [CreateComponent, EditComponent, ListComponent], imports: [ CommonModule, ReactiveFormsModule, ProductsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class ProductsModule {} ================================================ FILE: Lab1/client/Application/src/assets/.gitkeep ================================================ ================================================ FILE: Lab1/client/Application/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab1/client/Application/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiGatewayUrl: 'https://0grqqki7qk.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab1/client/Application/src/environments/environment.ts ================================================ export const environment = { production: false, apiGatewayUrl: 'https://0grqqki7qk.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab1/client/Application/src/index.html ================================================ Application ================================================ FILE: Lab1/client/Application/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab1/client/Application/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab1/client/Application/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab1/client/Application/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab1/client/Application/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~bootstrap/scss/functions"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/maps"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; .chart-container canvas { max-height: 250px; width: auto; } .chart-container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; } ================================================ FILE: Lab1/client/Application/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab1/client/Application/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab1/client/Application/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "strictPropertyInitialization": false, "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab1/client/Application/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab1/scripts/deployment.sh ================================================ #!/bin/bash if [[ "$#" -eq 0 ]]; then echo "Invalid parameters" echo "Command to deploy client code: deployment.sh -c --stack-name " echo "Command to deploy server code: deployment.sh -s --stack-name " echo "Command to deploy server & client code: deployment.sh -s -c --stack-name " exit 1 fi while [[ "$#" -gt 0 ]]; do case $1 in -s) server=1 ;; -c) client=1 ;; --stack-name) stackname=$2 shift ;; *) echo "Unknown parameter passed: $1" exit 1 ;; esac shift done if [[ -z "$stackname" ]]; then echo "Please provide CloudFormation stack name as parameter" echo "Note: Invoke script without parameters to know the list of script parameters" exit 1 fi if [[ $server -eq 1 ]]; then echo "Server code is getting deployed" cd ../server || exit # stop execution if cd fails REGION=$(aws configure get region) DEFAULT_SAM_S3_BUCKET=$(grep s3_bucket samconfig.toml | cut -d'=' -f2 | cut -d \" -f2) echo "aws s3 ls s3://$DEFAULT_SAM_S3_BUCKET" if ! aws s3 ls "s3://${DEFAULT_SAM_S3_BUCKET}"; then echo "S3 Bucket: $DEFAULT_SAM_S3_BUCKET specified in samconfig.toml is not readable. So creating a new S3 bucket and will update samconfig.toml with new bucket name." UUID=$(uuidgen | awk '{print tolower($0)}') SAM_S3_BUCKET=sam-bootstrap-bucket-$UUID aws s3 mb "s3://${SAM_S3_BUCKET}" --region "$REGION" aws s3api put-bucket-encryption \ --bucket "$SAM_S3_BUCKET" \ --server-side-encryption-configuration '{"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]}' if [[ $? -ne 0 ]]; then exit 1 fi # Updating samconfig.toml with new bucket name ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' samconfig.toml fi echo "Validating server code using pylint" python3 -m pylint -E -d E0401 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi sam build -t template.yaml --use-container sam deploy --config-file samconfig.toml --region="$REGION" --stack-name="$stackname" cd ../scripts || exit # stop execution if cd fails fi if [[ $client -eq 1 ]]; then echo "Client code is getting deployed" APP_SITE_BUCKET=$(aws cloudformation describe-stacks --stack-name "$stackname" --query "Stacks[0].Outputs[?OutputKey=='AppBucket'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name "$stackname" --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) APP_APIGATEWAYURL=$(aws cloudformation describe-stacks --stack-name "$stackname" --query "Stacks[0].Outputs[?OutputKey=='APIGatewayURL'].OutputValue" --output text) # Configuring application UI echo "aws s3 ls s3://${APP_SITE_BUCKET}" if ! aws s3 ls "s3://${APP_SITE_BUCKET}"; then echo "Error! S3 Bucket: $APP_SITE_BUCKET not readable" exit 1 fi cd ../client/Application || exit # stop execution if cd fails echo "Configuring environment for App Client" cat <./src/environments/environment.prod.ts export const environment = { production: true, apiGatewayUrl: '$APP_APIGATEWAYURL' }; EoF cat <./src/environments/environment.ts export const environment = { production: true, apiGatewayUrl: '$APP_APIGATEWAYURL' }; EoF npm install && npm run build echo "aws s3 sync --delete --cache-control no-store dist s3://${APP_SITE_BUCKET}" if ! aws s3 sync --delete --cache-control no-store dist "s3://${APP_SITE_BUCKET}"; then exit 1 fi echo "Completed configuring environment for App Client" echo "Application site URL: https://${APP_SITE_URL}" fi ================================================ FILE: Lab1/scripts/geturl.sh ================================================ #!/bin/bash stackname="serverless-saas-workshop-lab1" APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name "$stackname" --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) echo "Application site URL: https://${APP_SITE_URL}" ================================================ FILE: Lab1/server/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Lab1/server/OrderService/order_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Order: key='' def __init__(self, orderId, orderName, orderProducts): self.orderId = orderId self.orderName = orderName self.orderProducts = orderProducts class OrderProduct: def __init__(self, productId, price, quantity): self.productId = productId self.price = price self.quantity = quantity ================================================ FILE: Lab1/server/OrderService/order_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import order_service_dal from decimal import Decimal from types import SimpleNamespace from aws_lambda_powertools import Tracer def get_order(event, context): logger.info("Request received to get a order") params = event['pathParameters'] orderId = params['id'] order = order_service_dal.get_order(event, orderId) logger.info("Request completed to get a order") return utils.generate_response(order) def create_order(event, context): logger.info("Request received to create a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) order = order_service_dal.create_order(event, payload) logger.info("Request completed to create a order") return utils.generate_response(order) def update_order(event, context): logger.info("Request received to update a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] orderId = params['id'] order = order_service_dal.update_order(event, payload, orderId) logger.info("Request completed to update a order") return utils.generate_response(order) def delete_order(event, context): logger.info("Request received to delete a order") params = event['pathParameters'] orderId = params['id'] response = order_service_dal.delete_order(event, orderId) logger.info("Request completed to delete a order") return utils.create_success_response("Successfully deleted the order") def get_orders(event, context): logger.info("Request received to get all orders") response = order_service_dal.get_orders(event) logger.info("Request completed to get all orders") return utils.generate_response(response) ================================================ FILE: Lab1/server/OrderService/order_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import boto3 from botocore.exceptions import ClientError import uuid from order_models import Order import json import utils from types import SimpleNamespace import logger import random table_name = os.environ['ORDER_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) def get_order(event, orderId): try: response = table.get_item(Key={'orderId': orderId}) item = response['Item'] order = Order(item['orderId'], item['orderName'], item['orderProducts']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a order', e) else: return order def delete_order(event, orderId): try: response = table.delete_item(Key={'orderId': orderId}) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a order', e) else: logger.info("DeleteItem succeeded:") return response def create_order(event, payload): order = Order(str(uuid.uuid4()), payload.orderName, payload.orderProducts) try: response = table.put_item(Item={ 'orderId': order.orderId, 'orderName': order.orderName, 'orderProducts': get_order_products_dict(order.orderProducts) }) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a order', e) else: logger.info("PutItem succeeded:") return order def update_order(event, payload, orderId): try: order = Order(orderId,payload.orderName, payload.orderProducts) response = table.update_item(Key={'orderId': order.orderId}, UpdateExpression="set orderName=:orderName, " +"orderProducts=:orderProducts", ExpressionAttributeValues={ ':orderName': order.orderName, ':orderProducts': get_order_products_dict(order.orderProducts) }, ReturnValues="UPDATED_NEW") except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a order', e) else: logger.info("UpdateItem succeeded:") return order def get_orders(event): orders = [] try: response = table.scan() if (len(response['Items']) > 0): for item in response['Items']: order = Order(item['orderId'], item['orderName'], item['orderProducts']) orders.append(order) except ClientError as e: logger.error("Error getting all orders") raise Exception('Error getting all orders', e) else: logger.info("Get orders succeeded") return orders def get_order_products_dict(orderProducts): orderProductList = [] for i in range(len(orderProducts)): product = orderProducts[i] orderProductList.append(vars(product)) return orderProductList ================================================ FILE: Lab1/server/OrderService/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Lab1/server/ProductService/product_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Product: key ='' def __init__(self, productId, sku, name, price, category): self.productId = productId self.sku = sku self.name = name self.price = price self.category = category class Category: def __init__(self, id, name): self.id = id self.name = name ================================================ FILE: Lab1/server/ProductService/product_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import product_service_dal from decimal import Decimal from types import SimpleNamespace def get_product(event, context): logger.info("Request received to get a product") params = event['pathParameters'] productId = params['id'] product = product_service_dal.get_product(event, productId) logger.info("Request completed to get a product") return utils.generate_response(product) def create_product(event, context): logger.info("Request received to create a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) logger.info(payload) product = product_service_dal.create_product(event, payload) logger.info("Request completed to create a product") return utils.generate_response(product) def update_product(event, context): logger.info("Request received to update a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] product = product_service_dal.update_product(event, payload, key) logger.info("Request completed to update a product") return utils.generate_response(product) def delete_product(event, context): logger.info("Request received to delete a product") params = event['pathParameters'] key = params['id'] response = product_service_dal.delete_product(event, key) logger.info("Request completed to delete a product") return utils.create_success_response("Successfully deleted the product") def get_products(event, context): logger.info("Request received to get all products") response = product_service_dal.get_products(event) logger.info("Request completed to get all products") return utils.generate_response(response) ================================================ FILE: Lab1/server/ProductService/product_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import boto3 from botocore.exceptions import ClientError import uuid import logger from product_models import Product from types import SimpleNamespace from boto3.dynamodb.conditions import Key table_name = os.environ['PRODUCT_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) def get_product(event, productId): try: response = table.get_item(Key={'productId': productId}) item = response['Item'] product = Product(item['productId'], item['sku'], item['name'], item['price'], item['category']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a product', e) else: logger.info("GetItem succeeded:"+ str(product)) return product def delete_product(event, productId): try: response = table.delete_item(Key={'productId': productId}) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a product', e) else: logger.info("DeleteItem succeeded:") return response def create_product(event, payload): product = Product(str(uuid.uuid4()), payload.sku,payload.name, payload.price, payload.category) try: response = table.put_item( Item= { 'productId': product.productId, 'sku': product.sku, 'name': product.name, 'price': product.price, 'category': product.category } ) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: logger.info("PutItem succeeded:") return product def update_product(event, payload, productId): try: product = Product(productId,payload.sku, payload.name, payload.price, payload.category) response = table.update_item(Key={'productId': product.productId}, UpdateExpression="set sku=:sku, #n=:productName, price=:price, category=:category", ExpressionAttributeNames= {'#n':'name'}, ExpressionAttributeValues={ ':sku': product.sku, ':productName': product.name, ':price': product.price, ':category': product.category }, ReturnValues="UPDATED_NEW") except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a product', e) else: logger.info("UpdateItem succeeded:") return product def get_products(event): products =[] try: response = table.scan() if (len(response['Items']) > 0): for item in response['Items']: product = Product(item['productId'], item['sku'], item['name'], item['price'], item['category']) products.append(product) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting all products', e) else: logger.info("Get products succeeded") return products ================================================ FILE: Lab1/server/ProductService/requirements.txt ================================================ requests pytest-mock aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Lab1/server/README.md ================================================ if using EE sam build --use-container && sam package --output-template-file packaged.yaml --s3-bucket aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx --region us-west-2 sam deploy --template-file packaged.yaml --config-file samconfig.toml Normally sam build -t template.yaml --use-container sam deploy --config-file samconfig.toml ================================================ FILE: Lab1/server/layers/logger.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from aws_lambda_powertools import Logger logger = Logger() """Log info messages """ def info(log_message): logger.info (log_message) """Log error messages """ def error(log_message): logger.error (log_message) ================================================ FILE: Lab1/server/layers/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] simplejson jsonpickle ================================================ FILE: Lab1/server/layers/utils.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import jsonpickle import simplejson import boto3 from enum import Enum class StatusCodes(Enum): SUCCESS = 200 UN_AUTHORIZED = 401 NOT_FOUND = 404 def create_success_response(message): return { "statusCode": StatusCodes.SUCCESS.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def generate_response(inputObject): return { "statusCode": 200, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": encode_to_json_object(inputObject), } def encode_to_json_object(inputObject): jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True) jsonpickle.set_preferred_backend('simplejson') return jsonpickle.encode(inputObject, unpicklable=False, use_decimal=True) ================================================ FILE: Lab1/server/samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas-lab1" s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx" s3_prefix = "serverless-saas-lab1" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM" cached="true" parallel="true" ================================================ FILE: Lab1/server/template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Lab1 - Basic Serverless Application Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG Parameters: StageName: Type: String Default: "prod" Description: "Stage Name for the api" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-workshoplab1 Description: Utilities for project ContentUri: layers/ CompatibleRuntimes: - python3.9 LicenseInfo: 'MIT' RetentionPolicy: Retain Metadata: BuildMethod: python3.9 ProductTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: productId AttributeType: S KeySchema: - AttributeName: productId KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: Product-Lab1 OrderTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: orderId AttributeType: S KeySchema: - AttributeName: orderId KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: Order-Lab1 ProductFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: product-function-execution-role-lab1 Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: product-function-policy-lab1 PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query - dynamodb:Scan Resource: - !GetAtt ProductTable.Arn GetProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable GetProductsFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_products Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable CreateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.create_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable UpdateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.update_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable DeleteProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.delete_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable OrderFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: order-function-execution-role-lab1 Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: order-function-policy-lab1 PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query - dynamodb:Scan Resource: - !GetAtt OrderTable.Arn GetOrdersFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_orders Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable GetOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable CreateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.create_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable UpdateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.update_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable DeleteOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.delete_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable ApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/api-gateway/access-logs-serverless-saas-workshop-lab1-api RetentionInDays: 30 ApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: '/*' HttpMethod: '*' AccessLogSetting: DestinationArn: !GetAtt ApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: 'serverless-saas-workshop-lab1' basePath: !Join ['', ['/', !Ref StageName]] schemes: - https paths: /order/{id}: get: summary: Returns a order description: Return a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a order description: Deletes a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /orders: get: summary: Returns all orders description: Returns all orders. produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrdersFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /order: post: produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product/{id}: get: summary: Returns a product description: Return a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a product description: Deletes a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /products: get: summary: Returns all products description: Returns all products. produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductsFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product: post: produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock StageName: !Ref StageName GetProductsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt GetProductsFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] GetProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] CreateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] UpdateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] DeleteProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] GetOrdersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrdersFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] GetOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] CreateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] UpdateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] DeleteOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distribution" AppBucket: Type: AWS::S3::Bucket DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AppSiteReadPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref AppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId ApplicationSite: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: lab1-tenantapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'AppBucket.RegionalDomainName' Id: lab1-tenantapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: APIGatewayURL: Description: "API Gateway endpoint URL for API" Value: !Join ['', [!Sub "https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/", !Ref StageName]] ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt ApplicationSite.DomainName AppBucket: Description: The name of the bucket for uploading the Tenant Management site to Value: !Ref AppBucket ================================================ FILE: Lab2/client/Admin/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab2/client/Admin/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab2/client/Admin/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab2/client/Admin/README.md ================================================ # Admin This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab2/client/Admin/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "admin": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "admin:build:production" }, "development": { "browserTarget": "admin:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "admin:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab2/client/Admin/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab2/client/Admin/package.json ================================================ { "name": "admin", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/flex-layout": "~14.0.0-beta.40", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.1.3", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab2/client/Admin/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Tenants', url: '/tenants', icon: 'groups', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Lab2/client/Admin/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: '', component: NavComponent, data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'tenants', loadChildren: () => import('./views/tenants/tenants.module').then((m) => m.TenantsModule), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab2/client/Admin/src/app/app.component.scss ================================================ ================================================ FILE: Lab2/client/Admin/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Lab2/client/Admin/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { HttpInterceptorProviders } from './interceptors'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; @NgModule({ declarations: [AppComponent, NavComponent, AuthComponent], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, HttpClientModule, HttpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab2/client/Admin/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Lab2/client/Admin/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const HttpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Lab2/client/Admin/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Lab2/client/Admin/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Lab2/client/Admin/src/app/nav/nav.component.css ================================================ .sidenav-container { height: 100%; } .sidenav-content-container { background-color: lightgray; } .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .mat-toolbar.mat-primary { position: sticky; top: 0; z-index: 1; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .spinner-container { height: 80%; display: flex; justify-content: center; align-items: center; box-sizing: border-box; } ================================================ FILE: Lab2/client/Admin/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ username$ | async }}
================================================ FILE: Lab2/client/Admin/src/app/nav/nav.component.spec.ts ================================================ import { LayoutModule } from '@angular/cdk/layout'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { NavComponent } from './nav.component'; describe('NavComponent', () => { let component: NavComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [NavComponent], imports: [ NoopAnimationsModule, LayoutModule, MatButtonModule, MatIconModule, MatListModule, MatSidenavModule, MatToolbarModule, ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(NavComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should compile', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: Lab2/client/Admin/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.css'], }) export class NavComponent implements OnInit { tenantName = ''; loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router ) { this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => err); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map((sesh) => sesh && sesh.isValid()) ); const token$ = session$.pipe(map((sesh) => sesh && sesh.getIdToken())); this.username$ = token$.pipe( map((t) => t && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } } ================================================ FILE: Lab2/client/Admin/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Lab2/client/Admin/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Lab2/client/Admin/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Lab2/client/Admin/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Lab2/client/Admin/src/app/views/dashboard/dashboard.component.html ================================================

Traffic

December 2020
================================================ FILE: Lab2/client/Admin/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } .chart-wrapper { padding-right: 20px; position: relative; margin: auto; height: 80vh; width: 80vw; } ================================================ FILE: Lab2/client/Admin/src/app/views/dashboard/dashboard.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ChartConfiguration, ChartType } from 'chart.js'; import { TenantsService } from '../tenants/tenants.service'; interface DataSet { label: string; data: number[]; } interface ChartData { tenantId: string; dataSet: DataSet[]; totalOrders: number; } @Component({ templateUrl: 'dashboard.component.html', styleUrls: ['./dashboard.component.scss'], selector: 'app-dashboard', }) export class DashboardComponent implements OnInit { constructor(private tenantSvc: TenantsService) {} data: ChartData[] = []; // lineChart public lineChartElements = 27; public lineChartData1: Array = []; public lineChartData2: Array = []; public lineChartData3: Array = []; public lineChartData: Array = [ { data: this.lineChartData1, label: 'Current', backgroundColor: 'rgba(148,159,177,0.2)', borderColor: 'rgba(148,159,177,1)', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, { data: this.lineChartData2, label: 'Previous', backgroundColor: 'rgba(77,83,96,0.2)', borderColor: 'rgba(77,83,96,1)', pointBackgroundColor: 'rgba(77,83,96,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(77,83,96,1)', fill: 'origin', }, { data: this.lineChartData3, label: 'BEP', backgroundColor: 'rgba(255,0,0,0.3)', borderColor: 'red', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, ]; public lineChartLegend = false; /* tslint:disable:max-line-length */ public lineChartLabels: Array = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ]; /* tslint:enable:max-line-length */ public lineChartType: ChartType = 'line'; public lineChartOptions: ChartConfiguration['options'] = { maintainAspectRatio: false, elements: { line: { tension: 0.5, }, }, scales: { // We use this empty structure as a placeholder for dynamic theming. x: {}, 'y-axis-0': { position: 'left', }, 'y-axis-1': { position: 'right', grid: { color: 'rgba(80,80,80,0.3)', }, ticks: { color: '#808080', }, }, }, }; public random(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } ngOnInit(): void { for (let i = 0; i <= this.lineChartElements; i++) { this.lineChartData1.push(this.random(50, 200)); this.lineChartData2.push(this.random(80, 100)); this.lineChartData3.push(65); } } } ================================================ FILE: Lab2/client/Admin/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Lab2/client/Admin/src/app/views/tenants/create/create.component.html ================================================
Provision tenant Name Email Phone Address Plan
================================================ FILE: Lab2/client/Admin/src/app/views/tenants/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab2/client/Admin/src/app/views/tenants/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { TenantsService } from '../tenants.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { submitting = false; tenantForm: FormGroup = this.fb.group({ tenantName: [null, [Validators.required]], tenantEmail: [null, [Validators.email, Validators.required]], tenantTier: [null, [Validators.required]], tenantPhone: [null], tenantAddress: [null], }); constructor( private fb: FormBuilder, private tenantSvc: TenantsService, private router: Router, private _snackBar: MatSnackBar ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; const user = { ...this.tenantForm.value, }; this.tenantSvc.post(this.tenantForm.value).subscribe({ next: () => { this.submitting = false; this.openErrorMessageSnackBar('Successfully created new tenant!'); this.router.navigate(['tenants']); }, error: (err) => { this.submitting = false; this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); }, }); } } ================================================ FILE: Lab2/client/Admin/src/app/views/tenants/list/list.component.html ================================================
Tenant List
Id {{ element.tenantId }} Tenant Name {{ element.tenantName }} E-Mail {{ element.tenantEmail }} Plan {{ element.tenantTier }} Status {{ element.isActive }}
================================================ FILE: Lab2/client/Admin/src/app/views/tenants/list/list.component.scss ================================================ .tenant-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab2/client/Admin/src/app/views/tenants/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Tenant } from '../models/tenant'; import { TenantsService } from '../tenants.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { tenants$ = new Observable(); tenantData: Tenant[] = []; isLoading: boolean = true; displayedColumns = [ 'tenantId', 'tenantName', 'tenantEmail', 'tenantTier', 'isActive', ]; constructor(private tenantSvc: TenantsService) {} ngOnInit(): void { this.tenantSvc.fetch().subscribe((data) => { this.tenantData = data; this.isLoading = false; }); } } ================================================ FILE: Lab2/client/Admin/src/app/views/tenants/models/tenant.ts ================================================ export interface Tenant { tenantId: string; tenantName: string; tenantEmail: string; tenantTier: string; isActive: boolean; } ================================================ FILE: Lab2/client/Admin/src/app/views/tenants/tenants-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', component: ListComponent, data: { title: 'Tenant List', }, }, { path: 'create', component: CreateComponent, data: { title: 'Provision new tenant', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class TenantsRoutingModule {} ================================================ FILE: Lab2/client/Admin/src/app/views/tenants/tenants.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ListComponent } from './list/list.component'; import { TenantsRoutingModule } from './tenants-routing.module'; import { CreateComponent } from './create/create.component'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, TenantsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ReactiveFormsModule, MatSnackBarModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class TenantsModule {} ================================================ FILE: Lab2/client/Admin/src/app/views/tenants/tenants.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Tenant } from './models/tenant'; @Injectable({ providedIn: 'root', }) export class TenantsService { constructor(private http: HttpClient) {} baseUrl = `${environment.apiUrl}`; tenantsApiUrl = `${this.baseUrl}/tenants`; registrationApiUrl = `${this.baseUrl}/registration`; // TODO strongly-type these anys as tenants once we dial in what the tenant call should return fetch(): Observable { return this.http.get(this.tenantsApiUrl); } post(tenant: Tenant): Observable { return this.http.post(this.registrationApiUrl, tenant); } } ================================================ FILE: Lab2/client/Admin/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
Tenant Id folder_special
================================================ FILE: Lab2/client/Admin/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab2/client/Admin/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], tenantId: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Lab2/client/Admin/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Lab2/client/Admin/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab2/client/Admin/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Lab2/client/Admin/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Lab2/client/Admin/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Lab2/client/Admin/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Lab2/client/Admin/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.apiUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } } ================================================ FILE: Lab2/client/Admin/src/assets/.gitkeep ================================================ ================================================ FILE: Lab2/client/Admin/src/aws-exports.ts ================================================ const awsmobile = { aws_project_region: 'us-west-2', aws_cognito_region: 'us-west-2', aws_user_pools_id: 'us-west-2_nWhwjijMc', aws_user_pools_web_client_id: '61ipvhmvdrfso0cftpil9irk8k', }; export default awsmobile; ================================================ FILE: Lab2/client/Admin/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab2/client/Admin/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab2/client/Admin/src/environments/environment.ts ================================================ export const environment = { production: false, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab2/client/Admin/src/index.html ================================================ Dashboard ================================================ FILE: Lab2/client/Admin/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { Amplify } from 'aws-amplify'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import aws_exports from './aws-exports'; Amplify.configure(aws_exports); if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab2/client/Admin/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab2/client/Admin/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab2/client/Admin/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab2/client/Admin/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; ================================================ FILE: Lab2/client/Admin/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab2/client/Admin/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab2/client/Admin/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab2/client/Admin/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab2/client/Landing/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab2/client/Landing/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab2/client/Landing/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab2/client/Landing/README.md ================================================ # Landing This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab2/client/Landing/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "Landing": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/custom-theme.scss", "src/styles.scss", "node_modules/font-awesome/css/font-awesome.min.css" ] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "10kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "Landing:build:production" }, "development": { "browserTarget": "Landing:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "Landing:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab2/client/Landing/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab2/client/Landing/package.json ================================================ { "name": "landing", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "bootstrap": "~5.1.3", "font-awesome": "~4.7.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab2/client/Landing/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; export const routes: Routes = [ { path: '', redirectTo: 'landing', pathMatch: 'full', }, { path: 'landing', component: LandingComponent, }, { path: 'register', component: RegisterComponent, }, { path: '**', redirectTo: 'landing', pathMatch: 'full', }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab2/client/Landing/src/app/app.component.scss ================================================ ================================================ FILE: Lab2/client/Landing/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'landing'; } ================================================ FILE: Lab2/client/Landing/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; @NgModule({ declarations: [AppComponent, LandingComponent, RegisterComponent], imports: [ AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, MatButtonModule, MatCardModule, MatIconModule, MatProgressSpinnerModule, MatSidenavModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, FormsModule, HttpClientModule, ReactiveFormsModule, MatSnackBarModule, MatInputModule, MatFormFieldModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab2/client/Landing/src/app/views/landing/landing.component.html ================================================

Serverless SaaS Reference Architecture

It's so nice it blows your mind.

Sign up now!

Serverless SaaS Reference Architecture is so awesome.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere temporibus omnis illum, officia. Architecto voluptatibus commodi voluptatem perspiciatis eos possimus, eius at molestias quaerat magnam? Odio qui quos ipsam natus.

Serverless SaaS Reference Architecture so awesome. Makes you awesome - go sign up!

Serverless SaaS Reference Architecture so great. Makes you even greater - go sign up now. Super cheap deal!

Feel lonely? Go sign up and have a friend!

Take Serverless SaaS Reference Architecture with you everywhere you go.

Serverless SaaS Reference Architecture is all you need. Anywhere - ever. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita sapiente hic voluptatum quo sunt totam accusamus distinctio minus aliquid quis!

Love Serverless SaaS Reference Architecture. So nice! So good! Could not live without!

Satisfied Customer

Reasons to sign up this product:

  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!
  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!

Why you still reading?

Sign up now!
================================================ FILE: Lab2/client/Landing/src/app/views/landing/landing.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // $color-red: #cc615f; $color-grey: #4b4b4b; $color-grey--light: #d3d3d3; $btn-amzn: #ec7211; $color-primary: #232f3e; $bp-s: 65.75em; //700px; $bp-xs: 34.375em; //550px; @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,800,300); *{ box-sizing: border-box; } html{ width: 100%; height:100%; margin: 0; padding: 0; } body{ width: 100%; height:100%; font-family: 'Open Sans','Helvetica Neue',Helvetica, sans-serif; font-size: 100%; line-height: 1.45; color: #141414; } a{ text-decoration: none; &:hover{ text-decoration: none; } } img{ max-width: 100%; } .btn{ display: inline-block; margin: 1rem 0; color: white; font-weight: 500; font-size: 1.3rem; background: $btn-amzn; letter-spacing: .02em; border: none; border-radius: 5px; padding: .8rem 1rem .9rem; text-shadow: 0 1px rgba(black,.3); box-shadow: 0 0 2px rgba(black,.2); @media (max-width: $bp-s){ padding: .5rem .7rem .6rem; font-size: 1rem; } &:hover{ background: lighten($btn-amzn,5%); color: #fff; } &:focus, &:active, &:focus:active{ background: darken($btn-amzn,5%); border-color: darken($btn-amzn,5%); box-shadow: 0 2px 5px 0 rgba(black,.5) inset; } } .container{ margin: 0 auto; width: 90%; max-width: 900px; } header{ color: white; background: #232f3e; padding: 10rem 0; text-align: center; position: relative; z-index: 1; overflow: hidden; @media (max-width: $bp-s){ padding: 2rem 0; } h1{ font-size: 3rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 2rem; } } h2{ font-weight: 300; font-size: 1.5rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } } section{ background: #fff; color: $color-grey; padding: 3.5rem 0; @media (max-width: $bp-s){ padding: 2rem 0; } &.section--primary{ background: $color-primary; color: #fff; } &.section--primary--alt{ background: desaturate(lighten($color-primary,15%),10%); color: #fff; } &.section--primary--light{ background: rgba($color-primary,.1); } &.section--grey{ background: $color-grey; color: #fff; } &.section--grey--light{ background: $color-grey--light; color: #fff; } h3{ text-align: center; font-size: 2rem; font-weight: 300; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } li{ font-size: 1.2rem; font-weight: 300; } p{ font-size: 1.2rem; font-weight: 300; } } .col{ margin: 0 1.5%; display: inline-block; vertical-align: top; } .col-7{ @extend .col; width: 64%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-3{ @extend .col; width: 29%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-5{ @extend .col; width: 30%; @media (max-width: $bp-xs){ width: 60%; margin: 0; } } .details{ text-align: left; h3{ font-size: 2rem; text-align: left; } } .features{ text-align: center; padding: 1rem; &:hover{ background: rgba(white,.1); } @media (max-width: $bp-s){ width: 100%; margin: 0; text-align: left; border-bottom: 1px solid rgba(white,.2); &:last-child{ border: none; } } i{ font-size: 4rem; margin: 0 0 2rem 0; @media (max-width: $bp-s){ font-size: 1.5rem; width: 2rem; text-align: center; margin: 0 0 1rem 0; float: left; } } p{ margin: 0 0 1rem 0; font-size: 1rem; @media (max-width: $bp-s){ margin-left: 3rem; } } } blockquote{ position: relative; margin: 0; padding: 0; text-align: center; &:before{ display: inline-block; color: $color-primary; font-size: 2rem; content: '\201C'; } p{ margin: 0; display: inline; font-size: 1.5rem; @media (max-width: $bp-s){ font-size: 1.2rem; } } cite{ font-size: 1rem; display: block; margin: .5rem 0 0 1.2rem; @media (max-width: $bp-s){ font-size: .8rem; } &:before{ content: '–'; } } } footer{ background: $color-primary; color: #fff; padding: 2rem 0; text-align: center; font-size: .8rem; color: rgba(white,.4); ul{ margin: 0; padding: 0; list-style: none; li{ display: inline-block; a{ display: block; padding: .4rem .7rem; font-size: .9rem; text-decoration: none; color: rgba(white,.7); &:hover{ color: white; } } } } } .text--center{ text-align: center; } .text--left{ text-align: left; } .bg-image{ background: $color-primary; text-align: center; position: relative; z-index: 1; overflow: hidden; //text-shadow: 2px 0 5px black; &:before{ content: ''; display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; z-index: -1; background-size: 100%; background-attachment: fixed; filter: blur(3px); opacity: .8; transform: scale(1.1); } &.bg-image-2:before{ opacity: .6; background-position: center center; } } ================================================ FILE: Lab2/client/Landing/src/app/views/landing/landing.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-landing', templateUrl: './landing.component.html', styleUrls: ['./landing.component.scss'], }) export class LandingComponent implements OnInit { constructor(private router: Router) {} ngOnInit() {} register() { this.router.navigate(['register']); return false; } } ================================================ FILE: Lab2/client/Landing/src/app/views/register/register.component.html ================================================
Provision a new Tenant Login details will be sent to the email provided. Name Email Phone Address Plan
================================================ FILE: Lab2/client/Landing/src/app/views/register/register.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .tenant-form { display: flex; align-items: center; justify-content: center; } .wide-form { min-width: 150px; max-width: 500px; width: 100%; } .full-width { width: 100%; } .mat-form-field { width: 100%; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab2/client/Landing/src/app/views/register/register.component.ts ================================================ import { MatSnackBar } from '@angular/material/snack-bar'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.scss'], }) export class RegisterComponent implements OnInit { submitting: boolean = false; tenantForm: FormGroup = this.fb.group({ tenantName: ['', [Validators.required]], tenantEmail: ['', [Validators.email, Validators.required]], tenantTier: ['', [Validators.required]], tenantPhone: [''], tenantAddress: [''], }); constructor( private fb: FormBuilder, private _snackBar: MatSnackBar, private http: HttpClient ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; this.tenantForm.disable(); const tenant = { ...this.tenantForm.value, }; this.http .post(`${environment.apiGatewayUrl}/registration`, tenant) .subscribe({ next: () => { this.openErrorMessageSnackBar('Successfully created new tenant!'); this.tenantForm.reset(); this.tenantForm.enable(); this.submitting = false; }, error: (err) => { this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); this.tenantForm.enable(); this.submitting = false; }, }); } } ================================================ FILE: Lab2/client/Landing/src/assets/.gitkeep ================================================ ================================================ FILE: Lab2/client/Landing/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab2/client/Landing/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab2/client/Landing/src/environments/environment.ts ================================================ export const environment = { production: false, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab2/client/Landing/src/index.html ================================================ Landing ================================================ FILE: Lab2/client/Landing/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab2/client/Landing/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab2/client/Landing/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab2/client/Landing/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab2/client/Landing/src/styles.scss ================================================ ================================================ FILE: Lab2/client/Landing/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab2/client/Landing/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab2/client/Landing/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab2/client/Landing/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab2/scripts/deploy-updates.sh ================================================ #!/bin/bash cd ../server || exit # stop execution if cd fails rm -rf .aws-sam/ python3 -m pylint -E -d E0401 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi #Deploying shared services changes echo "Deploying shared services changes" echo Y | sam sync --stack-name serverless-saas --code --resource-id LambdaFunctions/CreateUserFunction --resource-id LambdaFunctions/RegisterTenantFunction --resource-id LambdaFunctions/GetTenantFunction -u cd ../scripts || exit ./geturl.sh ================================================ FILE: Lab2/scripts/deployment.sh ================================================ #!/bin/bash if [[ "$#" -eq 0 ]]; then echo "Invalid parameters" echo "Command to deploy client code: deployment.sh -c --email " echo "Command to deploy server code: deployment.sh -s --email " echo "Command to deploy server & client code: deployment.sh -s -c --email " exit 1 fi while [[ "$#" -gt 0 ]]; do case $1 in -s) server=1 ;; -c) client=1 ;; --email) email=$2 shift ;; *) echo "Unknown parameter passed: $1" exit 1 ;; esac shift done # During AWS hosted events using event engine tool # we pre-provision cloudfront and s3 buckets which hosts UI code. # So that it improves this labs total execution time. # Below code checks if cloudfront and s3 buckets are # pre-provisioned or not and then concludes if the workshop # is running in AWS hosted event through event engine tool or not. IS_RUNNING_IN_EVENT_ENGINE=false PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" IS_RUNNING_IN_EVENT_ENGINE=true ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) ADMIN_SITE_BUCKET=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminSiteBucket'].Value" --output text) LANDING_APP_SITE_BUCKET=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSiteBucket'].Value" --output text) fi if [[ $server -eq 1 ]]; then echo "Server code is getting deployed" cd ../server || exit # stop execution if cd fails REGION=$(aws configure get region) DEFAULT_SAM_S3_BUCKET=$(grep s3_bucket samconfig.toml | cut -d'=' -f2 | cut -d \" -f2) echo "aws s3 ls s3://$DEFAULT_SAM_S3_BUCKET" if ! aws s3 ls "s3://${DEFAULT_SAM_S3_BUCKET}"; then echo "S3 Bucket: $DEFAULT_SAM_S3_BUCKET specified in samconfig.toml is not readable. So creating a new S3 bucket and will update samconfig.toml with new bucket name." UUID=$(uuidgen | awk '{print tolower($0)}') SAM_S3_BUCKET=sam-bootstrap-bucket-$UUID aws s3 mb "s3://${SAM_S3_BUCKET}" --region "$REGION" aws s3api put-bucket-encryption \ --bucket "$SAM_S3_BUCKET" \ --server-side-encryption-configuration '{"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]}' if [[ $? -ne 0 ]]; then exit 1 fi # Updating all labs samconfig.toml with new bucket name ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab3/server/shared-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab3/server/tenant-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab4/server/shared-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab4/server/tenant-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab5/server/shared-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab5/server/tenant-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab6/server/shared-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab6/server/tenant-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab7/samconfig.toml fi echo "Validating server code using pylint" python3 -m pylint -E -d E0401 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi sam build -t template.yaml --use-container if [ "$IS_RUNNING_IN_EVENT_ENGINE" = true ]; then sam deploy --config-file samconfig.toml --region="$REGION" --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE AdminUserPoolCallbackURLParameter=$ADMIN_SITE_URL else sam deploy --config-file samconfig.toml --region="$REGION" --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE fi cd ../scripts || exit # stop execution if cd fails fi if [ "$IS_RUNNING_IN_EVENT_ENGINE" = false ]; then ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) ADMIN_SITE_BUCKET=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminSiteBucket'].OutputValue" --output text) LANDING_APP_SITE_BUCKET=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSiteBucket'].OutputValue" --output text) fi if [[ $client -eq 1 ]]; then if [[ -z "$email" ]]; then echo "Please provide email address to setup an admin user" echo "Note: Invoke script without parameters to know the list of script parameters" exit 1 fi echo "Client code is getting deployed" ADMIN_APIGATEWAYURL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminApi'].OutputValue" --output text) ADMIN_APPCLIENTID=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='CognitoOperationUsersUserPoolClientId'].OutputValue" --output text) ADMIN_USERPOOL_ID=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='CognitoOperationUsersUserPoolId'].OutputValue" --output text) ADMIN_USER_GROUP_NAME=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='CognitoAdminUserGroupName'].OutputValue" --output text) # Create admin-user in OperationUsers userpool with given input email address CREATE_ADMIN_USER=$(aws cognito-idp admin-create-user \ --user-pool-id "$ADMIN_USERPOOL_ID" \ --username admin-user \ --user-attributes Name=email,Value="$email" Name=email_verified,Value="True" Name=phone_number,Value="+11234567890" Name="custom:userRole",Value="SystemAdmin" Name="custom:tenantId",Value="system_admins" \ --desired-delivery-mediums EMAIL) echo "$CREATE_ADMIN_USER" # Add admin-user to admin user group ADD_ADMIN_USER_TO_GROUP=$(aws cognito-idp admin-add-user-to-group \ --user-pool-id "$ADMIN_USERPOOL_ID" \ --username admin-user \ --group-name "$ADMIN_USER_GROUP_NAME") echo "$ADD_ADMIN_USER_TO_GROUP" # Configuring admin UI echo "aws s3 ls s3://$ADMIN_SITE_BUCKET" if ! aws s3 ls "s3://${ADMIN_SITE_BUCKET}"; then echo "Error! S3 Bucket: $ADMIN_SITE_BUCKET not readable" exit 1 fi cd ../client/Admin || exit # stop execution if cd fails echo "Configuring environment for Admin Client" cat <./src/environments/environment.prod.ts export const environment = { production: true, apiUrl: '$ADMIN_APIGATEWAYURL', }; EoF cat <./src/environments/environment.ts export const environment = { production: false, apiUrl: '$ADMIN_APIGATEWAYURL', }; EoF cat <./src/aws-exports.ts const awsmobile = { "aws_project_region": "$REGION", "aws_cognito_region": "$REGION", "aws_user_pools_id": "$ADMIN_USERPOOL_ID", "aws_user_pools_web_client_id": "$ADMIN_APPCLIENTID", }; export default awsmobile; EoF npm install && npm run build echo "aws s3 sync --delete --cache-control no-store dist s3://${ADMIN_SITE_BUCKET}" aws s3 sync --delete --cache-control no-store dist "s3://${ADMIN_SITE_BUCKET}" if [[ $? -ne 0 ]]; then exit 1 fi echo "Completed configuring environment for Admin Client" # Configuring landing UI echo "aws s3 ls s3://${LANDING_APP_SITE_BUCKET}" if ! aws s3 ls "s3://${LANDING_APP_SITE_BUCKET}"; then echo "Error! S3 Bucket: $LANDING_APP_SITE_BUCKET not readable" exit 1 fi cd ../ cd Landing || exit # stop execution if cd fails echo "Configuring environment for Landing Client" cat <./src/environments/environment.prod.ts export const environment = { production: true, apiGatewayUrl: '$ADMIN_APIGATEWAYURL' }; EoF cat <./src/environments/environment.ts export const environment = { production: false, apiGatewayUrl: '$ADMIN_APIGATEWAYURL' }; EoF npm install && npm run build echo "aws s3 sync --delete --cache-control no-store dist s3://${LANDING_APP_SITE_BUCKET}" aws s3 sync --delete --cache-control no-store dist "s3://${LANDING_APP_SITE_BUCKET}" if [[ $? -ne 0 ]]; then exit 1 fi echo "Completed configuring environment for Landing Client" echo "Successfully completed deploying Admin UI and Landing UI" fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" ================================================ FILE: Lab2/scripts/geturl.sh ================================================ #!/bin/bash PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) else ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" ================================================ FILE: Lab2/server/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Lab2/server/OrderService/order_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Order: key='' def __init__(self, orderId, orderName, orderProducts): self.orderId = orderId self.orderName = orderName self.orderProducts = orderProducts class OrderProduct: def __init__(self, productId, price, quantity): self.productId = productId self.price = price self.quantity = quantity ================================================ FILE: Lab2/server/OrderService/order_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import order_service_dal from decimal import Decimal from types import SimpleNamespace from aws_lambda_powertools import Tracer def get_order(event, context): logger.info("Request received to get a order") params = event['pathParameters'] orderId = params['id'] order = order_service_dal.get_order(event, orderId) logger.info("Request completed to get a order") return utils.generate_response(order) def create_order(event, context): logger.info("Request received to create a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) order = order_service_dal.create_order(event, payload) logger.info("Request completed to create a order") return utils.generate_response(order) def update_order(event, context): logger.info("Request received to update a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] orderId = params['id'] order = order_service_dal.update_order(event, payload, orderId) logger.info("Request completed to update a order") return utils.generate_response(order) def delete_order(event, context): logger.info("Request received to delete a order") params = event['pathParameters'] orderId = params['id'] response = order_service_dal.delete_order(event, orderId) logger.info("Request completed to delete a order") return utils.create_success_response("Successfully deleted the order") def get_orders(event, context): logger.info("Request received to get all orders") response = order_service_dal.get_orders(event) logger.info("Request completed to get all orders") return utils.generate_response(response) ================================================ FILE: Lab2/server/OrderService/order_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import boto3 from botocore.exceptions import ClientError import uuid from order_models import Order import json import utils from types import SimpleNamespace import logger import random table_name = os.environ['ORDER_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) def get_order(event, orderId): try: response = table.get_item(Key={'orderId': orderId}) item = response['Item'] order = Order(item['orderId'], item['orderName'], item['orderProducts']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a order', e) else: return order def delete_order(event, orderId): try: response = table.delete_item(Key={'orderId': orderId}) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a order', e) else: logger.info("DeleteItem succeeded:") return response def create_order(event, payload): order = Order(str(uuid.uuid4()), payload.orderName, payload.orderProducts) try: response = table.put_item(Item={ 'orderId': order.orderId, 'orderName': order.orderName, 'orderProducts': get_order_products_dict(order.orderProducts) }) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a order', e) else: logger.info("PutItem succeeded:") return order def update_order(event, payload, orderId): try: order = Order(orderId,payload.orderName, payload.orderProducts) response = table.update_item(Key={'orderId': order.orderId}, UpdateExpression="set orderName=:orderName, " +"orderProducts=:orderProducts", ExpressionAttributeValues={ ':orderName': order.orderName, ':orderProducts': get_order_products_dict(order.orderProducts) }, ReturnValues="UPDATED_NEW") except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a order', e) else: logger.info("UpdateItem succeeded:") return order def get_orders(event): orders = [] try: response = table.scan() if (len(response['Items']) > 0): for item in response['Items']: order = Order(item['orderId'], item['orderName'], item['orderProducts']) orders.append(order) except ClientError as e: logger.error("Error getting all orders") raise Exception('Error getting all orders', e) else: logger.info("Get orders succeeded") return orders def get_order_products_dict(orderProducts): orderProductList = [] for i in range(len(orderProducts)): product = orderProducts[i] orderProductList.append(vars(product)) return orderProductList ================================================ FILE: Lab2/server/OrderService/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Lab2/server/ProductService/product_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Product: key ='' def __init__(self, productId, sku, name, price, category): self.productId = productId self.sku = sku self.name = name self.price = price self.category = category class Category: def __init__(self, id, name): self.id = id self.name = name ================================================ FILE: Lab2/server/ProductService/product_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import product_service_dal from decimal import Decimal from types import SimpleNamespace def get_product(event, context): logger.info("Request received to get a product") params = event['pathParameters'] productId = params['id'] product = product_service_dal.get_product(event, productId) logger.info("Request completed to get a product") return utils.generate_response(product) def create_product(event, context): logger.info("Request received to create a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) logger.info(payload) product = product_service_dal.create_product(event, payload) logger.info("Request completed to create a product") return utils.generate_response(product) def update_product(event, context): logger.info("Request received to update a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] product = product_service_dal.update_product(event, payload, key) logger.info("Request completed to update a product") return utils.generate_response(product) def delete_product(event, context): logger.info("Request received to delete a product") params = event['pathParameters'] key = params['id'] response = product_service_dal.delete_product(event, key) logger.info("Request completed to delete a product") return utils.create_success_response("Successfully deleted the product") def get_products(event, context): logger.info("Request received to get all products") response = product_service_dal.get_products(event) logger.info("Request completed to get all products") return utils.generate_response(response) ================================================ FILE: Lab2/server/ProductService/product_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import boto3 from botocore.exceptions import ClientError import uuid import logger from product_models import Product from types import SimpleNamespace from boto3.dynamodb.conditions import Key table_name = os.environ['PRODUCT_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) def get_product(event, productId): try: response = table.get_item(Key={'productId': productId}) item = response['Item'] product = Product(item['productId'], item['sku'], item['name'], item['price'], item['category']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a product', e) else: logger.info("GetItem succeeded:"+ str(product)) return product def delete_product(event, productId): try: response = table.delete_item(Key={'productId': productId}) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a product', e) else: logger.info("DeleteItem succeeded:") return response def create_product(event, payload): product = Product(str(uuid.uuid4()), payload.sku,payload.name, payload.price, payload.category) try: response = table.put_item( Item= { 'productId': product.productId, 'sku': product.sku, 'name': product.name, 'price': product.price, 'category': product.category } ) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: logger.info("PutItem succeeded:") return product def update_product(event, payload, productId): try: product = Product(productId,payload.sku, payload.name, payload.price, payload.category) response = table.update_item(Key={'productId': product.productId}, UpdateExpression="set sku=:sku, #n=:productName, price=:price, category=:category", ExpressionAttributeNames= {'#n':'name'}, ExpressionAttributeValues={ ':sku': product.sku, ':productName': product.name, ':price': product.price, ':category': product.category }, ReturnValues="UPDATED_NEW") except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a product', e) else: logger.info("UpdateItem succeeded:") return product def get_products(event): products =[] try: response = table.scan() if (len(response['Items']) > 0): for item in response['Items']: product = Product(item['productId'], item['sku'], item['name'], item['price'], item['category']) products.append(product) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting all products', e) else: logger.info("Get products succeeded") return products ================================================ FILE: Lab2/server/ProductService/requirements.txt ================================================ requests pytest-mock aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Lab2/server/README.md ================================================ if using EE sam build --use-container && sam package --output-template-file packaged.yaml --s3-bucket aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx --region us-west-2 sam deploy --template-file packaged.yaml --config-file samconfig.toml Normally sam build -t template.yaml --use-container sam deploy --config-file samconfig.toml ================================================ FILE: Lab2/server/Resources/requirements.txt ================================================ python-jose[cryptography] ================================================ FILE: Lab2/server/Resources/shared_service_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import utils region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) userpool_id = user_pool_operation_user appclient_id = app_client_operation_user #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] policy.allowAllMethods() authResponse = policy.build() context = { 'userName': user_name, 'userPoolId': userpool_id } authResponse['context'] = context return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Lab2/server/TenantManagementService/requirements.txt ================================================ requests aws_requests_auth ================================================ FILE: Lab2/server/TenantManagementService/tenant-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import json import boto3 from boto3.dynamodb.conditions import Key import urllib.parse import utils from botocore.exceptions import ClientError import logger import requests region = os.environ['AWS_REGION'] dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') #This method has been locked down to be only called from tenant registration service def create_tenant(event, context): tenant_details = json.loads(event['body']) try: response = table_tenant_details.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'tenantName' : tenant_details['tenantName'], 'tenantAddress': tenant_details['tenantAddress'], 'tenantEmail': tenant_details['tenantEmail'], 'tenantPhone': tenant_details['tenantPhone'], 'tenantTier': tenant_details['tenantTier'], 'isActive': True } ) except Exception as e: raise Exception('Error creating a new tenant', e) else: return utils.create_success_response("Tenant Created") def get_tenants(event, context): try: response = table_tenant_details.scan() except Exception as e: raise Exception('Error getting all tenants', e) else: return utils.generate_response(response['Items']) def update_tenant(event, context): tenant_details = json.loads(event['body']) tenant_id = event['pathParameters']['tenantid'] logger.info("Request received to update tenant") response_update = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set tenantName = :tenantName, tenantAddress = :tenantAddress, tenantEmail = :tenantEmail, tenantPhone = :tenantPhone, tenantTier=:tenantTier", ExpressionAttributeValues={ ':tenantName' : tenant_details['tenantName'], ':tenantAddress': tenant_details['tenantAddress'], ':tenantEmail': tenant_details['tenantEmail'], ':tenantPhone': tenant_details['tenantPhone'], ':tenantTier': tenant_details['tenantTier'] }, ReturnValues="UPDATED_NEW" ) logger.info(response_update) logger.info("Request completed to update tenant") return utils.create_success_response("Tenant Updated") #TODO: Implement the below method def get_tenant(event, context): pass def deactivate_tenant(event, context): url_disable_users = os.environ['DISABLE_USERS_BY_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) tenant_id = event['pathParameters']['tenantid'] logger.info("Request received to deactivate tenant") response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': False }, ReturnValues="ALL_NEW" ) logger.info(response) update_user_response = __invoke_disable_users(headers, auth, host, stage_name, url_disable_users, tenant_id) logger.info(update_user_response) logger.info("Request completed to deactivate tenant") return utils.create_success_response("Tenant Deactivated") def activate_tenant(event, context): url_enable_users = os.environ['ENABLE_USERS_BY_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) tenant_id = event['pathParameters']['tenantid'] logger.info("Request received to activate tenant") response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': True }, ReturnValues="ALL_NEW" ) logger.info(response) update_user_response = __invoke_enable_users(headers, auth, host, stage_name, url_enable_users, tenant_id) logger.info(update_user_response) logger.info("Request completed to activate tenant") return utils.create_success_response("Tenant activated") def __invoke_disable_users(headers, auth, host, stage_name, invoke_url, tenant_id): try: url = ''.join(['https://', host, '/', stage_name, invoke_url, '/', tenant_id]) response = requests.put(url, auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while disabling users for the tenant') except Exception as e: logger.error('Error occured while disabling users for the tenant') raise Exception('Error occured while disabling users for the tenant', e) else: return "Success invoking disable users" def __invoke_enable_users(headers, auth, host, stage_name, invoke_url, tenant_id): try: url = ''.join(['https://', host, '/', stage_name, invoke_url, '/', tenant_id]) response = requests.put(url, auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while enabling users for the tenant') except Exception as e: logger.error('Error occured while enabling users for the tenant') raise Exception('Error occured while enabling users for the tenant', e) else: return "Success invoking enable users" class TenantInfo: def __init__(self, tenant_name, tenant_address, tenant_email, tenant_phone): self.tenant_name = tenant_name self.tenant_address = tenant_address self.tenant_email = tenant_email self.tenant_phone = tenant_phone ================================================ FILE: Lab2/server/TenantManagementService/tenant-registration.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import utils import uuid import logger import requests import re region = os.environ['AWS_REGION'] create_tenant_admin_user_resource_path = os.environ['CREATE_TENANT_ADMIN_USER_RESOURCE_PATH'] create_tenant_resource_path = os.environ['CREATE_TENANT_RESOURCE_PATH'] lambda_client = boto3.client('lambda') #TODO: Implement this method def register_tenant(event, context): pass def __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_admin_user_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while calling the create tenant admin user service') raise Exception('Error occured while calling the create tenant admin user service', e) else: return response_json def __create_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_resource_path]) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while creating the tenant record in table') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json ================================================ FILE: Lab2/server/TenantManagementService/user-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import sys import logger import utils from boto3.dynamodb.conditions import Key client = boto3.client('cognito-idp') dynamodb = boto3.resource('dynamodb') table_tenant_user_map = dynamodb.Table('ServerlessSaaS-TenantUserMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_id = os.environ['TENANT_USER_POOL_ID'] def create_tenant_admin_user(event, context): logger.info(event) app_client_id = os.environ['TENANT_APP_CLIENT_ID'] tenant_details = json.loads(event['body']) tenant_id = tenant_details['tenantId'] logger.info(tenant_details) user_mgmt = UserManagement() #Add tenant admin now based upon user pool tenant_user_group_response = user_mgmt.create_user_group(user_pool_id,tenant_id,"User group for tenant {0}".format(tenant_id)) tenant_admin_user_name = 'tenant-admin-{0}'.format(tenant_details['tenantId']) create_tenant_admin_response = user_mgmt.create_tenant_admin(user_pool_id, tenant_admin_user_name, tenant_details) add_tenant_admin_to_group_response = user_mgmt.add_user_to_group(user_pool_id, tenant_admin_user_name, tenant_user_group_response['Group']['GroupName']) tenant_user_mapping_response = user_mgmt.create_user_tenant_mapping(tenant_admin_user_name,tenant_id) response = {"userPoolId": user_pool_id, "appClientId": app_client_id, "tenantAdminUserName": tenant_admin_user_name} return utils.create_success_response(response) #only tenant admin can create users #TODO: Implement the below method def create_user(event, context): pass def get_users(event, context): users = [] logger.info("Request received to get users") logger.info(event) response = client.list_users( UserPoolId=user_pool_id ) logger.info(response) num_of_users = len(response['Users']) if (num_of_users > 0): for user in response['Users']: user_info = UserInfo() for attr in user["Attributes"]: if(attr["Name"] == "custom:tenantId"): user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] user_info.enabled = user["Enabled"] user_info.created = user["UserCreateDate"] user_info.modified = user["UserLastModifiedDate"] user_info.status = user["UserStatus"] user_info.user_name = user["Username"] users.append(user_info) # return an empty list when there are no users otherwise will result in API Gateway error return utils.generate_response(users) def get_user(event, context): user_name = event['pathParameters']['username'] logger.info("Request received to get user") user_info = get_user_info(user_pool_id, user_name) logger.info("Request completed to get new user ") return utils.create_success_response(user_info.__dict__) def update_user(event, context): user_details = json.loads(event['body']) user_name = event['pathParameters']['username'] logger.info("Request received to update user") response = client.admin_update_user_attributes( Username=user_name, UserPoolId=user_pool_id, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] } ] ) logger.info(response) logger.info("Request completed to update user") return utils.create_success_response("user updated") def disable_user(event, context): user_name = event['pathParameters']['username'] logger.info("Request received to disable new user") response = client.admin_disable_user( Username=user_name, UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to disable new user") return utils.create_success_response("User disabled") #This method uses IAM Authorization and protected using a resource policy. This method is also invoked async def disable_users_by_tenant(event, context): logger.info("Request received to disable users by tenant") logger.info(event) tenantid_to_update = event['tenantid'] filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_disable_user( Username=user['userName'], UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to disable users") return utils.create_success_response("Users disabled") #This method uses IAM Authorization and protected using a resource policy. This method is also invoked async def enable_users_by_tenant(event, context): logger.info("Request received to enable users by tenant") logger.info(event) tenantid_to_update = event['tenantid'] filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_enable_user( Username=user['userName'], UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to enable users") return utils.create_success_response("Users enables") def get_user_info(user_pool_id, user_name): response = client.admin_get_user( UserPoolId=user_pool_id, Username=user_name ) logger.info(response) user_info = UserInfo() user_info.user_name = response["Username"] for attr in response["UserAttributes"]: if(attr["Name"] == "custom:tenantId"): user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] logger.info(user_info) return user_info class UserManagement: def create_user_group(self, user_pool_id, group_name, group_description): response = client.create_group( GroupName=group_name, UserPoolId=user_pool_id, Description= group_description, Precedence=0 ) return response def create_tenant_admin(self, user_pool_id, tenant_admin_user_name, user_details): response = client.admin_create_user( Username=tenant_admin_user_name, UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['tenantEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': 'TenantAdmin' }, { 'Name': 'custom:tenantId', 'Value': user_details['tenantId'] } ] ) return response def add_user_to_group(self, user_pool_id, user_name, group_name): response = client.admin_add_user_to_group( UserPoolId=user_pool_id, Username=user_name, GroupName=group_name ) return response def create_user_tenant_mapping(self, user_name, tenant_id): response = table_tenant_user_map.put_item( Item={ 'tenantId': tenant_id, 'userName': user_name } ) return response class UserInfo: def __init__(self, user_name=None, tenant_id=None, user_role=None, email=None, status=None, enabled=None, created=None, modified=None): self.user_name = user_name self.tenant_id = tenant_id self.user_role = user_role self.email = email self.status = status self.enabled = enabled self.created = created self.modified = modified ================================================ FILE: Lab2/server/layers/logger.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from aws_lambda_powertools import Logger logger = Logger() """Log info messages """ def info(log_message): logger.info (log_message) """Log error messages """ def error(log_message): logger.error (log_message) ================================================ FILE: Lab2/server/layers/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] simplejson jsonpickle aws_requests_auth ================================================ FILE: Lab2/server/layers/utils.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import jsonpickle import simplejson import boto3 from aws_requests_auth.aws_auth import AWSRequestsAuth from enum import Enum class StatusCodes(Enum): SUCCESS = 200 UN_AUTHORIZED = 401 NOT_FOUND = 404 def create_success_response(message): return { "statusCode": StatusCodes.SUCCESS.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def generate_response(inputObject): return { "statusCode": 200, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": encode_to_json_object(inputObject), } def encode_to_json_object(inputObject): jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True) jsonpickle.set_preferred_backend('simplejson') return jsonpickle.encode(inputObject, unpicklable=False, use_decimal=True) def get_auth(host, region): session = boto3.Session() credentials = session.get_credentials() auth = AWSRequestsAuth(aws_access_key=credentials.access_key, aws_secret_access_key=credentials.secret_key, aws_token=credentials.token, aws_host=host, aws_region=region, aws_service='execute-api') return auth def get_headers(event): return event['headers'] ================================================ FILE: Lab2/server/nested_templates/apigateway.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway, apis, api keys and usage plan as part of bootstrap Parameters: StageName: Type: String RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String Resources: ApiGatewayCloudWatchLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub apigateway-cloudwatch-publish-role-${AWS::Region} Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole ApiGatewayAttachCloudwatchLogArn: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchLogRole.Arn AdminApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/api-gateway/access-logs-serverless-saas-admin-api RetentionInDays: 30 AdminApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: "/*" HttpMethod: "*" Auth: ResourcePolicy: CustomStatements: - Effect: Allow Principal: "*" Action: "execute-api:Invoke" Resource: ["execute-api:/*/*/*"] - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/tenant" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/user/tenant-admin" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref RegisterTenantLambdaExecutionRoleArn - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/disable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/enable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn AccessLogSetting: DestinationArn: !GetAtt AdminApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ["", ["serverless-saas-admin-api-", !Ref "AWS::Region"]] basePath: !Join ["", ["/", !Ref StageName]] schemes: - https paths: /registration: post: summary: Register a new tenant description: Register a new tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref RegisterTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant/activation/{tenantid}: put: security: - api_key: [] - Authorizer: [] summary: Activate an existing tenant description: Activate an existing tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ActivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenants: get: summary: Returns all tenants description: Returns all tenants produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantsFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant: post: summary: Creates a tenant description: Creates a tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/{tenantid}: get: summary: Returns a tenant description: Return a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Disables a tenant description: Disables a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeactivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy put: summary: Updates a tenant description: Updates a tenant produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user/{username}: get: summary: Returns a user description: Return a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUserFunctionArn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Diables a user description: Disable a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUserFunctionArn - /invocations httpMethod: POST type: aws_proxy /user/tenant-admin: post: summary: Creates a tenant admin user description: Creates a tenant admin user produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantAdminUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user: post: summary: Create a user description: Create a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users: get: summary: Get all users by tenantId description: Get all users by tenantId produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUsersFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/disable/{tenantid}: put: summary: disable users by tenant id description: disable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' requestTemplates: application/json: "{\"tenantid\": \"$input.params('tenantid')\" }" options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/enable/{tenantid}: put: summary: enable users by tenant id description: enable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref EnableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' requestTemplates: application/json: "{ \"tenantid\": \"$input.params('tenantid')\" }" options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: sigv4Reference: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "awsSigv4" Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref AuthorizerFunctionArn - /invocations authorizerResultTtlInSeconds: 60 type: "token" StageName: prod Outputs: AdminApiGatewayApi: Value: !Ref AdminApiGatewayApi ================================================ FILE: Lab2/server/nested_templates/apigateway_lambdapermissions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway and apis as part of bootstrap Parameters: RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String AdminApiGatewayApi: Type: String Resources: #provide api gateway permissions to call lambda functions RegisterTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref RegisterTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateTenantAdminUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantAdminUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] EnableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref EnableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUsersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUsersFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref AuthorizerFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*" ]] CreateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantsFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeactivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeactivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ActivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ActivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ================================================ FILE: Lab2/server/nested_templates/cognito.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup cognito as part of bootstrap Parameters: AdminEmailParameter: Type: String Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Description: "Enter the role name for system admin" AdminUserPoolCallbackURLParameter: Type: String Description: "Enter Admin Management userpool call back url" Resources: CognitoUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: PooledTenant-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Thanks for signing up. " - "You username is {username} and temporary password is {####}" EmailSubject: !Join - "" - - "Your temporary password for tenant UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSClient GenerateSecret: false UserPoolId: !Ref CognitoUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - https://example.com LogoutURLs: - https://example.com AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [pooledtenant-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoUserPool CognitoOperationUsersUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: OperationUsers-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into admin UI application at " - "https://" - !Ref AdminUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for admin UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoOperationUsersUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSOperationUsersPoolClient GenerateSecret: false UserPoolId: !Ref CognitoOperationUsersUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoOperationUsersUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [operationsusers-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUserGroup: Type: AWS::Cognito::UserPoolGroup Properties: GroupName: SystemAdmins Description: Admin user group Precedence: 0 UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUser: Type: AWS::Cognito::UserPoolUser Properties: Username: admin DesiredDeliveryMediums: - EMAIL ForceAliasCreation: true UserAttributes: - Name: email Value: !Ref AdminEmailParameter - Name: custom:tenantId Value: system_admins - Name: custom:userRole Value: !Ref SystemAdminRoleNameParameter UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAddUserToGroup: Type: AWS::Cognito::UserPoolUserToGroupAttachment Properties: GroupName: !Ref CognitoAdminUserGroup Username: !Ref CognitoAdminUser UserPoolId: !Ref CognitoOperationUsersUserPool Outputs: CognitoUserPoolId: Value: !Ref CognitoUserPool CognitoUserPoolClientId: Value: !Ref CognitoUserPoolClient CognitoOperationUsersUserPoolId: Value: !Ref CognitoOperationUsersUserPool CognitoOperationUsersUserPoolClientId: Value: !Ref CognitoOperationUsersUserPoolClient CognitoOperationUsersUserPoolProviderURL: Value: !GetAtt CognitoOperationUsersUserPool.ProviderURL CognitoAdminUserGroupName: Value: !Ref CognitoAdminUserGroup ================================================ FILE: Lab2/server/nested_templates/lambdafunctions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy lambda functions as part of bootstrap Parameters: CognitoOperationUsersUserPoolId: Type: String CognitoOperationUsersUserPoolClientId: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String TenantDetailsTableArn: Type: String TenantUserMappingTableArn: Type: String Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies Description: Utilities for project ContentUri: ../layers/ CompatibleRuntimes: - python3.9 LicenseInfo: "MIT" RetentionPolicy: Retain Metadata: BuildMethod: python3.9 AuthorizerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: authorizer-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: authorizer-execution-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:List* Resource: - !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn SharedServicesAuthorizerFunction: Type: AWS::Serverless::Function DependsOn: AuthorizerExecutionRole Properties: CodeUri: ../Resources/ Handler: shared_service_authorizer.lambda_handler Runtime: python3.9 Role: !GetAtt AuthorizerExecutionRole.Arn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !Ref CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !Ref CognitoOperationUsersUserPoolClientId #Create user pool for the tenant TenantUserPoolLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-userpool-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-userpool-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn - Effect: Allow Action: - dynamodb:GetItem - dynamodb:Query Resource: - !Ref TenantUserMappingTableArn CreateUserLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub create-user-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-user-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:PutItem Resource: - !Ref TenantUserMappingTableArn - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn CreateTenantAdminUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_tenant_admin_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId TENANT_APP_CLIENT_ID: !Ref CognitoUserPoolClientId POWERTOOLS_SERVICE_NAME: "UserManagement.CreateTenantAdmin" #User management CreateUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.CreateUser" UpdateUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.update_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.UpdateUser" DisableUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUser" DisableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUsersByTenant" EnableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.enable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.EnableUsersByTenant" GetUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.GetUser" GetUsersFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_users Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.GetUsers" #Tenant Management TenantManagementLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-management-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-tenant-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:Scan - dynamodb:Query Resource: - !Ref TenantDetailsTableArn - !Join ["", [!Ref TenantDetailsTableArn, '/index/*']] CreateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.create_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.CreateTenant" ActivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.activate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.ActivateTenant" ENABLE_USERS_BY_TENANT: "/users/enable" PROVISION_TENANT: "/provisioning/" GetTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.GetTenant" DeactivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.deactivate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.DeactivateTenant" DEPROVISION_TENANT: "/provisioning/" DISABLE_USERS_BY_TENANT: "/users/disable" UpdateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.update_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.UpdateTenant" GetTenantsFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenants Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers #Tenant Registration RegisterTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-registration-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess RegisterTenantFunction: Type: AWS::Serverless::Function DependsOn: RegisterTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-registration.register_tenant Runtime: python3.9 Role: !GetAtt RegisterTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: # Need to find a better way than hard coding resource paths CREATE_TENANT_ADMIN_USER_RESOURCE_PATH: "/user/tenant-admin" CREATE_TENANT_RESOURCE_PATH: "/tenant" PROVISION_TENANT_RESOURCE_PATH: "/provisioning" POWERTOOLS_SERVICE_NAME: "TenantRegistration.RegisterTenant" Outputs: RegisterTenantLambdaExecutionRoleArn: Value: !GetAtt RegisterTenantLambdaExecutionRole.Arn TenantManagementLambdaExecutionRoleArn: Value: !GetAtt TenantManagementLambdaExecutionRole.Arn RegisterTenantFunctionArn: Value: !GetAtt RegisterTenantFunction.Arn ActivateTenantFunctionArn: Value: !GetAtt ActivateTenantFunction.Arn GetTenantsFunctionArn: Value: !GetAtt GetTenantsFunction.Arn CreateTenantFunctionArn: Value: !GetAtt CreateTenantFunction.Arn GetTenantFunctionArn: Value: !GetAtt GetTenantFunction.Arn DeactivateTenantFunctionArn: Value: !GetAtt DeactivateTenantFunction.Arn UpdateTenantFunctionArn: Value: !GetAtt UpdateTenantFunction.Arn GetUsersFunctionArn: Value: !GetAtt GetUsersFunction.Arn GetUserFunctionArn: Value: !GetAtt GetUserFunction.Arn UpdateUserFunctionArn: Value: !GetAtt UpdateUserFunction.Arn DisableUserFunctionArn: Value: !GetAtt DisableUserFunction.Arn CreateTenantAdminUserFunctionArn: Value: !GetAtt CreateTenantAdminUserFunction.Arn CreateUserFunctionArn: Value: !GetAtt CreateUserFunction.Arn DisableUsersByTenantFunctionArn: Value: !GetAtt DisableUsersByTenantFunction.Arn EnableUsersByTenantFunctionArn: Value: !GetAtt EnableUsersByTenantFunction.Arn SharedServicesAuthorizerFunctionArn: Value: !GetAtt SharedServicesAuthorizerFunction.Arn ================================================ FILE: Lab2/server/nested_templates/tables.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to create dynamodb tables as part of bootstrap Resources: TenantDetailsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: tenantName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: ServerlessSaas-TenantConfig KeySchema: - AttributeName: tenantName KeyType: HASH Projection: NonKeyAttributes: - userPoolId - appClientId - apiGatewayUrl ProjectionType: INCLUDE TableName: ServerlessSaaS-TenantDetails TenantUserMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: userName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH - AttributeName: userName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantUserMapping GlobalSecondaryIndexes: - IndexName: UserName KeySchema: - AttributeName: userName KeyType: HASH - AttributeName: tenantId KeyType: RANGE Projection: ProjectionType: ALL Outputs: TenantDetailsTableArn: Value: !GetAtt TenantDetailsTable.Arn TenantDetailsTableName: Value: !Ref TenantDetailsTable TenantUserMappingTableArn: Value: !GetAtt TenantUserMappingTable.Arn TenantUserMappingTableName: Value: !Ref TenantUserMappingTable ================================================ FILE: Lab2/server/nested_templates/userinterface.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: IsCloudFrontAndS3PreProvisioned: Type: String Default: false Description: "Tells if cloudfront and s3 buckets are pre-provisioned or not. They get pre-provisioned when the workshop is running as a part of AWS Event through AWS event engine tool." Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref IsCloudFrontAndS3PreProvisioned, true] ] Resources: CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: IsNotRunningInEventEngine Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distributions" AdminAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AdminAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AdminAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AdminAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId AdminAppSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: #Aliases: # - !Sub 'admin.${CustomDomainName}' CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS CachedMethods: - GET - HEAD - OPTIONS Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: adminapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt AdminAppBucket.RegionalDomainName Id: adminapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' LandingAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True LandingAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref LandingAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${LandingAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId LandingApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: landingapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'LandingAppBucket.RegionalDomainName' Id: landingapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: AdminBucket: Description: The name of the bucket for uploading the Admin Management site to Value: !Ref AdminAppBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt AdminAppSite.DomainName Condition: IsNotRunningInEventEngine LandingAppBucket: Description: The name of the bucket for uploading the Landing site to Value: !Ref LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt LandingApplicationSite.DomainName Condition: IsNotRunningInEventEngine ================================================ FILE: Lab2/server/samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas" s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx" s3_prefix = "serverless-saas" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" cached="true" parallel="true" ================================================ FILE: Lab2/server/template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Lab2 - Bootstrap common resources Parameters: AdminEmailParameter: Type: String Default: "test@test.com" Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Default: "SystemAdmin" Description: "Enter the role name for system admin" StageName: Type: String Default: "prod" Description: "Stage Name for the api" EventEngineParameter: Type: String Default: false Description: "Tells if this workshop is running as a part of AWS Event through AWS EventEngine or not" AdminUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Admin Management userpool call back url" Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref EventEngineParameter, true] ] Resources: DynamoDBTables: Type: AWS::Serverless::Application Properties: Location: nested_templates/tables.yaml #Create cloudfront and s3 for UI Cde UserInterface: Type: AWS::Serverless::Application Properties: Location: nested_templates/userinterface.yaml Parameters: IsCloudFrontAndS3PreProvisioned: !Ref EventEngineParameter Cognito: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/cognito.yaml Parameters: AdminEmailParameter: !Ref AdminEmailParameter SystemAdminRoleNameParameter: !Ref SystemAdminRoleNameParameter AdminUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.AdminAppSite, !Ref AdminUserPoolCallbackURLParameter] LambdaFunctions: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/lambdafunctions.yaml Parameters: CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId CognitoOperationUsersUserPoolId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId CognitoOperationUsersUserPoolClientId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId TenantDetailsTableArn: !GetAtt DynamoDBTables.Outputs.TenantDetailsTableArn TenantUserMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantUserMappingTableArn APIs: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway.yaml Parameters: StageName: !Ref StageName RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn APIGatewayLambdaPermissions: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway_lambdapermissions.yaml Parameters: RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn AdminApiGatewayApi: !GetAtt APIs.Outputs.AdminApiGatewayApi Outputs: AdminApi: Description: "API Gateway endpoint URL for Admin API" Value: !Join ["", ["https://", !GetAtt APIs.Outputs.AdminApiGatewayApi, ".execute-api.", !Ref "AWS::Region", ".amazonaws.com/", !Ref StageName]] AdminSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant administration application Value: !GetAtt UserInterface.Outputs.AdminBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt UserInterface.Outputs.AdminAppSite Condition: IsNotRunningInEventEngine LandingApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the landing application Value: !GetAtt UserInterface.Outputs.LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt UserInterface.Outputs.LandingApplicationSite Condition: IsNotRunningInEventEngine CognitoOperationUsersUserPoolProviderURL: Description: The Admin Management userpool provider url Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolProviderURL CognitoOperationUsersUserPoolClientId: Description: The Admin Management userpool client id Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId CognitoOperationUsersUserPoolId: Description: The user pool id of Admin Management userpool Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId CognitoAdminUserGroupName: Description: The Admin Management userpool admin user group name Value: !GetAtt Cognito.Outputs.CognitoAdminUserGroupName ================================================ FILE: Lab3/client/Admin/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab3/client/Admin/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab3/client/Admin/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab3/client/Admin/README.md ================================================ # Admin This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab3/client/Admin/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "admin": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "admin:build:production" }, "development": { "browserTarget": "admin:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "admin:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab3/client/Admin/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab3/client/Admin/package.json ================================================ { "name": "admin", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/flex-layout": "~14.0.0-beta.40", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.1.3", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab3/client/Admin/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Tenants', url: '/tenants', icon: 'groups', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Lab3/client/Admin/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: '', component: NavComponent, data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'tenants', loadChildren: () => import('./views/tenants/tenants.module').then((m) => m.TenantsModule), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab3/client/Admin/src/app/app.component.scss ================================================ ================================================ FILE: Lab3/client/Admin/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Lab3/client/Admin/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { HttpInterceptorProviders } from './interceptors'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; @NgModule({ declarations: [AppComponent, NavComponent, AuthComponent], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, HttpClientModule, HttpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab3/client/Admin/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Lab3/client/Admin/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const HttpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Lab3/client/Admin/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Lab3/client/Admin/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Lab3/client/Admin/src/app/nav/nav.component.css ================================================ .sidenav-container { height: 100%; } .sidenav-content-container { background-color: lightgray; } .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .mat-toolbar.mat-primary { position: sticky; top: 0; z-index: 1; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .spinner-container { height: 80%; display: flex; justify-content: center; align-items: center; box-sizing: border-box; } ================================================ FILE: Lab3/client/Admin/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ username$ | async }}
================================================ FILE: Lab3/client/Admin/src/app/nav/nav.component.spec.ts ================================================ import { LayoutModule } from '@angular/cdk/layout'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { NavComponent } from './nav.component'; describe('NavComponent', () => { let component: NavComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [NavComponent], imports: [ NoopAnimationsModule, LayoutModule, MatButtonModule, MatIconModule, MatListModule, MatSidenavModule, MatToolbarModule, ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(NavComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should compile', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: Lab3/client/Admin/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.css'], }) export class NavComponent implements OnInit { tenantName = ''; loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router ) { this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => err); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map((sesh) => sesh && sesh.isValid()) ); const token$ = session$.pipe(map((sesh) => sesh && sesh.getIdToken())); this.username$ = token$.pipe( map((t) => t && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } } ================================================ FILE: Lab3/client/Admin/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Lab3/client/Admin/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Lab3/client/Admin/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Lab3/client/Admin/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Lab3/client/Admin/src/app/views/dashboard/dashboard.component.html ================================================

Traffic

December 2020
================================================ FILE: Lab3/client/Admin/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } .chart-wrapper { padding-right: 20px; position: relative; margin: auto; height: 80vh; width: 80vw; } ================================================ FILE: Lab3/client/Admin/src/app/views/dashboard/dashboard.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ChartConfiguration, ChartType } from 'chart.js'; import { TenantsService } from '../tenants/tenants.service'; interface DataSet { label: string; data: number[]; } interface ChartData { tenantId: string; dataSet: DataSet[]; totalOrders: number; } @Component({ templateUrl: 'dashboard.component.html', styleUrls: ['./dashboard.component.scss'], selector: 'app-dashboard', }) export class DashboardComponent implements OnInit { constructor(private tenantSvc: TenantsService) {} data: ChartData[] = []; // lineChart public lineChartElements = 27; public lineChartData1: Array = []; public lineChartData2: Array = []; public lineChartData3: Array = []; public lineChartData: Array = [ { data: this.lineChartData1, label: 'Current', backgroundColor: 'rgba(148,159,177,0.2)', borderColor: 'rgba(148,159,177,1)', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, { data: this.lineChartData2, label: 'Previous', backgroundColor: 'rgba(77,83,96,0.2)', borderColor: 'rgba(77,83,96,1)', pointBackgroundColor: 'rgba(77,83,96,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(77,83,96,1)', fill: 'origin', }, { data: this.lineChartData3, label: 'BEP', backgroundColor: 'rgba(255,0,0,0.3)', borderColor: 'red', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, ]; public lineChartLegend = false; /* tslint:disable:max-line-length */ public lineChartLabels: Array = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ]; /* tslint:enable:max-line-length */ public lineChartType: ChartType = 'line'; public lineChartOptions: ChartConfiguration['options'] = { maintainAspectRatio: false, elements: { line: { tension: 0.5, }, }, scales: { // We use this empty structure as a placeholder for dynamic theming. x: {}, 'y-axis-0': { position: 'left', }, 'y-axis-1': { position: 'right', grid: { color: 'rgba(80,80,80,0.3)', }, ticks: { color: '#808080', }, }, }, }; public random(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } ngOnInit(): void { for (let i = 0; i <= this.lineChartElements; i++) { this.lineChartData1.push(this.random(50, 200)); this.lineChartData2.push(this.random(80, 100)); this.lineChartData3.push(65); } } } ================================================ FILE: Lab3/client/Admin/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Lab3/client/Admin/src/app/views/tenants/create/create.component.html ================================================
Provision tenant Name Email Phone Address Plan
================================================ FILE: Lab3/client/Admin/src/app/views/tenants/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab3/client/Admin/src/app/views/tenants/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { TenantsService } from '../tenants.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { submitting = false; tenantForm: FormGroup = this.fb.group({ tenantName: [null, [Validators.required]], tenantEmail: [null, [Validators.email, Validators.required]], tenantTier: [null, [Validators.required]], tenantPhone: [null], tenantAddress: [null], }); constructor( private fb: FormBuilder, private tenantSvc: TenantsService, private router: Router, private _snackBar: MatSnackBar ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; const user = { ...this.tenantForm.value, }; this.tenantSvc.post(this.tenantForm.value).subscribe({ next: () => { this.submitting = false; this.openErrorMessageSnackBar('Successfully created new tenant!'); this.router.navigate(['tenants']); }, error: (err) => { this.submitting = false; this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); }, }); } } ================================================ FILE: Lab3/client/Admin/src/app/views/tenants/list/list.component.html ================================================
Tenant List
Id {{ element.tenantId }} Tenant Name {{ element.tenantName }} E-Mail {{ element.tenantEmail }} Plan {{ element.tenantTier }} Status {{ element.isActive }}
================================================ FILE: Lab3/client/Admin/src/app/views/tenants/list/list.component.scss ================================================ .tenant-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab3/client/Admin/src/app/views/tenants/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Tenant } from '../models/tenant'; import { TenantsService } from '../tenants.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { tenants$ = new Observable(); tenantData: Tenant[] = []; isLoading: boolean = true; displayedColumns = [ 'tenantId', 'tenantName', 'tenantEmail', 'tenantTier', 'isActive', ]; constructor(private tenantSvc: TenantsService) {} ngOnInit(): void { this.tenantSvc.fetch().subscribe((data) => { this.tenantData = data; this.isLoading = false; }); } } ================================================ FILE: Lab3/client/Admin/src/app/views/tenants/models/tenant.ts ================================================ export interface Tenant { tenantId: string; tenantName: string; tenantEmail: string; tenantTier: string; isActive: boolean; } ================================================ FILE: Lab3/client/Admin/src/app/views/tenants/tenants-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', component: ListComponent, data: { title: 'Tenant List', }, }, { path: 'create', component: CreateComponent, data: { title: 'Provision new tenant', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class TenantsRoutingModule {} ================================================ FILE: Lab3/client/Admin/src/app/views/tenants/tenants.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ListComponent } from './list/list.component'; import { TenantsRoutingModule } from './tenants-routing.module'; import { CreateComponent } from './create/create.component'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, TenantsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ReactiveFormsModule, MatSnackBarModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class TenantsModule {} ================================================ FILE: Lab3/client/Admin/src/app/views/tenants/tenants.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Tenant } from './models/tenant'; @Injectable({ providedIn: 'root', }) export class TenantsService { constructor(private http: HttpClient) {} baseUrl = `${environment.apiUrl}`; tenantsApiUrl = `${this.baseUrl}/tenants`; registrationApiUrl = `${this.baseUrl}/registration`; // TODO strongly-type these anys as tenants once we dial in what the tenant call should return fetch(): Observable { return this.http.get(this.tenantsApiUrl); } post(tenant: Tenant): Observable { return this.http.post(this.registrationApiUrl, tenant); } } ================================================ FILE: Lab3/client/Admin/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
Tenant Id folder_special
================================================ FILE: Lab3/client/Admin/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab3/client/Admin/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], tenantId: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Lab3/client/Admin/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Lab3/client/Admin/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab3/client/Admin/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Lab3/client/Admin/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Lab3/client/Admin/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Lab3/client/Admin/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Lab3/client/Admin/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.apiUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } } ================================================ FILE: Lab3/client/Admin/src/assets/.gitkeep ================================================ ================================================ FILE: Lab3/client/Admin/src/aws-exports.ts ================================================ const awsmobile = { aws_project_region: 'us-west-2', aws_cognito_region: 'us-west-2', aws_user_pools_id: 'us-west-2_nWhwjijMc', aws_user_pools_web_client_id: '61ipvhmvdrfso0cftpil9irk8k', }; export default awsmobile; ================================================ FILE: Lab3/client/Admin/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab3/client/Admin/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab3/client/Admin/src/environments/environment.ts ================================================ export const environment = { production: false, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab3/client/Admin/src/index.html ================================================ Dashboard ================================================ FILE: Lab3/client/Admin/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { Amplify } from 'aws-amplify'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import aws_exports from './aws-exports'; Amplify.configure(aws_exports); if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab3/client/Admin/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab3/client/Admin/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab3/client/Admin/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab3/client/Admin/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; ================================================ FILE: Lab3/client/Admin/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab3/client/Admin/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab3/client/Admin/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab3/client/Admin/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab3/client/Application/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab3/client/Application/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab3/client/Application/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end #cypress cypress/videos/* cypress.env.json # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab3/client/Application/README.md ================================================ # Application This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab3/client/Application/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "application": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "2mb", "maximumError": "2mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "6kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "application:build:production" }, "development": { "browserTarget": "application:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "application:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab3/client/Application/cypress/README.md ================================================ # Application End-to-End Testing ## Instructions To run End-to-End (e2e) tests against the Sample Application, take the following steps: 1. Make a copy of the example env file (`cypress.env.json.example`): ```bash cp cypress.env.json.example cypress.env.json ``` 2. Edit the new file and replace the sample values with real values. The following should help when deciding what to use in place of the sample values provided: - `host`: The URL where the Sample Application is running. If testing locally, this is usually `"http://localhost:4200"`. - `tenantName`: The name of the tenant used to identify the appropriate Cognito User Pool to use for Authentication. - `tenantUsername`: The username to use when logging in. - `tenantUserPassword`: The password to use when logging in. - `email`: The email address to use for testing. (This should be a valid email address.) 3. Navigate to the root of the Application project (`aws-saas-factory-ref-solution-serverless-saas/clients/Application/`) and run the following: ```bash npx cypress run ``` This will run the tests located in the `cypress/e2e` folder. The documentation [here](https://docs.cypress.io/guides/guides/command-line#cypress-run) has more information on what can be passed in as arguments when running the Cypress tests. For example, running the following will show the Cypress UI and what is happening as each of the tests are run: ```bash npx cypress run --headed ``` ================================================ FILE: Lab3/client/Application/cypress/e2e/1-getting-started/basic-access.cy.js ================================================ /// describe('check that the app redirects to /unauthorized when tenant is not set', () => { it('redirects to unauthorized when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); }) it('redirects to unauthorized when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); }) it('redirects to unauthorized when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); }) }) describe('check that the app redirects to a page with a sign-in form when tenant is set and user is not logged in', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').should('exist') cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.location('href').should('contain', '/dashboard') }) it('redirects when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) }) ================================================ FILE: Lab3/client/Application/cypress/e2e/1-getting-started/product-testing.cy.js ================================================ /// describe('check that product, order and user functionality works as expected', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.get('form input[name="username"]').type(Cypress.env('tenantUsername')) cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() cy.wait(1500) cy.get('body').then(body => { if (body.find('form input[name="confirm_password"]').length > 0) { cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form input[name="confirm_password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() } }) }) it('can create new users and display them', () => { const email_username = Cypress.env('email').split('@')[0]; const email_domain = Cypress.env('email').split('@')[1]; const random_suffix = '+test'+Date.now().toString().slice(-3); const myUser = { name: "myUser-"+Date.now(), email: email_username + random_suffix + '@' + email_domain, role: 'userRole'+Date.now().toString().slice(-5) } cy.get("a").contains("Users").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.get("button[color='primary'").contains("Add User").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="userName"]').type(myUser.name) cy.get('input[formcontrolname="userEmail"]').type(myUser.email) cy.get('input[formcontrolname="userRole"]').type(myUser.role) }) cy.intercept({ method: 'POST', url: '**/user', }).as('postUser') cy.intercept({ method: 'GET', url: '**/users', }).as('getUsers') cy.get("button").contains("Create").click() cy.wait('@postUser') cy.go('back') cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.wait('@getUsers') cy.get('table td').contains(myUser.email) }) it('can create a new order with a new product and see them listed', () => { const myProduct = { name: "myProduct-"+Date.now(), price: Date.now().toString().slice(-3), sku: Date.now().toString().slice(-5), category: "category3", } const myOrder = { name: "myOrder-"+Date.now(), } // NOW TESTING PRODUCT CREATION // cy.get("a").contains("Products").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.get("button[color='primary'").contains("Create Product").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="name"]').type(myProduct.name) cy.get('input[formcontrolname="price"]').type(myProduct.price) cy.get('input[formcontrolname="sku"]').type(myProduct.sku) cy.get('mat-select[formcontrolname="category"]').click() }) cy.get('.mat-option-text').contains(myProduct.category).click() cy.get("button").contains("Submit").should('not.be.disabled') cy.intercept({ method: 'POST', url: '**/product', }).as('postProduct') cy.intercept({ method: 'GET', url: '**/products', }).as('getProducts') cy.get("button").contains("Submit").click() cy.wait('@postProduct') cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.wait('@getProducts') cy.get('table td').contains(myProduct.name) cy.get('table td').contains(myProduct.price) cy.get("a").contains("Orders").click() // DONE TESTING PRODUCT CREATION // // NOW TESTING ORDER CREATION // cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.get("button[color='primary']").contains("Create Order").click() cy.location().should((loc) => { expect(loc.href).to.contain('/orders/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="orderName"]').type(myOrder.name) }) cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.intercept({ method: 'POST', url: '**/order', }).as('postOrder') cy.intercept({ method: 'GET', url: '**/orders', }).as('getOrders') cy.get("button").contains("Submit").click() cy.wait('@postOrder') cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.wait('@getOrders') cy.get('table td').contains(myOrder.name) cy.get('table td').contains(new Intl.NumberFormat().format(myProduct.price * 2)) // DONE TESTING ORDER CREATION // }) }) ================================================ FILE: Lab3/client/Application/cypress.config.ts ================================================ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { supportFile: false, setupNodeEvents(on, config) { // implement node event listeners here }, }, }); ================================================ FILE: Lab3/client/Application/cypress.env.json.example ================================================ { "host": "http://localhost:4200", "tenantName": "UPDATE_ME!", "tenantUsername": "UPDATE_ME!", "tenantUserPassword": "UPDATE_ME!", "email": "test@example.com" } ================================================ FILE: Lab3/client/Application/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/application'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab3/client/Application/package.json ================================================ { "name": "application", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "e2e": "npx cypress run" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.2.0", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "cypress": "~10.3.1", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab3/client/Application/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Products', url: '/products', icon: 'sell', }, { name: 'Orders', url: '/orders', icon: 'shopping_cart', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Lab3/client/Application/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { CognitoGuard } from './cognito.guard'; export const routes: Routes = [ { path: '', redirectTo: 'unauthorized', pathMatch: 'full', }, { path: '', component: NavComponent, canActivate: [CognitoGuard], data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'orders', loadChildren: () => import('./views/orders/orders.module').then((m) => m.OrdersModule), }, { path: 'products', loadChildren: () => import('./views/products/products.module').then( (m) => m.ProductsModule ), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, { path: 'unauthorized', component: UnauthorizedComponent, }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab3/client/Application/src/app/app.component.scss ================================================ ================================================ FILE: Lab3/client/Application/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Lab3/client/Application/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { httpInterceptorProviders } from './interceptors'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; @NgModule({ declarations: [ AppComponent, NavComponent, AuthComponent, UnauthorizedComponent, ], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, MatSnackBarModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatIconModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, httpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab3/client/Application/src/app/cognito.guard.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, } from '@angular/router'; import { Auth } from 'aws-amplify'; import { AuthConfigurationService } from './views/auth/auth-configuration.service'; @Injectable({ providedIn: 'root' }) export class CognitoGuard implements CanActivate { constructor( private router: Router, private authConfigService: AuthConfigurationService ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Promise { if (!this.authConfigService.configureAmplifyAuth()) { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return new Promise((res, rej) => { res(false); }); } return Auth.currentSession() .then((u) => { if (u.isValid()) { return true; } else { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return false; } }) .catch((e) => { if (state.url === '/dashboard') { // if we're going to the dashboard and we're not logged in, // don't stop the flow as the amplify-authenticator will // route requests going to the dashboard to the sign-in page. return true; } console.log('Error getting current session', e); this.router.navigate(['/unauthorized']); return false; }); } } ================================================ FILE: Lab3/client/Application/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Lab3/client/Application/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const httpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Lab3/client/Application/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Lab3/client/Application/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Lab3/client/Application/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ (companyName$ | async) || "" }} {{ (username$ | async) || user.username}}
================================================ FILE: Lab3/client/Application/src/app/nav/nav.component.scss ================================================ .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .sidebar-icon-container { height: 64px; background-color: whitesmoke; display: flex; align-items: center; justify-content: center; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .footer { position: fixed; bottom: 0; width: 100%; height: 40px; background-color: whitesmoke; color: #2d3337; // text-align: center; margin: 0px; } .footer-text { display: flex; } .content { position: absolute; width: 100%; height: 90%; max-height: 90%; overflow: auto; background-color: lightgray; } ================================================ FILE: Lab3/client/Application/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; import { AuthConfigurationService } from './../views/auth/auth-configuration.service'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.scss'], }) export class NavComponent implements OnInit { loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router, private authConfigService: AuthConfigurationService ) { // this.configSvc.loadConfigurations().subscribe((val) => console.log(val)); this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => { console.log('Failed to get current session. Err: ', err); return err; }); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map( (sesh) => sesh && typeof sesh.isValid === 'function' && sesh.isValid() ) ); const token$ = session$.pipe( map( (sesh) => sesh && typeof sesh.getIdToken === 'function' && sesh.getIdToken() ) ); this.username$ = token$.pipe( map((t) => t && t.payload && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload && t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } async logout() { await Auth.signOut({ global: true }) .then((e) => { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); }) .catch((err) => { console.error('Error logging out: ', err); }); } } ================================================ FILE: Lab3/client/Application/src/app/views/auth/auth-configuration.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpParams, HttpParamsOptions, } from '@angular/common/http'; import { Injectable, OnInit } from '@angular/core'; import { map, switchMap, catchError } from 'rxjs/operators'; import { throwError } from 'rxjs'; import { environment } from '../../../environments/environment'; import { ConfigParams } from './models/config-params'; import { ActivatedRoute } from '@angular/router'; import Amplify from 'aws-amplify'; import { Auth } from 'aws-amplify'; import { Router } from '@angular/router'; import { from, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AuthConfigurationService { params$: Observable; params: ConfigParams; tenantName: string; constructor( private http: HttpClient, private route: ActivatedRoute, private router: Router ) {} public setTenantConfig(tenantName: string): Promise { const url = `${environment.regApiGatewayUrl}/tenant/init/` + tenantName; this.params$ = this.http.get(url); const setup$ = this.params$.pipe( map((val) => { // remove trailing slash (/) if present val.apiGatewayUrl = val.apiGatewayUrl.replace(/\/$/, ''); localStorage.setItem('userPoolId', val.userPoolId); localStorage.setItem('tenantName', tenantName); localStorage.setItem('appClientId', val.appClientId); localStorage.setItem('apiGatewayUrl', val.apiGatewayUrl); return 'success'; }), catchError((error) => { console.log('Error setting tenant config: ', error); return throwError(error); }) ); return setup$.toPromise(); } configureAmplifyAuth(): boolean { try { const userPoolId = localStorage.getItem('userPoolId'); const appClientId = localStorage.getItem('appClientId'); if (!userPoolId || !appClientId) { return false; } const region = userPoolId?.split('_')[0]; const awsmobile = { aws_project_region: region, aws_cognito_region: region, aws_user_pools_id: userPoolId, aws_user_pools_web_client_id: appClientId, }; Amplify.configure(awsmobile); return true; } catch (err) { console.error('Unable to initialize amplify auth.', err); return false; } } cleanLocalStorage() { localStorage.removeItem('tenantName'); localStorage.removeItem('userPoolId'); localStorage.removeItem('appClientId'); localStorage.removeItem('apiGatewayUrl'); } } ================================================ FILE: Lab3/client/Application/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Lab3/client/Application/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Lab3/client/Application/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Lab3/client/Application/src/app/views/auth/models/config-params.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ export interface ConfigParams { appClientId: string; userPoolId: string; apiGatewayUrl: string; } ================================================ FILE: Lab3/client/Application/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Lab3/client/Application/src/app/views/dashboard/dashboard.component.html ================================================

Dashboard

{{ card.title }}
================================================ FILE: Lab3/client/Application/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } ================================================ FILE: Lab3/client/Application/src/app/views/dashboard/dashboard.component.ts ================================================ import { Component, ViewChild } from '@angular/core'; import { map } from 'rxjs/operators'; import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js'; import { BaseChartDirective } from 'ng2-charts'; import DataLabelsPlugin from 'chartjs-plugin-datalabels'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'], }) export class DashboardComponent { @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; public barChartOptions: ChartConfiguration['options'] = { responsive: true, maintainAspectRatio: false, // We use these empty structures as placeholders for dynamic theming. scales: { x: {}, y: { min: 10, }, }, plugins: { legend: { display: true, }, datalabels: { anchor: 'end', align: 'end', }, }, }; public barChartType: ChartType = 'bar'; public barChartPlugins = [DataLabelsPlugin]; public barChartData: ChartData<'bar'> = { labels: ['2006', '2007', '2008', '2009', '2010', '2011', '2012'], datasets: [ { data: [65, 59, 80, 81, 56, 55, 40], label: 'Series A' }, { data: [28, 48, 40, 19, 86, 27, 90], label: 'Series B' }, ], }; // events public chartClicked({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public chartHovered({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public randomize(): void { // Only Change 3 values this.barChartData.datasets[0].data = [ Math.round(Math.random() * 100), 59, 80, Math.round(Math.random() * 100), 56, Math.round(Math.random() * 100), 40, ]; this.chart?.update(); } /** Based on the screen size, switch from standard to one column per row */ cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe( map(({ matches }) => { if (matches) { return [ { title: 'Card 1', cols: 1, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 1 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; } return [ { title: 'Card 1', cols: 2, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 2 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; }) ); constructor(private breakpointObserver: BreakpointObserver) {} } ================================================ FILE: Lab3/client/Application/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Lab3/client/Application/src/app/views/error/404.component.html ================================================

404

Oops! You're lost.

The page you are looking for was not found.

================================================ FILE: Lab3/client/Application/src/app/views/error/404.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '404.component.html', }) export class P404Component { constructor() {} } ================================================ FILE: Lab3/client/Application/src/app/views/error/500.component.html ================================================

500

Houston, we have a problem!

The page you are looking for is temporarily unavailable.

================================================ FILE: Lab3/client/Application/src/app/views/error/500.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '500.component.html', }) export class P500Component { constructor() {} } ================================================ FILE: Lab3/client/Application/src/app/views/error/unauthorized.component.html ================================================
Unauthorized Enter your tenant name and click submit below Tenant Name home
================================================ FILE: Lab3/client/Application/src/app/views/error/unauthorized.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .center-screen { display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; min-height: 100vh; } ================================================ FILE: Lab3/client/Application/src/app/views/error/unauthorized.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AuthConfigurationService } from './../auth/auth-configuration.service'; import { Observable } from 'rxjs'; import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-unauthorized', templateUrl: './unauthorized.component.html', styleUrls: ['./unauthorized.component.scss'], }) export class UnauthorizedComponent implements OnInit { tenantForm: FormGroup; params$: Observable; error = false; errorMessage: string; tenantNameRequired: boolean = true; constructor( private fb: FormBuilder, private authConfigService: AuthConfigurationService, private _snackBar: MatSnackBar, private router: Router ) { if ( 'userPoolId' in environment && 'appClientId' in environment && 'apiGatewayUrl' in environment ) { // If a tenant's cognito configuration is provided in the // "environment" object, then we take that instead of asking // the visitor to provide the name of their tenant in order // to do a look-up for that tenant's cognito configuration. localStorage.setItem('tenantName', 'PooledTenants'); localStorage.setItem('userPoolId', (environment as any).userPoolId); localStorage.setItem('appClientId', (environment as any).appClientId); localStorage.setItem('apiGatewayUrl', (environment as any).apiGatewayUrl); this.tenantNameRequired = false; } } ngOnInit(): void { this.tenantForm = this.fb.group({ tenantName: [null, [Validators.required]], }); } isFieldInvalid(field: string) { const formField = this.tenantForm.get(field); return ( formField && formField.invalid && (formField.dirty || formField.touched) ); } displayFieldCss(field: string) { return { 'is-invalid': this.isFieldInvalid(field), }; } hasRequiredError(field: string) { return !!this.tenantForm.get(field)?.hasError('required'); } openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } login() { if (!this.tenantNameRequired) { this.router.navigate(['/dashboard']); return true; } let tenantName = this.tenantForm.value.tenantName; if (!tenantName) { this.errorMessage = 'No tenant name provided.'; this.error = true; this.openErrorMessageSnackBar(this.errorMessage); return false; } this.authConfigService .setTenantConfig(tenantName) .then((val) => { this.router.navigate(['/dashboard']); }) .catch((errorResponse) => { this.error = true; this.errorMessage = errorResponse.error.message || 'An unexpected error occurred!'; this.openErrorMessageSnackBar(this.errorMessage); }); return false; } } ================================================ FILE: Lab3/client/Application/src/app/views/orders/create/create.component.html ================================================
Create new order Enter order name Name is required
{{ op.product.name }}
{{ op.product.price | currency }}
{{ op.quantity || 0 }}
================================================ FILE: Lab3/client/Application/src/app/views/orders/create/create.component.scss ================================================ // .card { // margin: 20px; // display: flex; // flex-direction: column; // align-items: flex-start; // } .order-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab3/client/Application/src/app/views/orders/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { Product } from '../../products/models/product.interface'; import { ProductService } from '../../products/product.service'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; interface LineItem { product: Product; quantity?: number; } @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { displayedColumns = []; orderForm: FormGroup; orderProducts: LineItem[] = []; isLoadingProducts: boolean = true; error = ''; constructor( private fb: FormBuilder, private productSvc: ProductService, private orderSvc: OrdersService, private router: Router ) { this.orderForm = this.fb.group({ name: ['', Validators.required], }); } ngOnInit(): void { this.productSvc.fetch().subscribe((products) => { this.orderProducts = products.map((p) => ({ product: p })); this.isLoadingProducts = false; }); this.orderForm = this.fb.group({ orderName: ['', Validators.required], }); } get name() { return this.orderForm.get('name'); } get productQuantity() { return this.orderProducts .filter((p) => !!p.quantity).length > 0; } add(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity ? orderProduct.quantity + 1 : 1, }; } return p; }); } remove(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity && orderProduct.quantity > 1 ? orderProduct.quantity - 1 : undefined, }; } return p; }); } submit() { const val: Order = { ...this.orderForm?.value, orderProducts: this.orderProducts .filter((p) => !!p.quantity) .map((p) => ({ productId: p.product.productId, price: p.product.price, quantity: p.quantity, })), }; this.orderSvc.create(val).subscribe(() => this.router.navigate(['orders'])); } } ================================================ FILE: Lab3/client/Application/src/app/views/orders/detail/detail.component.html ================================================

Order Details mostly made up

NO: {{ orderId$ | async }} | Date: {{ today() | date }}
  • {{ tenantName() }}

Services / Products

Item / Details Unit Cost Sum Cost Discount Tax Total
{{ op.productId }}
{{ op.price | currency }}
Before Tax
{{ sum(op) | currency }}
{{ op.quantity }} Units
$0.00
None
{{ tax(op) | currency }}
Sales Tax 8.9%
{{ total(op) | currency }}
Sub Total Discount Total Tax Final
{{ subTotal(order) | currency }} -$0.00 {{ subTotal(order) | currency }} {{ calcTax(order) | currency }} {{ final(order) | currency }}
Comments / Notes
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Odit repudiandae numquam sit facere blanditiis, quasi distinctio ipsam? Libero odit ex expedita, facere sunt, possimus consectetur dolore, nobis iure amet vero.
================================================ FILE: Lab3/client/Application/src/app/views/orders/detail/detail.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .no-bottom-margin { margin-bottom: 0; } .nowrap { white-space: nowrap; } .invoice { font-family: Arial, Helvetica, sans-serif; width: 970px !important; margin: 50px auto; .invoice-header { padding: 25px 25px 15px; h1 { margin: 0; } .media { .media-body { font-size: 0.9em; margin: 0; } } } .invoice-body { border-radius: 10px; padding: 25px; background: #fff; } .invoice-footer { padding: 15px; font-size: 0.9em; text-align: center; color: #999; } } .logo { max-height: 70px; border-radius: 10px; } .dl-horizontal { margin: 0; dt { float: left; width: 80px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } dd { margin-left: 90px; } } .rowamount { padding-top: 15px !important; } .rowtotal { font-size: 1.3em; } .colfix { width: 12%; } .mono { font-family: monospace; } ================================================ FILE: Lab3/client/Application/src/app/views/orders/detail/detail.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Order } from '../models/order.interface'; import { OrderProduct } from '../models/orderproduct.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-detail', templateUrl: './detail.component.html', styleUrls: ['./detail.component.scss'], }) export class DetailComponent implements OnInit { orderId$: Observable | undefined; order$: Observable | undefined; orderProducts$: Observable | undefined; taxRate = 0.0899; constructor(private route: ActivatedRoute, private orderSvc: OrdersService) {} ngOnInit(): void { this.orderId$ = this.route.params.pipe(map((o) => o['orderId'])); this.order$ = this.orderId$.pipe(switchMap((o) => this.orderSvc.get(o))); this.orderProducts$ = this.order$.pipe(map((o) => o.orderProducts)); } today() { return new Date(); } tenantName() { return ''; } sum(op: OrderProduct) { return op.price * op.quantity; } tax(op: OrderProduct) { return this.sum(op) * this.taxRate; } total(op: OrderProduct) { return this.sum(op) + this.tax(op); } subTotal(order: Order) { return order.orderProducts .map((op) => op.price * op.quantity) .reduce((acc, curr) => acc + curr); } calcTax(order: Order) { return this.subTotal(order) * this.taxRate; } final(order: Order) { return this.subTotal(order) + this.calcTax(order); } } ================================================ FILE: Lab3/client/Application/src/app/views/orders/list/list.component.html ================================================

Order List

Name {{ element.orderName }} Line Items {{ element.orderProducts?.length }} Total {{ sum(element) | currency }}
================================================ FILE: Lab3/client/Application/src/app/views/orders/list/list.component.scss ================================================ .order-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Lab3/client/Application/src/app/views/orders/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { displayedColumns: string[] = ['name', 'lineItems', 'total']; orderData: Order[] = []; isLoading: boolean = true; constructor(private orderSvc: OrdersService, private router: Router) {} ngOnInit(): void { this.orderSvc.fetch().subscribe((data) => { this.isLoading = false; this.orderData = data; }); } sum(order: Order): number { return order.orderProducts .map((p) => p.price * p.quantity) .reduce((acc, curr) => acc + curr); } } ================================================ FILE: Lab3/client/Application/src/app/views/orders/models/order.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { OrderProduct } from './orderproduct.interface'; export interface Order { key: string; shardId: string; orderId: string; orderName: string; orderProducts: OrderProduct[]; } ================================================ FILE: Lab3/client/Application/src/app/views/orders/models/orderproduct.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface OrderProduct { productId: string; price: number; quantity: number; } ================================================ FILE: Lab3/client/Application/src/app/views/orders/orders-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', data: { title: 'All Orders', }, component: ListComponent, }, { path: 'create', data: { title: 'Create Order', }, component: CreateComponent, }, { path: 'detail/:orderId', data: { title: 'View Order Detail', }, component: DetailComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class OrdersRoutingModule {} ================================================ FILE: Lab3/client/Application/src/app/views/orders/orders.module.ts ================================================ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; import { OrdersRoutingModule } from './orders-routing.module'; @NgModule({ declarations: [CreateComponent, ListComponent, DetailComponent], imports: [ CommonModule, OrdersRoutingModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class OrdersModule {} ================================================ FILE: Lab3/client/Application/src/app/views/orders/orders.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Order } from './models/order.interface'; @Injectable({ providedIn: 'root', }) export class OrdersService { orders: Order[] = []; baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; constructor(private http: HttpClient) {} fetch(): Observable { return this.http.get(`${this.baseUrl}/orders`); } get(orderId: string): Observable { const url = `${this.baseUrl}/order/${orderId}`; return this.http.get(url); } create(order: Order): Observable { return this.http.post(`${this.baseUrl}/order`, order); } } ================================================ FILE: Lab3/client/Application/src/app/views/products/create/create.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required SKU Category {{category}}
================================================ FILE: Lab3/client/Application/src/app/views/products/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab3/client/Application/src/app/views/products/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ProductService } from '../product.service'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); } get name() { return this.productForm.get('name'); } get price() { return this.productForm.get('price'); } submit() { this.productSvc.post(this.productForm.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err.message); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Lab3/client/Application/src/app/views/products/edit/edit.component.html ================================================
Edit Product Product ID Enter product name Name is required Enter product price Price is required SKU Category {{ category }}
================================================ FILE: Lab3/client/Application/src/app/views/products/edit/edit.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab3/client/Application/src/app/views/products/edit/edit.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.scss'], }) export class EditComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; product$: Observable | undefined; productId$: Observable | undefined; productName$: Observable | undefined; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router, private route: ActivatedRoute ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ shardId: [], productId: [], name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); this.productId$ = this.route.params.pipe(map((p) => p['productId'])); this.product$ = this.productId$.pipe( switchMap((p) => this.productSvc.get(p)) ); this.productName$ = this.product$.pipe(map((p) => p?.name)); this.product$.subscribe((val) => { this.productForm?.patchValue({ ...val, }); }); } get name() { return this.productForm?.get('name'); } get price() { return this.productForm?.get('price'); } submit() { this.productSvc.put(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => console.error(err), }); } delete() { this.productSvc.delete(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Lab3/client/Application/src/app/views/products/list/list.component.html ================================================

Product List

Name {{ element.name }} Price {{ element.price | currency }} SKU {{ element.sku }}
================================================ FILE: Lab3/client/Application/src/app/views/products/list/list.component.scss ================================================ .product-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Lab3/client/Application/src/app/views/products/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { productData: Product[] = []; isLoading: boolean = true; displayedColumns: string[] = ['name', 'price', 'sku']; constructor(private productSvc: ProductService, private router: Router) {} ngOnInit(): void { this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onEdit(product: Product) { this.router.navigate(['products', 'edit', product.productId]); return false; } onRemove(product: Product) { this.productSvc.delete(product); this.isLoading = true; this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onCreate() { this.router.navigate(['products', 'create']); } } ================================================ FILE: Lab3/client/Application/src/app/views/products/models/product.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface Product { key: string; shardId: string; productId: string; name: string; price: number; sku: string; category: string; pictureUrl?: string; } ================================================ FILE: Lab3/client/Application/src/app/views/products/product.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Product } from './models/product.interface'; @Injectable({ providedIn: 'root', }) export class ProductService { constructor(private http: HttpClient) {} baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; fetch(): Observable { return this.http.get(`${this.baseUrl}/products`); } get(productId: string): Observable { const url = `${this.baseUrl}/product/${productId}`; return this.http.get(url); } delete(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.delete(url); } put(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.put(url, product); } post(product: Product) { return this.http.post(`${this.baseUrl}/product`, product); } } ================================================ FILE: Lab3/client/Application/src/app/views/products/products-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'Product List', }, component: ListComponent, }, { path: 'create', data: { title: 'Create new Product', }, component: CreateComponent, }, { path: 'edit/:productId', data: { title: 'Edit Product', }, component: EditComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class ProductsRoutingModule {} ================================================ FILE: Lab3/client/Application/src/app/views/products/products.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ProductsRoutingModule } from './products-routing.module'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [CreateComponent, EditComponent, ListComponent], imports: [ CommonModule, ReactiveFormsModule, ProductsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class ProductsModule {} ================================================ FILE: Lab3/client/Application/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
================================================ FILE: Lab3/client/Application/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab3/client/Application/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Lab3/client/Application/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Lab3/client/Application/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab3/client/Application/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Lab3/client/Application/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Lab3/client/Application/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Lab3/client/Application/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Lab3/client/Application/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { find, mergeMap, defaultIfEmpty } from 'rxjs/operators'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.regApiGatewayUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } update(email: string, user: User) {} } ================================================ FILE: Lab3/client/Application/src/assets/.gitkeep ================================================ ================================================ FILE: Lab3/client/Application/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab3/client/Application/src/environments/environment.prod.ts ================================================ export const environment = { production: false, regApiGatewayUrl: 'https://ulp15c9bv2.execute-api.us-west-2.amazonaws.com/prod/', }; ================================================ FILE: Lab3/client/Application/src/environments/environment.ts ================================================ export const environment = { production: false, regApiGatewayUrl: 'https://ulp15c9bv2.execute-api.us-west-2.amazonaws.com/prod/', }; ================================================ FILE: Lab3/client/Application/src/index.html ================================================ Application ================================================ FILE: Lab3/client/Application/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab3/client/Application/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab3/client/Application/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab3/client/Application/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab3/client/Application/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "~bootstrap/scss/functions"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/maps"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; .chart-container canvas { max-height: 250px; width: auto; } .chart-container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; } ================================================ FILE: Lab3/client/Application/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab3/client/Application/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab3/client/Application/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "strictPropertyInitialization": false, "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab3/client/Application/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab3/client/Landing/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab3/client/Landing/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab3/client/Landing/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab3/client/Landing/README.md ================================================ # Landing This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab3/client/Landing/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "Landing": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/custom-theme.scss", "src/styles.scss", "node_modules/font-awesome/css/font-awesome.min.css" ] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "10kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "Landing:build:production" }, "development": { "browserTarget": "Landing:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "Landing:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab3/client/Landing/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab3/client/Landing/package.json ================================================ { "name": "landing", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "bootstrap": "~5.1.3", "font-awesome": "~4.7.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab3/client/Landing/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; export const routes: Routes = [ { path: '', redirectTo: 'landing', pathMatch: 'full', }, { path: 'landing', component: LandingComponent, }, { path: 'register', component: RegisterComponent, }, { path: '**', redirectTo: 'landing', pathMatch: 'full', }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab3/client/Landing/src/app/app.component.scss ================================================ ================================================ FILE: Lab3/client/Landing/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'landing'; } ================================================ FILE: Lab3/client/Landing/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; @NgModule({ declarations: [AppComponent, LandingComponent, RegisterComponent], imports: [ AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, MatButtonModule, MatCardModule, MatIconModule, MatProgressSpinnerModule, MatSidenavModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, FormsModule, HttpClientModule, ReactiveFormsModule, MatSnackBarModule, MatInputModule, MatFormFieldModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab3/client/Landing/src/app/views/landing/landing.component.html ================================================

Serverless SaaS Reference Architecture

It's so nice it blows your mind.

Sign up now!

Serverless SaaS Reference Architecture is so awesome.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere temporibus omnis illum, officia. Architecto voluptatibus commodi voluptatem perspiciatis eos possimus, eius at molestias quaerat magnam? Odio qui quos ipsam natus.

Serverless SaaS Reference Architecture so awesome. Makes you awesome - go sign up!

Serverless SaaS Reference Architecture so great. Makes you even greater - go sign up now. Super cheap deal!

Feel lonely? Go sign up and have a friend!

Take Serverless SaaS Reference Architecture with you everywhere you go.

Serverless SaaS Reference Architecture is all you need. Anywhere - ever. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita sapiente hic voluptatum quo sunt totam accusamus distinctio minus aliquid quis!

Love Serverless SaaS Reference Architecture. So nice! So good! Could not live without!

Satisfied Customer

Reasons to sign up this product:

  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!
  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!

Why you still reading?

Sign up now!
================================================ FILE: Lab3/client/Landing/src/app/views/landing/landing.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // $color-red: #cc615f; $color-grey: #4b4b4b; $color-grey--light: #d3d3d3; $btn-amzn: #ec7211; $color-primary: #232f3e; $bp-s: 65.75em; //700px; $bp-xs: 34.375em; //550px; @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,800,300); *{ box-sizing: border-box; } html{ width: 100%; height:100%; margin: 0; padding: 0; } body{ width: 100%; height:100%; font-family: 'Open Sans','Helvetica Neue',Helvetica, sans-serif; font-size: 100%; line-height: 1.45; color: #141414; } a{ text-decoration: none; &:hover{ text-decoration: none; } } img{ max-width: 100%; } .btn{ display: inline-block; margin: 1rem 0; color: white; font-weight: 500; font-size: 1.3rem; background: $btn-amzn; letter-spacing: .02em; border: none; border-radius: 5px; padding: .8rem 1rem .9rem; text-shadow: 0 1px rgba(black,.3); box-shadow: 0 0 2px rgba(black,.2); @media (max-width: $bp-s){ padding: .5rem .7rem .6rem; font-size: 1rem; } &:hover{ background: lighten($btn-amzn,5%); color: #fff; } &:focus, &:active, &:focus:active{ background: darken($btn-amzn,5%); border-color: darken($btn-amzn,5%); box-shadow: 0 2px 5px 0 rgba(black,.5) inset; } } .container{ margin: 0 auto; width: 90%; max-width: 900px; } header{ color: white; background: #232f3e; padding: 10rem 0; text-align: center; position: relative; z-index: 1; overflow: hidden; @media (max-width: $bp-s){ padding: 2rem 0; } h1{ font-size: 3rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 2rem; } } h2{ font-weight: 300; font-size: 1.5rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } } section{ background: #fff; color: $color-grey; padding: 3.5rem 0; @media (max-width: $bp-s){ padding: 2rem 0; } &.section--primary{ background: $color-primary; color: #fff; } &.section--primary--alt{ background: desaturate(lighten($color-primary,15%),10%); color: #fff; } &.section--primary--light{ background: rgba($color-primary,.1); } &.section--grey{ background: $color-grey; color: #fff; } &.section--grey--light{ background: $color-grey--light; color: #fff; } h3{ text-align: center; font-size: 2rem; font-weight: 300; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } li{ font-size: 1.2rem; font-weight: 300; } p{ font-size: 1.2rem; font-weight: 300; } } .col{ margin: 0 1.5%; display: inline-block; vertical-align: top; } .col-7{ @extend .col; width: 64%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-3{ @extend .col; width: 29%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-5{ @extend .col; width: 30%; @media (max-width: $bp-xs){ width: 60%; margin: 0; } } .details{ text-align: left; h3{ font-size: 2rem; text-align: left; } } .features{ text-align: center; padding: 1rem; &:hover{ background: rgba(white,.1); } @media (max-width: $bp-s){ width: 100%; margin: 0; text-align: left; border-bottom: 1px solid rgba(white,.2); &:last-child{ border: none; } } i{ font-size: 4rem; margin: 0 0 2rem 0; @media (max-width: $bp-s){ font-size: 1.5rem; width: 2rem; text-align: center; margin: 0 0 1rem 0; float: left; } } p{ margin: 0 0 1rem 0; font-size: 1rem; @media (max-width: $bp-s){ margin-left: 3rem; } } } blockquote{ position: relative; margin: 0; padding: 0; text-align: center; &:before{ display: inline-block; color: $color-primary; font-size: 2rem; content: '\201C'; } p{ margin: 0; display: inline; font-size: 1.5rem; @media (max-width: $bp-s){ font-size: 1.2rem; } } cite{ font-size: 1rem; display: block; margin: .5rem 0 0 1.2rem; @media (max-width: $bp-s){ font-size: .8rem; } &:before{ content: '–'; } } } footer{ background: $color-primary; color: #fff; padding: 2rem 0; text-align: center; font-size: .8rem; color: rgba(white,.4); ul{ margin: 0; padding: 0; list-style: none; li{ display: inline-block; a{ display: block; padding: .4rem .7rem; font-size: .9rem; text-decoration: none; color: rgba(white,.7); &:hover{ color: white; } } } } } .text--center{ text-align: center; } .text--left{ text-align: left; } .bg-image{ background: $color-primary; text-align: center; position: relative; z-index: 1; overflow: hidden; //text-shadow: 2px 0 5px black; &:before{ content: ''; display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; z-index: -1; background-size: 100%; background-attachment: fixed; filter: blur(3px); opacity: .8; transform: scale(1.1); } &.bg-image-2:before{ opacity: .6; background-position: center center; } } ================================================ FILE: Lab3/client/Landing/src/app/views/landing/landing.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-landing', templateUrl: './landing.component.html', styleUrls: ['./landing.component.scss'], }) export class LandingComponent implements OnInit { constructor(private router: Router) {} ngOnInit() {} register() { this.router.navigate(['register']); return false; } } ================================================ FILE: Lab3/client/Landing/src/app/views/register/register.component.html ================================================
Provision a new Tenant Login details will be sent to the email provided. Name Email Phone Address Plan
================================================ FILE: Lab3/client/Landing/src/app/views/register/register.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .tenant-form { display: flex; align-items: center; justify-content: center; } .wide-form { min-width: 150px; max-width: 500px; width: 100%; } .full-width { width: 100%; } .mat-form-field { width: 100%; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab3/client/Landing/src/app/views/register/register.component.ts ================================================ import { MatSnackBar } from '@angular/material/snack-bar'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.scss'], }) export class RegisterComponent implements OnInit { submitting: boolean = false; tenantForm: FormGroup = this.fb.group({ tenantName: ['', [Validators.required]], tenantEmail: ['', [Validators.email, Validators.required]], tenantTier: ['', [Validators.required]], tenantPhone: [''], tenantAddress: [''], }); constructor( private fb: FormBuilder, private _snackBar: MatSnackBar, private http: HttpClient ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; this.tenantForm.disable(); const tenant = { ...this.tenantForm.value, }; this.http .post(`${environment.apiGatewayUrl}/registration`, tenant) .subscribe({ next: () => { this.openErrorMessageSnackBar('Successfully created new tenant!'); this.tenantForm.reset(); this.tenantForm.enable(); this.submitting = false; }, error: (err) => { this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); this.tenantForm.enable(); this.submitting = false; }, }); } } ================================================ FILE: Lab3/client/Landing/src/assets/.gitkeep ================================================ ================================================ FILE: Lab3/client/Landing/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab3/client/Landing/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab3/client/Landing/src/environments/environment.ts ================================================ export const environment = { production: false, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab3/client/Landing/src/index.html ================================================ Landing ================================================ FILE: Lab3/client/Landing/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab3/client/Landing/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab3/client/Landing/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab3/client/Landing/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab3/client/Landing/src/styles.scss ================================================ ================================================ FILE: Lab3/client/Landing/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab3/client/Landing/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab3/client/Landing/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab3/client/Landing/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab3/client/dummy.txt ================================================ ================================================ FILE: Lab3/scripts/deploy-updates.sh ================================================ #!/bin/bash cd ../server || exit # stop execution if cd fails rm -rf .aws-sam/ python3 -m pylint -E -d E0401 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi #Deploying shared services changes echo "Deploying shared services changes" echo Y | sam sync --stack-name serverless-saas -t shared-template.yaml --code --resource-id LambdaFunctions/ServerlessSaaSLayers --resource-id LambdaFunctions/SharedServicesAuthorizerFunction -u #Deploying tenant services changes echo "Deploying tenant services changes" rm -rf .aws-sam/ echo Y | sam sync --stack-name stack-pooled -t tenant-template.yaml --code --resource-id ServerlessSaaSLayers --resource-id BusinessServicesAuthorizerFunction --resource-id CreateProductFunction -u cd ../scripts || exit ./geturl.sh ================================================ FILE: Lab3/scripts/deployment.sh ================================================ #!/bin/bash if [[ "$#" -eq 0 ]]; then echo "Invalid parameters" echo "Command to deploy client code: deployment.sh -c" echo "Command to deploy bootstrap server code: deployment.sh -b" echo "Command to deploy tenant server code: deployment.sh -t" echo "Command to deploy bootstrap & tenant server code: deployment.sh -s" echo "Command to deploy server & client code: deployment.sh -s -c" exit 1 fi while [[ "$#" -gt 0 ]]; do case $1 in -s) server=1 ;; -b) bootstrap=1 ;; -t) tenant=1 ;; -c) client=1 ;; *) echo "Unknown parameter passed: $1"; exit 1 ;; esac shift done # During AWS hosted events using event engine tool # we pre-provision cloudfront and s3 buckets which hosts UI code. # So that it improves this labs total execution time. # Below code checks if cloudfront and s3 buckets are # pre-provisioned or not and then concludes if the workshop # is running in AWS hosted event through event engine tool or not. IS_RUNNING_IN_EVENT_ENGINE=false PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" IS_RUNNING_IN_EVENT_ENGINE=true ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_BUCKET=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSiteBucket'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) fi if [[ $server -eq 1 ]] || [[ $bootstrap -eq 1 ]] || [[ $tenant -eq 1 ]]; then echo "Validating server code using pylint" cd ../server python3 -m pylint -E -d E0401,E1111 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi cd ../scripts fi if [[ $server -eq 1 ]] || [[ $bootstrap -eq 1 ]]; then echo "Bootstrap server code is getting deployed" cd ../server REGION=$(aws configure get region) sam build -t shared-template.yaml --use-container if [ "$IS_RUNNING_IN_EVENT_ENGINE" = true ]; then sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE AdminUserPoolCallbackURLParameter=$ADMIN_SITE_URL TenantUserPoolCallbackURLParameter=$APP_SITE_URL else sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE fi cd ../scripts fi if [[ $server -eq 1 ]] || [[ $tenant -eq 1 ]]; then echo "Tenant server code is getting deployed" cd ../server REGION=$(aws configure get region) sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml --region=$REGION cd ../scripts fi if [ "$IS_RUNNING_IN_EVENT_ENGINE" = false ]; then ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_BUCKET=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSiteBucket'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi if [[ $client -eq 1 ]]; then echo "Client code is getting deployed" ADMIN_APIGATEWAYURL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminApi'].OutputValue" --output text) APP_APIGATEWAYURL=$(aws cloudformation describe-stacks --stack-name stack-pooled --query "Stacks[0].Outputs[?OutputKey=='TenantAPI'].OutputValue" --output text) APP_APPCLIENTID=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='CognitoTenantAppClientId'].OutputValue" --output text) APP_USERPOOLID=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='CognitoTenantUserPoolId'].OutputValue" --output text) # Admin UI and Landing UI are configured in Lab2 echo "Admin UI and Landing UI are configured in Lab2. Only App UI will be configured in this Lab3." # Configuring app UI echo "aws s3 ls s3://$APP_SITE_BUCKET" aws s3 ls s3://$APP_SITE_BUCKET if [ $? -ne 0 ]; then echo "Error! S3 Bucket: $APP_SITE_BUCKET not readable" exit 1 fi cd ../client/Application echo "Configuring environment for App Client" cat << EoF > ./src/environments/environment.prod.ts export const environment = { production: true, regApiGatewayUrl: '$ADMIN_APIGATEWAYURL', apiGatewayUrl: '$APP_APIGATEWAYURL', userPoolId: '$APP_USERPOOLID', appClientId: '$APP_APPCLIENTID', }; EoF cat << EoF > ./src/environments/environment.ts export const environment = { production: true, regApiGatewayUrl: '$ADMIN_APIGATEWAYURL', apiGatewayUrl: '$APP_APIGATEWAYURL', userPoolId: '$APP_USERPOOLID', appClientId: '$APP_APPCLIENTID', }; EoF npm install --legacy-peer-deps && npm run build echo "aws s3 sync --delete --cache-control no-store dist s3://$APP_SITE_BUCKET" aws s3 sync --delete --cache-control no-store dist s3://$APP_SITE_BUCKET if [[ $? -ne 0 ]]; then exit 1 fi echo "Completed configuring environment for App Client" echo "Successfully completed deploying Application UI" fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Lab3/scripts/geturl.sh ================================================ PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) else ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Lab3/server/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Lab3/server/OrderService/order_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Order: key='' def __init__(self, shardId, orderId, orderName, orderProducts): self.shardId = shardId self.orderId = orderId self.key = shardId + ':' + orderId self.orderName = orderName self.orderProducts = orderProducts class OrderProduct: def __init__(self, productId, price, quantity): self.productId = productId self.price = price self.quantity = quantity ================================================ FILE: Lab3/server/OrderService/order_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import order_service_dal from decimal import Decimal from types import SimpleNamespace from aws_lambda_powertools import Tracer tracer = Tracer() @tracer.capture_lambda_handler def get_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a order") params = event['pathParameters'] key = params['id'] logger.log_with_tenant_context(event, params) order = order_service_dal.get_order(event, key) logger.log_with_tenant_context(event, "Request completed to get a order") metrics_manager.record_metric(event, "SingleOrderRequested", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def create_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) order = order_service_dal.create_order(event, payload) logger.log_with_tenant_context(event, "Request completed to create a order") metrics_manager.record_metric(event, "OrderCreated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def update_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] order = order_service_dal.update_order(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a order") metrics_manager.record_metric(event, "OrderUpdated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def delete_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a order") params = event['pathParameters'] key = params['id'] response = order_service_dal.delete_order(event, key) logger.log_with_tenant_context(event, "Request completed to delete a order") metrics_manager.record_metric(event, "OrderDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the order") @tracer.capture_lambda_handler def get_orders(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all orders") response = order_service_dal.get_orders(event, tenantId) metrics_manager.record_metric(event, "OrdersRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all orders") return utils.generate_response(response) ================================================ FILE: Lab3/server/OrderService/order_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid from order_models import Order import json import utils import metrics_manager from types import SimpleNamespace import logger import random import threading from boto3.dynamodb.conditions import Key table_name = os.environ['ORDER_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) suffix_start = 1 suffix_end = 10 def get_order(event, key): try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) response = table.get_item(Key={'shardId': shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a order', e) else: return order def delete_order(event, key): try: shardId = key.split(":")[0] orderId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a order', e) else: logger.info("DeleteItem succeeded:") return response def create_order(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) order = Order(shardId, str(uuid.uuid4()), payload.orderName, payload.orderProducts) try: response = table.put_item(Item={ 'shardId':shardId, 'orderId': order.orderId, 'orderName': order.orderName, 'orderProducts': get_order_products_dict(order.orderProducts) }, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a order', e) else: logger.info("PutItem succeeded:") return order def update_order(event, payload, key): try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) order = Order(shardId, orderId,payload.orderName, payload.orderProducts) response = table.update_item(Key={'shardId':order.shardId, 'orderId': order.orderId}, UpdateExpression="set orderName=:orderName, " +"orderProducts=:orderProducts", ExpressionAttributeValues={ ':orderName': order.orderName, ':orderProducts': get_order_products_dict(order.orderProducts) }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a order', e) else: logger.info("UpdateItem succeeded:") return order def get_orders(event, tenantId): get_all_products_response = [] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error("Error getting all orders") raise Exception('Error getting all orders', e) else: logger.info("Get orders succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) get_all_products_response.append(order) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def get_order_products_dict(orderProducts): orderProductList = [] for i in range(len(orderProducts)): product = orderProducts[i] orderProductList.append(vars(product)) return orderProductList ================================================ FILE: Lab3/server/OrderService/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Lab3/server/ProductService/product_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Product: key ='' def __init__(self, shardId, productId, sku, name, price, category): self.shardId = shardId self.productId = productId self.key = shardId + ':' + productId self.sku = sku self.name = name self.price = price self.category = category class Category: def __init__(self, id, name): self.id = id self.name = name ================================================ FILE: Lab3/server/ProductService/product_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import product_service_dal from decimal import Decimal from aws_lambda_powertools import Tracer from types import SimpleNamespace tracer = Tracer() @tracer.capture_lambda_handler def get_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a product") params = event['pathParameters'] logger.log_with_tenant_context(event, params) key = params['id'] logger.log_with_tenant_context(event, key) product = product_service_dal.get_product(event, key) logger.log_with_tenant_context(event, "Request completed to get a product") metrics_manager.record_metric(event, "SingleProductRequested", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def create_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) product = product_service_dal.create_product(event, payload) logger.log_with_tenant_context(event, "Request completed to create a product") #TODO: Capture metrics to denote that one product was created by tenant return utils.generate_response(product) @tracer.capture_lambda_handler def update_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] product = product_service_dal.update_product(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a product") metrics_manager.record_metric(event, "ProductUpdated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def delete_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a product") params = event['pathParameters'] key = params['id'] response = product_service_dal.delete_product(event, key) logger.log_with_tenant_context(event, "Request completed to delete a product") metrics_manager.record_metric(event, "ProductDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the product") @tracer.capture_lambda_handler def get_products(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all products") response = product_service_dal.get_products(event, tenantId) metrics_manager.record_metric(event, "ProductsRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all products") return utils.generate_response(response) ================================================ FILE: Lab3/server/ProductService/product_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid import json import logger import random import threading import metrics_manager from product_models import Product from types import SimpleNamespace from boto3.dynamodb.conditions import Key table_name = os.environ['PRODUCT_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) suffix_start = 1 suffix_end = 10 def get_product(event, key): try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) response = table.get_item(Key={'shardId': shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a product', e) else: logger.info("GetItem succeeded:"+ str(product)) return product def delete_product(event, key): try: shardId = key.split(":")[0] productId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a product', e) else: logger.info("DeleteItem succeeded:") return response #TODO: Implement this method def create_product(event, payload): pass def update_product(event, payload, key): try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) product = Product(shardId,productId,payload.sku, payload.name, payload.price, payload.category) response = table.update_item(Key={'shardId':product.shardId, 'productId': product.productId}, UpdateExpression="set sku=:sku, #n=:productName, price=:price, category=:category", ExpressionAttributeNames= {'#n':'name'}, ExpressionAttributeValues={ ':sku': product.sku, ':productName': product.name, ':price': product.price, ':category': product.category }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a product', e) else: logger.info("UpdateItem succeeded:") return product def get_products(event, tenantId): get_all_products_response =[] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting all products', e) else: logger.info("Get products succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) get_all_products_response.append(product) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) ================================================ FILE: Lab3/server/ProductService/requirements.txt ================================================ requests pytest-mock aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Lab3/server/README.md ================================================ sam build -t shared-template.yaml --use-container sam deploy --config-file shared-samconfig.toml sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml ================================================ FILE: Lab3/server/Resources/requirements.txt ================================================ python-jose[cryptography] ================================================ FILE: Lab3/server/Resources/shared_service_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import utils import auth_manager region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] tenant_userpool_id = os.environ['TENANT_USER_POOL'] tenant_appclient_id = os.environ['TENANT_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) if(auth_manager.isSaaSProvider(unauthorized_claims['custom:userRole'])): userpool_id = user_pool_operation_user appclient_id = app_client_operation_user else: #get tenant user pool and app client to validate jwt token against userpool_id = tenant_userpool_id appclient_id = tenant_appclient_id #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] #only tenant admin and system admin can do certain actions like create and disable users #TODO: Add policy so that only tenant and SaaS admins can add/modify tenant information authResponse = policy.build() context = { 'userName': user_name, 'userPoolId': userpool_id, 'tenantId': tenant_id, 'userRole': user_role } authResponse['context'] = context return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Lab3/server/Resources/tenant_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import auth_manager import utils region = os.environ['AWS_REGION'] userpool_id = os.environ['TENANT_USER_POOL'] appclient_id = os.environ['TENANT_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] #roles are not fine-grained enough to allow selectively policy.allowAllMethods() authResponse = policy.build() # TODO: Add tenant context to authResponse return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Lab3/server/TenantManagementService/requirements.txt ================================================ requests aws_requests_auth ================================================ FILE: Lab3/server/TenantManagementService/tenant-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import json import boto3 from boto3.dynamodb.conditions import Key import urllib.parse import utils from botocore.exceptions import ClientError import logger import requests import metrics_manager import auth_manager from aws_lambda_powertools import Tracer tracer = Tracer() region = os.environ['AWS_REGION'] dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') #This method has been locked down to be only called from tenant registration service def create_tenant(event, context): tenant_details = json.loads(event['body']) try: response = table_tenant_details.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'tenantName' : tenant_details['tenantName'], 'tenantAddress': tenant_details['tenantAddress'], 'tenantEmail': tenant_details['tenantEmail'], 'tenantPhone': tenant_details['tenantPhone'], 'tenantTier': tenant_details['tenantTier'], 'isActive': True } ) except Exception as e: raise Exception('Error creating a new tenant', e) else: return utils.create_success_response("Tenant Created") def get_tenants(event, context): try: response = table_tenant_details.scan() except Exception as e: raise Exception('Error getting all tenants', e) else: return utils.generate_response(response['Items']) @tracer.capture_lambda_handler def update_tenant(event, context): requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_details = json.loads(event['body']) tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response_update = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set tenantName = :tenantName, tenantAddress = :tenantAddress, tenantEmail = :tenantEmail, tenantPhone = :tenantPhone, tenantTier=:tenantTier", ExpressionAttributeValues={ ':tenantName' : tenant_details['tenantName'], ':tenantAddress': tenant_details['tenantAddress'], ':tenantEmail': tenant_details['tenantEmail'], ':tenantPhone': tenant_details['tenantPhone'], ':tenantTier': tenant_details['tenantTier'] }, ReturnValues="UPDATED_NEW" ) logger.log_with_tenant_context(event, response_update) logger.log_with_tenant_context(event, "Request completed to update tenant") return utils.create_success_response("Tenant Updated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_tenant(event, context): requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get tenant details") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): tenant_details = table_tenant_details.get_item( Key={ 'tenantId': tenant_id, }, AttributesToGet=[ 'tenantName', 'tenantAddress', 'tenantEmail', 'tenantPhone' ] ) item = tenant_details['Item'] tenant_info = TenantInfo(item['tenantName'], item['tenantAddress'],item['tenantEmail'], item['tenantPhone']) logger.log_with_tenant_context(event, tenant_info) logger.log_with_tenant_context(event, "Request completed to get tenant details") return utils.create_success_response(tenant_info.__dict__) else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def deactivate_tenant(event, context): url_disable_users = os.environ['DISABLE_USERS_BY_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to deactivate tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': False }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) update_details = {} update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_disable_users(update_details, headers, auth, host, stage_name, url_disable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to deactivate tenant") return utils.create_success_response("Tenant Deactivated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def activate_tenant(event, context): url_enable_users = os.environ['ENABLE_USERS_BY_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to activate tenant") if (auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': True }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) update_details = {} update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_enable_users(update_details, headers, auth, host, stage_name, url_enable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to activate tenant") return utils.create_success_response("Tenant Activated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only system admin can activate tenant!") return utils.create_unauthorized_response() def __invoke_disable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url, '/']) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while disabling users for the tenant') except Exception as e: logger.error('Error occured while disabling users for the tenant') raise Exception('Error occured while disabling users for the tenant', e) else: return "Success invoking disable users" def __invoke_enable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url, '/']) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while enabling users for the tenant') except Exception as e: logger.error('Error occured while enabling users for the tenant') raise Exception('Error occured while enabling users for the tenant', e) else: return "Success invoking enable users" class TenantInfo: def __init__(self, tenant_name, tenant_address, tenant_email, tenant_phone): self.tenant_name = tenant_name self.tenant_address = tenant_address self.tenant_email = tenant_email self.tenant_phone = tenant_phone ================================================ FILE: Lab3/server/TenantManagementService/tenant-registration.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import utils import uuid import logger import requests import re region = os.environ['AWS_REGION'] create_tenant_admin_user_resource_path = os.environ['CREATE_TENANT_ADMIN_USER_RESOURCE_PATH'] create_tenant_resource_path = os.environ['CREATE_TENANT_RESOURCE_PATH'] lambda_client = boto3.client('lambda') def register_tenant(event, context): try: tenant_id = uuid.uuid1().hex tenant_details = json.loads(event['body']) tenant_details['tenantId'] = tenant_id logger.info(tenant_details) stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) create_user_response = __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name) logger.info (create_user_response) tenant_details['tenantAdminUserName'] = create_user_response['message']['tenantAdminUserName'] create_tenant_response = __create_tenant(tenant_details, headers, auth, host, stage_name) logger.info (create_tenant_response) except Exception as e: logger.error('Error registering a new tenant') raise Exception('Error registering a new tenant', e) else: return utils.create_success_response("You have been registered in our system") def __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_admin_user_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while calling the create tenant admin user service') raise Exception('Error occured while calling the create tenant admin user service', e) else: return response_json def __create_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_resource_path]) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while creating the tenant record in table') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json ================================================ FILE: Lab3/server/TenantManagementService/user-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import sys import logger import metrics_manager import auth_manager import utils from boto3.dynamodb.conditions import Key from aws_lambda_powertools import Tracer tracer = Tracer() client = boto3.client('cognito-idp') dynamodb = boto3.resource('dynamodb') table_tenant_user_map = dynamodb.Table('ServerlessSaaS-TenantUserMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_id = os.environ['TENANT_USER_POOL_ID'] def create_tenant_admin_user(event, context): app_client_id = os.environ['TENANT_APP_CLIENT_ID'] tenant_details = json.loads(event['body']) tenant_id = tenant_details['tenantId'] logger.info(tenant_details) user_mgmt = UserManagement() #Add tenant admin now based upon user pool tenant_user_group_response = user_mgmt.create_user_group(user_pool_id,tenant_id,"User group for tenant {0}".format(tenant_id)) tenant_admin_user_name = 'tenant-admin-{0}'.format(tenant_details['tenantId']) create_tenant_admin_response = user_mgmt.create_tenant_admin(user_pool_id, tenant_admin_user_name, tenant_details) add_tenant_admin_to_group_response = user_mgmt.add_user_to_group(user_pool_id, tenant_admin_user_name, tenant_user_group_response['Group']['GroupName']) tenant_user_mapping_response = user_mgmt.create_user_tenant_mapping(tenant_admin_user_name,tenant_id) response = {"userPoolId": user_pool_id, "appClientId": app_client_id, "tenantAdminUserName": tenant_admin_user_name} return utils.create_success_response(response) @tracer.capture_lambda_handler #only tenant admin can create users def create_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to create new user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = user_details['tenantId'] else: user_tenant_id = tenant_id if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): metrics_manager.record_metric(event, "UserCreated", "Count", 1) response = client.admin_create_user( Username=user_details['userName'], UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] }, { 'Name': 'custom:tenantId', 'Value': user_tenant_id } ] ) logger.log_with_tenant_context(event, response) user_mgmt = UserManagement() user_mgmt.add_user_to_group(user_pool_id, user_details['userName'], user_tenant_id) response_mapping = user_mgmt.create_user_tenant_mapping(user_details['userName'], user_tenant_id) logger.log_with_tenant_context(event, "Request completed to create new user ") return utils.create_success_response("New user created") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can create user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_users(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] users = [] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get users") if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): response = client.list_users( UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) num_of_users = len(response['Users']) metrics_manager.record_metric(event, "Number of users", "Count", num_of_users) if (num_of_users > 0): for user in response['Users']: is_same_tenant_user = False user_info = UserInfo() for attr in user["Attributes"]: if(attr["Name"] == "custom:tenantId" and attr["Value"] == tenant_id): is_same_tenant_user = True user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] if(is_same_tenant_user): user_info.enabled = user["Enabled"] user_info.created = user["UserCreateDate"] user_info.modified = user["UserLastModifiedDate"] user_info.status = user["UserStatus"] user_info.user_name = user["Username"] users.append(user_info) return utils.generate_response(users) else: logger.log_with_tenant_context(event, "Request completed as unauthorized.") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get user") if (auth_manager.isTenantUser(user_role) and user_name != requesting_user_name): logger.log_with_tenant_context(event, "Request completed as unauthorized. User can only get its information.") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserInfoRequested", "Count", 1) user_info = get_user_info(event, user_pool_id, user_name) logger.log_with_tenant_context(event, "Request completed to get new user ") return utils.create_success_response(user_info.__dict__) @tracer.capture_lambda_handler def update_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update user") if (auth_manager.isTenantUser(user_role)): logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update user!") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserUpdated", "Count", 1) response = client.admin_update_user_attributes( Username=user_name, UserPoolId=user_pool_id, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] } ] ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to update user") return utils.create_success_response("user updated") @tracer.capture_lambda_handler def disable_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to disable new user") if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): metrics_manager.record_metric(event, "UserDisabled", "Count", 1) response = client.admin_disable_user( Username=user_name, UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to disable new user") return utils.create_success_response("User disabled") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can disable user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def disable_users_by_tenant(event, context): logger.info("Request received to disable users by tenant") tenantid_to_update = event['tenantId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if ((auth_manager.isTenantAdmin(user_role) and tenantid_to_update == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_disable_user( Username=user['userName'], UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to disable users") return utils.create_success_response("Users disabled") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def enable_users_by_tenant(event, context): logger.info("Request received to enable users by tenant") tenantid_to_update = event['tenantId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if (auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_enable_user( Username=user['userName'], UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to enable users") return utils.create_success_response("Users enables") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() def get_user_info(event, user_pool_id, user_name): metrics_manager.record_metric(event, "UserInfoRequested", "Count", 1) response = client.admin_get_user( UserPoolId=user_pool_id, Username=user_name ) logger.log_with_tenant_context(event, response) user_info = UserInfo() user_info.user_name = response["Username"] for attr in response["UserAttributes"]: if(attr["Name"] == "custom:tenantId"): user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] logger.log_with_tenant_context(event, user_info) return user_info class UserManagement: def create_user_group(self, user_pool_id, group_name, group_description): response = client.create_group( GroupName=group_name, UserPoolId=user_pool_id, Description= group_description, Precedence=0 ) return response def create_tenant_admin(self, user_pool_id, tenant_admin_user_name, user_details): response = client.admin_create_user( Username=tenant_admin_user_name, UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['tenantEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': 'TenantAdmin' }, { 'Name': 'custom:tenantId', 'Value': user_details['tenantId'] } ] ) return response def add_user_to_group(self, user_pool_id, user_name, group_name): response = client.admin_add_user_to_group( UserPoolId=user_pool_id, Username=user_name, GroupName=group_name ) return response def create_user_tenant_mapping(self, user_name, tenant_id): response = table_tenant_user_map.put_item( Item={ 'tenantId': tenant_id, 'userName': user_name } ) return response class UserInfo: def __init__(self, user_name=None, tenant_id=None, user_role=None, email=None, status=None, enabled=None, created=None, modified=None): self.user_name = user_name self.tenant_id = tenant_id self.user_role = user_role self.email = email self.status = status self.enabled = enabled self.created = created self.modified = modified ================================================ FILE: Lab3/server/layers/auth_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils # These are the roles being supported in this reference architecture class UserRoles: SYSTEM_ADMIN = "SystemAdmin" CUSTOMER_SUPPORT = "CustomerSupport" TENANT_ADMIN = "TenantAdmin" TENANT_USER = "TenantUser" def isTenantAdmin(user_role): if (user_role == UserRoles.TENANT_ADMIN): return True else: return False def isSystemAdmin(user_role): if (user_role == UserRoles.SYSTEM_ADMIN): return True else: return False def isSaaSProvider(user_role): if (user_role == UserRoles.SYSTEM_ADMIN or user_role == UserRoles.CUSTOMER_SUPPORT): return True else: return False def isTenantUser(user_role): if (user_role == UserRoles.TENANT_USER): return True else: return False ================================================ FILE: Lab3/server/layers/logger.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from aws_lambda_powertools import Logger logger = Logger() """Log info messages """ def info(log_message): logger.info (log_message) """Log error messages """ def error(log_message): logger.error (log_message) def log_with_tenant_context(event, log_message): logger.structure_logs(append=True, tenant_id= event['requestContext']['authorizer']['tenantId']) logger.info (log_message) ================================================ FILE: Lab3/server/layers/metrics_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json from aws_lambda_powertools import Metrics metrics = Metrics() #TODO: Implement the below method def record_metric(event, metric_name, metric_unit, metric_value): pass ================================================ FILE: Lab3/server/layers/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] simplejson jsonpickle aws_requests_auth ================================================ FILE: Lab3/server/layers/utils.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import jsonpickle import simplejson import boto3 from aws_requests_auth.aws_auth import AWSRequestsAuth from enum import Enum class StatusCodes(Enum): SUCCESS = 200 UN_AUTHORIZED = 401 NOT_FOUND = 404 def create_success_response(message): return { "statusCode": StatusCodes.SUCCESS.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def create_unauthorized_response(): return { "statusCode": StatusCodes.UN_AUTHORIZED.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": "User not authorized to perform this action" }), } def generate_response(inputObject): return { "statusCode": 200, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": encode_to_json_object(inputObject), } def encode_to_json_object(inputObject): jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True) jsonpickle.set_preferred_backend('simplejson') return jsonpickle.encode(inputObject, unpicklable=False, use_decimal=True) def get_auth(host, region): session = boto3.Session() credentials = session.get_credentials() auth = AWSRequestsAuth(aws_access_key=credentials.access_key, aws_secret_access_key=credentials.secret_key, aws_token=credentials.token, aws_host=host, aws_region=region, aws_service='execute-api') return auth def get_headers(event): return event['headers'] ================================================ FILE: Lab3/server/nested_templates/apigateway.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway, apis, api keys and usage plan as part of bootstrap Parameters: StageName: Type: String RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String Resources: ApiGatewayCloudWatchLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub apigateway-cloudwatch-publish-role-${AWS::Region} Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole ApiGatewayAttachCloudwatchLogArn: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchLogRole.Arn AdminApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/api-gateway/access-logs-serverless-saas-admin-api RetentionInDays: 30 AdminApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: "/*" HttpMethod: "*" Auth: ResourcePolicy: CustomStatements: - Effect: Allow Principal: "*" Action: "execute-api:Invoke" Resource: ["execute-api:/*/*/*"] - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/tenant" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/user/tenant-admin" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref RegisterTenantLambdaExecutionRoleArn - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/disable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/enable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn AccessLogSetting: DestinationArn: !GetAtt AdminApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ["", ["serverless-saas-admin-api-", !Ref "AWS::Region"]] basePath: !Join ["", ["/", !Ref StageName]] schemes: - https paths: /registration: post: summary: Register a new tenant description: Register a new tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref RegisterTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant/activation/{tenantid}: put: security: - api_key: [] - Authorizer: [] summary: Activate an existing tenant description: Activate an existing tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ActivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenants: get: summary: Returns all tenants description: Returns all tenants produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantsFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant: post: summary: Creates a tenant description: Creates a tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/{tenantid}: get: summary: Returns a tenant description: Return a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Disables a tenant description: Disables a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeactivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy put: summary: Updates a tenant description: Updates a tenant produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user/{username}: get: summary: Returns a user description: Return a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUserFunctionArn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Diables a user description: Disable a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUserFunctionArn - /invocations httpMethod: POST type: aws_proxy /user/tenant-admin: post: summary: Creates a tenant admin user description: Creates a tenant admin user produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantAdminUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user: post: summary: Create a user description: Create a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users: get: summary: Get all users by tenantId description: Get all users by tenantId produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUsersFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/disable: put: summary: disable users by tenant id description: disable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/enable: put: summary: enable users by tenant id description: enable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref EnableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: sigv4Reference: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "awsSigv4" Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref AuthorizerFunctionArn - /invocations authorizerResultTtlInSeconds: 60 type: "token" StageName: prod Outputs: AdminApiGatewayApi: Value: !Ref AdminApiGatewayApi ================================================ FILE: Lab3/server/nested_templates/apigateway_lambdapermissions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway and apis as part of bootstrap Parameters: RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String AdminApiGatewayApi: Type: String Resources: #provide api gateway permissions to call lambda functions RegisterTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref RegisterTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateTenantAdminUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantAdminUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] EnableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref EnableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUsersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUsersFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref AuthorizerFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*" ]] CreateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantsFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeactivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeactivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ActivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ActivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ================================================ FILE: Lab3/server/nested_templates/cognito.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup cognito as part of bootstrap Parameters: AdminEmailParameter: Type: String Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Description: "Enter the role name for system admin" AdminUserPoolCallbackURLParameter: Type: String Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Description: "Enter Tenant Management userpool call back url" Resources: CognitoUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: PooledTenant-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into tenant UI application at " - "https://" - !Ref TenantUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for tenant UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSClient GenerateSecret: false UserPoolId: !Ref CognitoUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [pooledtenant-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoUserPool CognitoOperationUsersUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: OperationUsers-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into admin UI application at " - "https://" - !Ref AdminUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for admin UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoOperationUsersUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSOperationUsersPoolClient GenerateSecret: false UserPoolId: !Ref CognitoOperationUsersUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoOperationUsersUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [operationsusers-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUserGroup: Type: AWS::Cognito::UserPoolGroup Properties: GroupName: SystemAdmins Description: Admin user group Precedence: 0 UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUser: Type: AWS::Cognito::UserPoolUser Properties: Username: admin DesiredDeliveryMediums: - EMAIL ForceAliasCreation: true UserAttributes: - Name: email Value: !Ref AdminEmailParameter - Name: custom:tenantId Value: system_admins - Name: custom:userRole Value: !Ref SystemAdminRoleNameParameter UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAddUserToGroup: Type: AWS::Cognito::UserPoolUserToGroupAttachment Properties: GroupName: !Ref CognitoAdminUserGroup Username: !Ref CognitoAdminUser UserPoolId: !Ref CognitoOperationUsersUserPool Outputs: CognitoUserPoolId: Value: !Ref CognitoUserPool CognitoUserPoolClientId: Value: !Ref CognitoUserPoolClient CognitoOperationUsersUserPoolId: Value: !Ref CognitoOperationUsersUserPool CognitoOperationUsersUserPoolClientId: Value: !Ref CognitoOperationUsersUserPoolClient CognitoOperationUsersUserPoolProviderURL: Value: !GetAtt CognitoOperationUsersUserPool.ProviderURL ================================================ FILE: Lab3/server/nested_templates/lambdafunctions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy lambda functions as part of bootstrap Parameters: CognitoOperationUsersUserPoolId: Type: String CognitoOperationUsersUserPoolClientId: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String TenantDetailsTableArn: Type: String TenantUserMappingTableArn: Type: String Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies Description: Utilities for project ContentUri: ../layers/ CompatibleRuntimes: - python3.9 LicenseInfo: "MIT" RetentionPolicy: Retain Metadata: BuildMethod: python3.9 AuthorizerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: authorizer-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: authorizer-execution-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:List* Resource: - !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn SharedServicesAuthorizerFunction: Type: AWS::Serverless::Function DependsOn: AuthorizerExecutionRole Properties: CodeUri: ../Resources/ Handler: shared_service_authorizer.lambda_handler Runtime: python3.9 Role: !GetAtt AuthorizerExecutionRole.Arn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !Ref CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !Ref CognitoOperationUsersUserPoolClientId TENANT_USER_POOL: !Ref CognitoUserPoolId TENANT_APP_CLIENT: !Ref CognitoUserPoolClientId #Create user pool for the tenant TenantUserPoolLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-userpool-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-userpool-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn - Effect: Allow Action: - dynamodb:GetItem - dynamodb:Query Resource: - !Ref TenantUserMappingTableArn CreateUserLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub create-user-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-user-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:PutItem Resource: - !Ref TenantUserMappingTableArn - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn CreateTenantAdminUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_tenant_admin_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId TENANT_APP_CLIENT_ID: !Ref CognitoUserPoolClientId POWERTOOLS_SERVICE_NAME: "UserManagement.CreateTenantAdmin" #User management CreateUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.CreateUser" UpdateUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.update_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.UpdateUser" DisableUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUser" DisableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUsersByTenant" EnableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.enable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.EnableUsersByTenant" GetUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.GetUser" GetUsersFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_users Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.GetUsers" #Tenant Management TenantManagementLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-management-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-tenant-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:Scan - dynamodb:Query Resource: - !Ref TenantDetailsTableArn - !Join ["", [!Ref TenantDetailsTableArn, '/index/*']] CreateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.create_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.CreateTenant" ActivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.activate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.ActivateTenant" ENABLE_USERS_BY_TENANT: "/users/enable" PROVISION_TENANT: "/provisioning/" GetTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.GetTenant" DeactivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.deactivate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.DeactivateTenant" DEPROVISION_TENANT: "/provisioning/" DISABLE_USERS_BY_TENANT: "/users/disable" UpdateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.update_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.UpdateTenant" GetTenantsFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenants Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers #Tenant Registration RegisterTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-registration-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess RegisterTenantFunction: Type: AWS::Serverless::Function DependsOn: RegisterTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-registration.register_tenant Runtime: python3.9 Role: !GetAtt RegisterTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: # Need to find a better way than hard coding resource paths CREATE_TENANT_ADMIN_USER_RESOURCE_PATH: "/user/tenant-admin" CREATE_TENANT_RESOURCE_PATH: "/tenant" PROVISION_TENANT_RESOURCE_PATH: "/provisioning" POWERTOOLS_SERVICE_NAME: "TenantRegistration.RegisterTenant" Outputs: RegisterTenantLambdaExecutionRoleArn: Value: !GetAtt RegisterTenantLambdaExecutionRole.Arn TenantManagementLambdaExecutionRoleArn: Value: !GetAtt TenantManagementLambdaExecutionRole.Arn RegisterTenantFunctionArn: Value: !GetAtt RegisterTenantFunction.Arn ActivateTenantFunctionArn: Value: !GetAtt ActivateTenantFunction.Arn GetTenantsFunctionArn: Value: !GetAtt GetTenantsFunction.Arn CreateTenantFunctionArn: Value: !GetAtt CreateTenantFunction.Arn GetTenantFunctionArn: Value: !GetAtt GetTenantFunction.Arn DeactivateTenantFunctionArn: Value: !GetAtt DeactivateTenantFunction.Arn UpdateTenantFunctionArn: Value: !GetAtt UpdateTenantFunction.Arn GetUsersFunctionArn: Value: !GetAtt GetUsersFunction.Arn GetUserFunctionArn: Value: !GetAtt GetUserFunction.Arn UpdateUserFunctionArn: Value: !GetAtt UpdateUserFunction.Arn DisableUserFunctionArn: Value: !GetAtt DisableUserFunction.Arn CreateTenantAdminUserFunctionArn: Value: !GetAtt CreateTenantAdminUserFunction.Arn CreateUserFunctionArn: Value: !GetAtt CreateUserFunction.Arn DisableUsersByTenantFunctionArn: Value: !GetAtt DisableUsersByTenantFunction.Arn EnableUsersByTenantFunctionArn: Value: !GetAtt EnableUsersByTenantFunction.Arn SharedServicesAuthorizerFunctionArn: Value: !GetAtt SharedServicesAuthorizerFunction.Arn ================================================ FILE: Lab3/server/nested_templates/tables.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to create dynamodb tables as part of bootstrap Resources: TenantDetailsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: tenantName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: ServerlessSaas-TenantConfig KeySchema: - AttributeName: tenantName KeyType: HASH Projection: NonKeyAttributes: - userPoolId - appClientId - apiGatewayUrl ProjectionType: INCLUDE TableName: ServerlessSaaS-TenantDetails TenantUserMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: userName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH - AttributeName: userName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantUserMapping GlobalSecondaryIndexes: - IndexName: UserName KeySchema: - AttributeName: userName KeyType: HASH - AttributeName: tenantId KeyType: RANGE Projection: ProjectionType: ALL Outputs: TenantDetailsTableArn: Value: !GetAtt TenantDetailsTable.Arn TenantDetailsTableName: Value: !Ref TenantDetailsTable TenantUserMappingTableArn: Value: !GetAtt TenantUserMappingTable.Arn TenantUserMappingTableName: Value: !Ref TenantUserMappingTable ================================================ FILE: Lab3/server/nested_templates/userinterface.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: IsCloudFrontAndS3PreProvisioned: Type: String Default: false Description: "Tells if cloudfront and s3 buckets are pre-provisioned or not. They get pre-provisioned when the workshop is running as a part of AWS Event through AWS event engine tool." Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref IsCloudFrontAndS3PreProvisioned, true] ] Resources: CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: IsNotRunningInEventEngine Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distributions" AdminAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AdminAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AdminAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AdminAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId AdminAppSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: #Aliases: # - !Sub 'admin.${CustomDomainName}' CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS CachedMethods: - GET - HEAD - OPTIONS Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: adminapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt AdminAppBucket.RegionalDomainName Id: adminapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' LandingAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True LandingAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref LandingAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${LandingAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId LandingApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: landingapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'LandingAppBucket.RegionalDomainName' Id: landingapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' AppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId ApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: tenantapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'AppBucket.RegionalDomainName' Id: tenantapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: AdminBucket: Description: The name of the bucket for uploading the Admin Management site to Value: !Ref AdminAppBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt AdminAppSite.DomainName Condition: IsNotRunningInEventEngine LandingAppBucket: Description: The name of the bucket for uploading the Landing site to Value: !Ref LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt LandingApplicationSite.DomainName Condition: IsNotRunningInEventEngine AppBucket: Description: The name of the bucket for uploading the Tenant Management site to Value: !Ref AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt ApplicationSite.DomainName Condition: IsNotRunningInEventEngine ================================================ FILE: Lab3/server/shared-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas" s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx" s3_prefix = "serverless-saas" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" cached="true" parallel="true" ================================================ FILE: Lab3/server/shared-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to Bootstrap the Common Resources Parameters: AdminEmailParameter: Type: String Default: "test@test.com" Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Default: "SystemAdmin" Description: "Enter the role name for system admin" StageName: Type: String Default: "prod" Description: "Stage Name for the api" EventEngineParameter: Type: String Default: false Description: "Tells if this workshop is running as a part of AWS Event through AWS EventEngine or not" AdminUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Tenant Management userpool call back url" Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref EventEngineParameter, true] ] Resources: DynamoDBTables: Type: AWS::Serverless::Application Properties: Location: nested_templates/tables.yaml #Create cloudfront and s3 for UI Cde UserInterface: Type: AWS::Serverless::Application Properties: Location: nested_templates/userinterface.yaml Parameters: IsCloudFrontAndS3PreProvisioned: !Ref EventEngineParameter Cognito: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/cognito.yaml Parameters: AdminEmailParameter: !Ref AdminEmailParameter SystemAdminRoleNameParameter: !Ref SystemAdminRoleNameParameter AdminUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.AdminAppSite, !Ref AdminUserPoolCallbackURLParameter] TenantUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.ApplicationSite, !Ref TenantUserPoolCallbackURLParameter] LambdaFunctions: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/lambdafunctions.yaml Parameters: CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId CognitoOperationUsersUserPoolId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId CognitoOperationUsersUserPoolClientId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId TenantDetailsTableArn: !GetAtt DynamoDBTables.Outputs.TenantDetailsTableArn TenantUserMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantUserMappingTableArn APIs: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway.yaml Parameters: StageName: !Ref StageName RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn APIGatewayLambdaPermissions: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway_lambdapermissions.yaml Parameters: RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn AdminApiGatewayApi: !GetAtt APIs.Outputs.AdminApiGatewayApi Outputs: AdminApi: Description: "API Gateway endpoint URL for Admin API" Value: !Join ["", ["https://", !GetAtt APIs.Outputs.AdminApiGatewayApi, ".execute-api.", !Ref "AWS::Region", ".amazonaws.com/", !Ref StageName]] AdminSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant administration application Value: !GetAtt UserInterface.Outputs.AdminBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt UserInterface.Outputs.AdminAppSite Condition: IsNotRunningInEventEngine LandingApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the landing application Value: !GetAtt UserInterface.Outputs.LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt UserInterface.Outputs.LandingApplicationSite Condition: IsNotRunningInEventEngine ApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant application Value: !GetAtt UserInterface.Outputs.AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt UserInterface.Outputs.ApplicationSite Condition: IsNotRunningInEventEngine CognitoOperationUsersUserPoolProviderURL: Description: The Admin Management userpool provider url Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolProviderURL CognitoOperationUsersUserPoolClientId: Description: The Admin Management userpool client id Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId CognitoTenantUserPoolId: Description: The user pool id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolId Export: Name: "Serverless-SaaS-CognitoTenantUserPoolId" CognitoTenantAppClientId: Description: The app client id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolClientId Export: Name: "Serverless-SaaS-CognitoTenantAppClientId" ================================================ FILE: Lab3/server/tenant-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "stack-pooled" s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx" s3_prefix = "serverless-saas-tenant" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM" cached="true" parallel="true" ================================================ FILE: Lab3/server/tenant-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Serverless SaaS Reference Architecture Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Parameters: StageName: Type: String Default: "prod" Description: "Stage Name for the api" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies-pooled Description: Utilities for project ContentUri: layers/ CompatibleRuntimes: - python3.9 LicenseInfo: 'MIT' RetentionPolicy: Retain Metadata: BuildMethod: python3.9 ProductTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: productId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: productId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: Product-pooled OrderTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: orderId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: orderId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: Order-pooled ProductFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: pooled-product-function-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: pooled-product-function-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt ProductTable.Arn GetProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable GetProductsFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_products Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable CreateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.create_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable UpdateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.update_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable DeleteProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.delete_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable OrderFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: pooled-order-function-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: pooled-order-function-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt OrderTable.Arn GetOrdersFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_orders Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable GetOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable CreateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.create_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable UpdateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.update_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable DeleteOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.delete_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable #Tenant Authorizer TenantAuthorizerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: tenant-authorizer-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: tenant-authorizer-execution-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:List* Resource: - !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* BusinessServicesAuthorizerFunction: Type: AWS::Serverless::Function DependsOn: TenantAuthorizerExecutionRole Properties: CodeUri: Resources/ Handler: tenant_authorizer.lambda_handler Runtime: python3.9 Role: !GetAtt TenantAuthorizerExecutionRole.Arn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL: !ImportValue Serverless-SaaS-CognitoTenantUserPoolId TENANT_APP_CLIENT: !ImportValue Serverless-SaaS-CognitoTenantAppClientId ApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/api-gateway/access-logs-serverless-saas-tenant-api-pooled RetentionInDays: 30 ApiGatewayTenantApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: '/*' HttpMethod: '*' AccessLogSetting: DestinationArn: !GetAtt ApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: 'pooled-serverless-saas-tenant-api' basePath: !Join ['', ['/', !Ref StageName]] x-amazon-apigateway-api-key-source : "AUTHORIZER" schemes: - https paths: /order/{id}: get: summary: Returns a order description: Return a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a order description: Deletes a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /orders: get: summary: Returns all orders description: Returns all orders. produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrdersFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /order: post: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product/{id}: get: summary: Returns a product description: Return a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a product description: Deletes a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /products: get: summary: Returns all products description: Returns all products. produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductsFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product: post: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt BusinessServicesAuthorizerFunction.Arn - /invocations authorizerResultTtlInSeconds: 30 type: "token" StageName: !Ref StageName GetProductsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt GetProductsFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrdersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrdersFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt BusinessServicesAuthorizerFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref ApiGatewayTenantApi, "/*/*" ]] Outputs: TenantApiGatewayId: Description: Id for Tenant API Gateway Value: !Ref ApiGatewayTenantApi TenantAPI: Description: "API Gateway endpoint URL for Tenant API" Value: !Join ['', [!Sub "https://${ApiGatewayTenantApi}.execute-api.${AWS::Region}.amazonaws.com/", !Ref StageName]] ================================================ FILE: Lab4/client/Admin/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab4/client/Admin/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab4/client/Admin/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab4/client/Admin/README.md ================================================ # Admin This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab4/client/Admin/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "admin": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "admin:build:production" }, "development": { "browserTarget": "admin:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "admin:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab4/client/Admin/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab4/client/Admin/package.json ================================================ { "name": "admin", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/flex-layout": "~14.0.0-beta.40", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.1.3", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab4/client/Admin/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Tenants', url: '/tenants', icon: 'groups', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Lab4/client/Admin/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: '', component: NavComponent, data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'tenants', loadChildren: () => import('./views/tenants/tenants.module').then((m) => m.TenantsModule), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab4/client/Admin/src/app/app.component.scss ================================================ ================================================ FILE: Lab4/client/Admin/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Lab4/client/Admin/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { HttpInterceptorProviders } from './interceptors'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; @NgModule({ declarations: [AppComponent, NavComponent, AuthComponent], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, HttpClientModule, HttpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab4/client/Admin/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Lab4/client/Admin/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const HttpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Lab4/client/Admin/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Lab4/client/Admin/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Lab4/client/Admin/src/app/nav/nav.component.css ================================================ .sidenav-container { height: 100%; } .sidenav-content-container { background-color: lightgray; } .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .mat-toolbar.mat-primary { position: sticky; top: 0; z-index: 1; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .spinner-container { height: 80%; display: flex; justify-content: center; align-items: center; box-sizing: border-box; } ================================================ FILE: Lab4/client/Admin/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ username$ | async }}
================================================ FILE: Lab4/client/Admin/src/app/nav/nav.component.spec.ts ================================================ import { LayoutModule } from '@angular/cdk/layout'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { NavComponent } from './nav.component'; describe('NavComponent', () => { let component: NavComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [NavComponent], imports: [ NoopAnimationsModule, LayoutModule, MatButtonModule, MatIconModule, MatListModule, MatSidenavModule, MatToolbarModule, ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(NavComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should compile', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: Lab4/client/Admin/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.css'], }) export class NavComponent implements OnInit { tenantName = ''; loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router ) { this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => err); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map((sesh) => sesh && sesh.isValid()) ); const token$ = session$.pipe(map((sesh) => sesh && sesh.getIdToken())); this.username$ = token$.pipe( map((t) => t && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } } ================================================ FILE: Lab4/client/Admin/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Lab4/client/Admin/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Lab4/client/Admin/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Lab4/client/Admin/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Lab4/client/Admin/src/app/views/dashboard/dashboard.component.html ================================================

Traffic

December 2020
================================================ FILE: Lab4/client/Admin/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } .chart-wrapper { padding-right: 20px; position: relative; margin: auto; height: 80vh; width: 80vw; } ================================================ FILE: Lab4/client/Admin/src/app/views/dashboard/dashboard.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ChartConfiguration, ChartType } from 'chart.js'; import { TenantsService } from '../tenants/tenants.service'; interface DataSet { label: string; data: number[]; } interface ChartData { tenantId: string; dataSet: DataSet[]; totalOrders: number; } @Component({ templateUrl: 'dashboard.component.html', styleUrls: ['./dashboard.component.scss'], selector: 'app-dashboard', }) export class DashboardComponent implements OnInit { constructor(private tenantSvc: TenantsService) {} data: ChartData[] = []; // lineChart public lineChartElements = 27; public lineChartData1: Array = []; public lineChartData2: Array = []; public lineChartData3: Array = []; public lineChartData: Array = [ { data: this.lineChartData1, label: 'Current', backgroundColor: 'rgba(148,159,177,0.2)', borderColor: 'rgba(148,159,177,1)', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, { data: this.lineChartData2, label: 'Previous', backgroundColor: 'rgba(77,83,96,0.2)', borderColor: 'rgba(77,83,96,1)', pointBackgroundColor: 'rgba(77,83,96,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(77,83,96,1)', fill: 'origin', }, { data: this.lineChartData3, label: 'BEP', backgroundColor: 'rgba(255,0,0,0.3)', borderColor: 'red', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, ]; public lineChartLegend = false; /* tslint:disable:max-line-length */ public lineChartLabels: Array = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ]; /* tslint:enable:max-line-length */ public lineChartType: ChartType = 'line'; public lineChartOptions: ChartConfiguration['options'] = { maintainAspectRatio: false, elements: { line: { tension: 0.5, }, }, scales: { // We use this empty structure as a placeholder for dynamic theming. x: {}, 'y-axis-0': { position: 'left', }, 'y-axis-1': { position: 'right', grid: { color: 'rgba(80,80,80,0.3)', }, ticks: { color: '#808080', }, }, }, }; public random(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } ngOnInit(): void { for (let i = 0; i <= this.lineChartElements; i++) { this.lineChartData1.push(this.random(50, 200)); this.lineChartData2.push(this.random(80, 100)); this.lineChartData3.push(65); } } } ================================================ FILE: Lab4/client/Admin/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Lab4/client/Admin/src/app/views/tenants/create/create.component.html ================================================
Provision tenant Name Email Phone Address Plan
================================================ FILE: Lab4/client/Admin/src/app/views/tenants/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab4/client/Admin/src/app/views/tenants/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { TenantsService } from '../tenants.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { submitting = false; tenantForm: FormGroup = this.fb.group({ tenantName: [null, [Validators.required]], tenantEmail: [null, [Validators.email, Validators.required]], tenantTier: [null, [Validators.required]], tenantPhone: [null], tenantAddress: [null], }); constructor( private fb: FormBuilder, private tenantSvc: TenantsService, private router: Router, private _snackBar: MatSnackBar ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; const user = { ...this.tenantForm.value, }; this.tenantSvc.post(this.tenantForm.value).subscribe({ next: () => { this.submitting = false; this.openErrorMessageSnackBar('Successfully created new tenant!'); this.router.navigate(['tenants']); }, error: (err) => { this.submitting = false; this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); }, }); } } ================================================ FILE: Lab4/client/Admin/src/app/views/tenants/list/list.component.html ================================================
Tenant List
Id {{ element.tenantId }} Tenant Name {{ element.tenantName }} E-Mail {{ element.tenantEmail }} Plan {{ element.tenantTier }} Status {{ element.isActive }}
================================================ FILE: Lab4/client/Admin/src/app/views/tenants/list/list.component.scss ================================================ .tenant-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab4/client/Admin/src/app/views/tenants/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Tenant } from '../models/tenant'; import { TenantsService } from '../tenants.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { tenants$ = new Observable(); tenantData: Tenant[] = []; isLoading: boolean = true; displayedColumns = [ 'tenantId', 'tenantName', 'tenantEmail', 'tenantTier', 'isActive', ]; constructor(private tenantSvc: TenantsService) {} ngOnInit(): void { this.tenantSvc.fetch().subscribe((data) => { this.tenantData = data; this.isLoading = false; }); } } ================================================ FILE: Lab4/client/Admin/src/app/views/tenants/models/tenant.ts ================================================ export interface Tenant { tenantId: string; tenantName: string; tenantEmail: string; tenantTier: string; isActive: boolean; } ================================================ FILE: Lab4/client/Admin/src/app/views/tenants/tenants-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', component: ListComponent, data: { title: 'Tenant List', }, }, { path: 'create', component: CreateComponent, data: { title: 'Provision new tenant', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class TenantsRoutingModule {} ================================================ FILE: Lab4/client/Admin/src/app/views/tenants/tenants.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ListComponent } from './list/list.component'; import { TenantsRoutingModule } from './tenants-routing.module'; import { CreateComponent } from './create/create.component'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, TenantsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ReactiveFormsModule, MatSnackBarModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class TenantsModule {} ================================================ FILE: Lab4/client/Admin/src/app/views/tenants/tenants.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Tenant } from './models/tenant'; @Injectable({ providedIn: 'root', }) export class TenantsService { constructor(private http: HttpClient) {} baseUrl = `${environment.apiUrl}`; tenantsApiUrl = `${this.baseUrl}/tenants`; registrationApiUrl = `${this.baseUrl}/registration`; // TODO strongly-type these anys as tenants once we dial in what the tenant call should return fetch(): Observable { return this.http.get(this.tenantsApiUrl); } post(tenant: Tenant): Observable { return this.http.post(this.registrationApiUrl, tenant); } } ================================================ FILE: Lab4/client/Admin/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
Tenant Id folder_special
================================================ FILE: Lab4/client/Admin/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab4/client/Admin/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], tenantId: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Lab4/client/Admin/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Lab4/client/Admin/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab4/client/Admin/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Lab4/client/Admin/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Lab4/client/Admin/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Lab4/client/Admin/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Lab4/client/Admin/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.apiUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } } ================================================ FILE: Lab4/client/Admin/src/assets/.gitkeep ================================================ ================================================ FILE: Lab4/client/Admin/src/aws-exports.ts ================================================ const awsmobile = { aws_project_region: 'us-west-2', aws_cognito_region: 'us-west-2', aws_user_pools_id: 'us-west-2_nWhwjijMc', aws_user_pools_web_client_id: '61ipvhmvdrfso0cftpil9irk8k', }; export default awsmobile; ================================================ FILE: Lab4/client/Admin/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab4/client/Admin/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab4/client/Admin/src/environments/environment.ts ================================================ export const environment = { production: false, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab4/client/Admin/src/index.html ================================================ Dashboard ================================================ FILE: Lab4/client/Admin/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { Amplify } from 'aws-amplify'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import aws_exports from './aws-exports'; Amplify.configure(aws_exports); if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab4/client/Admin/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab4/client/Admin/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab4/client/Admin/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab4/client/Admin/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; ================================================ FILE: Lab4/client/Admin/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab4/client/Admin/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab4/client/Admin/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab4/client/Admin/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab4/client/Application/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab4/client/Application/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab4/client/Application/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end #cypress cypress/videos/* cypress.env.json # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab4/client/Application/README.md ================================================ # Application This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab4/client/Application/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "application": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "2mb", "maximumError": "2mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "6kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "application:build:production" }, "development": { "browserTarget": "application:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "application:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab4/client/Application/cypress/README.md ================================================ # Application End-to-End Testing ## Instructions To run End-to-End (e2e) tests against the Sample Application, take the following steps: 1. Make a copy of the example env file (`cypress.env.json.example`): ```bash cp cypress.env.json.example cypress.env.json ``` 2. Edit the new file and replace the sample values with real values. The following should help when deciding what to use in place of the sample values provided: - `host`: The URL where the Sample Application is running. If testing locally, this is usually `"http://localhost:4200"`. - `tenantName`: The name of the tenant used to identify the appropriate Cognito User Pool to use for Authentication. - `tenantUsername`: The username to use when logging in. - `tenantUserPassword`: The password to use when logging in. - `email`: The email address to use for testing. (This should be a valid email address.) 3. Navigate to the root of the Application project (`aws-saas-factory-ref-solution-serverless-saas/clients/Application/`) and run the following: ```bash npx cypress run ``` This will run the tests located in the `cypress/e2e` folder. The documentation [here](https://docs.cypress.io/guides/guides/command-line#cypress-run) has more information on what can be passed in as arguments when running the Cypress tests. For example, running the following will show the Cypress UI and what is happening as each of the tests are run: ```bash npx cypress run --headed ``` ================================================ FILE: Lab4/client/Application/cypress/e2e/1-getting-started/basic-access.cy.js ================================================ /// describe('check that the app redirects to /unauthorized when tenant is not set', () => { it('redirects to unauthorized when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); }) it('redirects to unauthorized when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); }) it('redirects to unauthorized when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); }) }) describe('check that the app redirects to a page with a sign-in form when tenant is set and user is not logged in', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').should('exist') cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.location('href').should('contain', '/dashboard') }) it('redirects when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) }) ================================================ FILE: Lab4/client/Application/cypress/e2e/1-getting-started/product-testing.cy.js ================================================ /// describe('check that product, order and user functionality works as expected', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.get('form input[name="username"]').type(Cypress.env('tenantUsername')) cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() cy.wait(1500) cy.get('body').then(body => { if (body.find('form input[name="confirm_password"]').length > 0) { cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form input[name="confirm_password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() } }) }) it('can create new users and display them', () => { const email_username = Cypress.env('email').split('@')[0]; const email_domain = Cypress.env('email').split('@')[1]; const random_suffix = '+test'+Date.now().toString().slice(-3); const myUser = { name: "myUser-"+Date.now(), email: email_username + random_suffix + '@' + email_domain, role: 'userRole'+Date.now().toString().slice(-5) } cy.get("a").contains("Users").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.get("button[color='primary'").contains("Add User").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="userName"]').type(myUser.name) cy.get('input[formcontrolname="userEmail"]').type(myUser.email) cy.get('input[formcontrolname="userRole"]').type(myUser.role) }) cy.intercept({ method: 'POST', url: '**/user', }).as('postUser') cy.intercept({ method: 'GET', url: '**/users', }).as('getUsers') cy.get("button").contains("Create").click() cy.wait('@postUser') cy.go('back') cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.wait('@getUsers') cy.get('table td').contains(myUser.email) }) it('can create a new order with a new product and see them listed', () => { const myProduct = { name: "myProduct-"+Date.now(), price: Date.now().toString().slice(-3), sku: Date.now().toString().slice(-5), category: "category3", } const myOrder = { name: "myOrder-"+Date.now(), } // NOW TESTING PRODUCT CREATION // cy.get("a").contains("Products").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.get("button[color='primary'").contains("Create Product").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="name"]').type(myProduct.name) cy.get('input[formcontrolname="price"]').type(myProduct.price) cy.get('input[formcontrolname="sku"]').type(myProduct.sku) cy.get('mat-select[formcontrolname="category"]').click() }) cy.get('.mat-option-text').contains(myProduct.category).click() cy.get("button").contains("Submit").should('not.be.disabled') cy.intercept({ method: 'POST', url: '**/product', }).as('postProduct') cy.intercept({ method: 'GET', url: '**/products', }).as('getProducts') cy.get("button").contains("Submit").click() cy.wait('@postProduct') cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.wait('@getProducts') cy.get('table td').contains(myProduct.name) cy.get('table td').contains(myProduct.price) cy.get("a").contains("Orders").click() // DONE TESTING PRODUCT CREATION // // NOW TESTING ORDER CREATION // cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.get("button[color='primary']").contains("Create Order").click() cy.location().should((loc) => { expect(loc.href).to.contain('/orders/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="orderName"]').type(myOrder.name) }) cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.intercept({ method: 'POST', url: '**/order', }).as('postOrder') cy.intercept({ method: 'GET', url: '**/orders', }).as('getOrders') cy.get("button").contains("Submit").click() cy.wait('@postOrder') cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.wait('@getOrders') cy.get('table td').contains(myOrder.name) cy.get('table td').contains(new Intl.NumberFormat().format(myProduct.price * 2)) // DONE TESTING ORDER CREATION // }) }) ================================================ FILE: Lab4/client/Application/cypress.config.ts ================================================ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { supportFile: false, setupNodeEvents(on, config) { // implement node event listeners here }, }, }); ================================================ FILE: Lab4/client/Application/cypress.env.json.example ================================================ { "host": "http://localhost:4200", "tenantName": "UPDATE_ME!", "tenantUsername": "UPDATE_ME!", "tenantUserPassword": "UPDATE_ME!", "email": "test@example.com" } ================================================ FILE: Lab4/client/Application/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/application'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab4/client/Application/package.json ================================================ { "name": "application", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "e2e": "npx cypress run" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.2.0", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "cypress": "~10.3.1", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab4/client/Application/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Products', url: '/products', icon: 'sell', }, { name: 'Orders', url: '/orders', icon: 'shopping_cart', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Lab4/client/Application/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { CognitoGuard } from './cognito.guard'; export const routes: Routes = [ { path: '', redirectTo: 'unauthorized', pathMatch: 'full', }, { path: '', component: NavComponent, canActivate: [CognitoGuard], data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'orders', loadChildren: () => import('./views/orders/orders.module').then((m) => m.OrdersModule), }, { path: 'products', loadChildren: () => import('./views/products/products.module').then( (m) => m.ProductsModule ), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, { path: 'unauthorized', component: UnauthorizedComponent, }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab4/client/Application/src/app/app.component.scss ================================================ ================================================ FILE: Lab4/client/Application/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Lab4/client/Application/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { httpInterceptorProviders } from './interceptors'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { environment } from 'src/environments/environment'; @NgModule({ declarations: [ AppComponent, NavComponent, AuthComponent, UnauthorizedComponent, ], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, MatSnackBarModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatIconModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, httpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab4/client/Application/src/app/cognito.guard.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, } from '@angular/router'; import { Auth } from 'aws-amplify'; import { AuthConfigurationService } from './views/auth/auth-configuration.service'; @Injectable({ providedIn: 'root' }) export class CognitoGuard implements CanActivate { constructor( private router: Router, private authConfigService: AuthConfigurationService ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Promise { if (!this.authConfigService.configureAmplifyAuth()) { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return new Promise((res, rej) => { res(false); }); } return Auth.currentSession() .then((u) => { if (u.isValid()) { return true; } else { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return false; } }) .catch((e) => { if (state.url === '/dashboard') { // if we're going to the dashboard and we're not logged in, // don't stop the flow as the amplify-authenticator will // route requests going to the dashboard to the sign-in page. return true; } console.log('Error getting current session', e); this.router.navigate(['/unauthorized']); return false; }); } } ================================================ FILE: Lab4/client/Application/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Lab4/client/Application/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const httpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Lab4/client/Application/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Lab4/client/Application/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Lab4/client/Application/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ (companyName$ | async) || "" }} {{ (username$ | async) || user.username}}
================================================ FILE: Lab4/client/Application/src/app/nav/nav.component.scss ================================================ .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .sidebar-icon-container { height: 64px; background-color: whitesmoke; display: flex; align-items: center; justify-content: center; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .footer { position: fixed; bottom: 0; width: 100%; height: 40px; background-color: whitesmoke; color: #2d3337; // text-align: center; margin: 0px; } .footer-text { display: flex; } .content { position: absolute; width: 100%; height: 90%; max-height: 90%; overflow: auto; background-color: lightgray; } ================================================ FILE: Lab4/client/Application/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; import { AuthConfigurationService } from './../views/auth/auth-configuration.service'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.scss'], }) export class NavComponent implements OnInit { loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router, private authConfigService: AuthConfigurationService ) { // this.configSvc.loadConfigurations().subscribe((val) => console.log(val)); this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => { console.log('Failed to get current session. Err: ', err); return err; }); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map( (sesh) => sesh && typeof sesh.isValid === 'function' && sesh.isValid() ) ); const token$ = session$.pipe( map( (sesh) => sesh && typeof sesh.getIdToken === 'function' && sesh.getIdToken() ) ); this.username$ = token$.pipe( map((t) => t && t.payload && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload && t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } async logout() { await Auth.signOut({ global: true }) .then((e) => { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); }) .catch((err) => { console.error('Error logging out: ', err); }); } } ================================================ FILE: Lab4/client/Application/src/app/views/auth/auth-configuration.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpParams, HttpParamsOptions, } from '@angular/common/http'; import { Injectable, OnInit } from '@angular/core'; import { map, switchMap, catchError } from 'rxjs/operators'; import { throwError } from 'rxjs'; import { environment } from '../../../environments/environment'; import { ConfigParams } from './models/config-params'; import { ActivatedRoute } from '@angular/router'; import Amplify from 'aws-amplify'; import { Auth } from 'aws-amplify'; import { Router } from '@angular/router'; import { from, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AuthConfigurationService { params$: Observable; params: ConfigParams; tenantName: string; constructor( private http: HttpClient, private route: ActivatedRoute, private router: Router ) {} public setTenantConfig(tenantName: string): Promise { const url = `${environment.regApiGatewayUrl}/tenant/init/` + tenantName; this.params$ = this.http.get(url); const setup$ = this.params$.pipe( map((val) => { // remove trailing slash (/) if present val.apiGatewayUrl = val.apiGatewayUrl.replace(/\/$/, ''); localStorage.setItem('userPoolId', val.userPoolId); localStorage.setItem('tenantName', tenantName); localStorage.setItem('appClientId', val.appClientId); localStorage.setItem('apiGatewayUrl', val.apiGatewayUrl); return 'success'; }), catchError((error) => { console.log('Error setting tenant config: ', error); return throwError(error); }) ); return setup$.toPromise(); } configureAmplifyAuth(): boolean { try { const userPoolId = localStorage.getItem('userPoolId'); const appClientId = localStorage.getItem('appClientId'); if (!userPoolId || !appClientId) { return false; } const region = userPoolId?.split('_')[0]; const awsmobile = { aws_project_region: region, aws_cognito_region: region, aws_user_pools_id: userPoolId, aws_user_pools_web_client_id: appClientId, }; Amplify.configure(awsmobile); return true; } catch (err) { console.error('Unable to initialize amplify auth.', err); return false; } } cleanLocalStorage() { localStorage.removeItem('tenantName'); localStorage.removeItem('userPoolId'); localStorage.removeItem('appClientId'); localStorage.removeItem('apiGatewayUrl'); } } ================================================ FILE: Lab4/client/Application/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Lab4/client/Application/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Lab4/client/Application/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Lab4/client/Application/src/app/views/auth/models/config-params.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ export interface ConfigParams { appClientId: string; userPoolId: string; apiGatewayUrl: string; } ================================================ FILE: Lab4/client/Application/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Lab4/client/Application/src/app/views/dashboard/dashboard.component.html ================================================

Dashboard

{{ card.title }}
================================================ FILE: Lab4/client/Application/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } ================================================ FILE: Lab4/client/Application/src/app/views/dashboard/dashboard.component.ts ================================================ import { Component, ViewChild } from '@angular/core'; import { map } from 'rxjs/operators'; import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js'; import { BaseChartDirective } from 'ng2-charts'; import DataLabelsPlugin from 'chartjs-plugin-datalabels'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'], }) export class DashboardComponent { @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; public barChartOptions: ChartConfiguration['options'] = { responsive: true, maintainAspectRatio: false, // We use these empty structures as placeholders for dynamic theming. scales: { x: {}, y: { min: 10, }, }, plugins: { legend: { display: true, }, datalabels: { anchor: 'end', align: 'end', }, }, }; public barChartType: ChartType = 'bar'; public barChartPlugins = [DataLabelsPlugin]; public barChartData: ChartData<'bar'> = { labels: ['2006', '2007', '2008', '2009', '2010', '2011', '2012'], datasets: [ { data: [65, 59, 80, 81, 56, 55, 40], label: 'Series A' }, { data: [28, 48, 40, 19, 86, 27, 90], label: 'Series B' }, ], }; // events public chartClicked({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public chartHovered({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public randomize(): void { // Only Change 3 values this.barChartData.datasets[0].data = [ Math.round(Math.random() * 100), 59, 80, Math.round(Math.random() * 100), 56, Math.round(Math.random() * 100), 40, ]; this.chart?.update(); } /** Based on the screen size, switch from standard to one column per row */ cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe( map(({ matches }) => { if (matches) { return [ { title: 'Card 1', cols: 1, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 1 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; } return [ { title: 'Card 1', cols: 2, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 2 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; }) ); constructor(private breakpointObserver: BreakpointObserver) {} } ================================================ FILE: Lab4/client/Application/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Lab4/client/Application/src/app/views/error/404.component.html ================================================

404

Oops! You're lost.

The page you are looking for was not found.

================================================ FILE: Lab4/client/Application/src/app/views/error/404.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '404.component.html', }) export class P404Component { constructor() {} } ================================================ FILE: Lab4/client/Application/src/app/views/error/500.component.html ================================================

500

Houston, we have a problem!

The page you are looking for is temporarily unavailable.

================================================ FILE: Lab4/client/Application/src/app/views/error/500.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '500.component.html', }) export class P500Component { constructor() {} } ================================================ FILE: Lab4/client/Application/src/app/views/error/unauthorized.component.html ================================================
Unauthorized Enter your tenant name and click submit below Tenant Name home
================================================ FILE: Lab4/client/Application/src/app/views/error/unauthorized.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .center-screen { display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; min-height: 100vh; } ================================================ FILE: Lab4/client/Application/src/app/views/error/unauthorized.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AuthConfigurationService } from './../auth/auth-configuration.service'; import { Observable } from 'rxjs'; import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-unauthorized', templateUrl: './unauthorized.component.html', styleUrls: ['./unauthorized.component.scss'], }) export class UnauthorizedComponent implements OnInit { tenantForm: FormGroup; params$: Observable; error = false; errorMessage: string; tenantNameRequired: boolean = false; constructor( private fb: FormBuilder, private authConfigService: AuthConfigurationService, private _snackBar: MatSnackBar, private router: Router ) { if ( environment.userPoolId && environment.appClientId && environment.apiGatewayUrl ) { // If a tenant's cognito configuration is provided in the // "environment" object, then we take that instead of asking // the visitor to provide the name of their tenant in order // to do a look-up for that tenant's cognito configuration. localStorage.setItem('tenantName', 'PooledTenants'); localStorage.setItem('userPoolId', environment.userPoolId); localStorage.setItem('appClientId', environment.appClientId); localStorage.setItem('apiGatewayUrl', environment.apiGatewayUrl); this.tenantNameRequired = false; } } ngOnInit(): void { this.tenantForm = this.fb.group({ tenantName: [null, [Validators.required]], }); } isFieldInvalid(field: string) { const formField = this.tenantForm.get(field); return ( formField && formField.invalid && (formField.dirty || formField.touched) ); } displayFieldCss(field: string) { return { 'is-invalid': this.isFieldInvalid(field), }; } hasRequiredError(field: string) { return !!this.tenantForm.get(field)?.hasError('required'); } openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } login() { if (!this.tenantNameRequired) { this.router.navigate(['/dashboard']); return true; } let tenantName = this.tenantForm.value.tenantName; if (!tenantName) { this.errorMessage = 'No tenant name provided.'; this.error = true; this.openErrorMessageSnackBar(this.errorMessage); return false; } this.authConfigService .setTenantConfig(tenantName) .then((val) => { this.router.navigate(['/dashboard']); }) .catch((errorResponse) => { this.error = true; this.errorMessage = errorResponse.error.message || 'An unexpected error occurred!'; this.openErrorMessageSnackBar(this.errorMessage); }); return false; } } ================================================ FILE: Lab4/client/Application/src/app/views/orders/create/create.component.html ================================================
Create new order Enter order name Name is required
{{ op.product.name }}
{{ op.product.price | currency }}
{{ op.quantity || 0 }}
================================================ FILE: Lab4/client/Application/src/app/views/orders/create/create.component.scss ================================================ // .card { // margin: 20px; // display: flex; // flex-direction: column; // align-items: flex-start; // } .order-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab4/client/Application/src/app/views/orders/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { Product } from '../../products/models/product.interface'; import { ProductService } from '../../products/product.service'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; interface LineItem { product: Product; quantity?: number; } @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { displayedColumns = []; orderForm: FormGroup; orderProducts: LineItem[] = []; isLoadingProducts: boolean = true; error = ''; constructor( private fb: FormBuilder, private productSvc: ProductService, private orderSvc: OrdersService, private router: Router ) { this.orderForm = this.fb.group({ name: ['', Validators.required], }); } ngOnInit(): void { this.productSvc.fetch().subscribe((products) => { this.orderProducts = products.map((p) => ({ product: p })); this.isLoadingProducts = false; }); this.orderForm = this.fb.group({ orderName: ['', Validators.required], }); } get name() { return this.orderForm.get('name'); } get productQuantity() { return this.orderProducts .filter((p) => !!p.quantity).length > 0; } add(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity ? orderProduct.quantity + 1 : 1, }; } return p; }); } remove(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity && orderProduct.quantity > 1 ? orderProduct.quantity - 1 : undefined, }; } return p; }); } submit() { const val: Order = { ...this.orderForm?.value, orderProducts: this.orderProducts .filter((p) => !!p.quantity) .map((p) => ({ productId: p.product.productId, price: p.product.price, quantity: p.quantity, })), }; this.orderSvc.create(val).subscribe(() => this.router.navigate(['orders'])); } } ================================================ FILE: Lab4/client/Application/src/app/views/orders/detail/detail.component.html ================================================

Order Details mostly made up

NO: {{ orderId$ | async }} | Date: {{ today() | date }}
  • {{ tenantName() }}

Services / Products

Item / Details Unit Cost Sum Cost Discount Tax Total
{{ op.productId }}
{{ op.price | currency }}
Before Tax
{{ sum(op) | currency }}
{{ op.quantity }} Units
$0.00
None
{{ tax(op) | currency }}
Sales Tax 8.9%
{{ total(op) | currency }}
Sub Total Discount Total Tax Final
{{ subTotal(order) | currency }} -$0.00 {{ subTotal(order) | currency }} {{ calcTax(order) | currency }} {{ final(order) | currency }}
Comments / Notes
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Odit repudiandae numquam sit facere blanditiis, quasi distinctio ipsam? Libero odit ex expedita, facere sunt, possimus consectetur dolore, nobis iure amet vero.
================================================ FILE: Lab4/client/Application/src/app/views/orders/detail/detail.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .no-bottom-margin { margin-bottom: 0; } .nowrap { white-space: nowrap; } .invoice { font-family: Arial, Helvetica, sans-serif; width: 970px !important; margin: 50px auto; .invoice-header { padding: 25px 25px 15px; h1 { margin: 0; } .media { .media-body { font-size: 0.9em; margin: 0; } } } .invoice-body { border-radius: 10px; padding: 25px; background: #fff; } .invoice-footer { padding: 15px; font-size: 0.9em; text-align: center; color: #999; } } .logo { max-height: 70px; border-radius: 10px; } .dl-horizontal { margin: 0; dt { float: left; width: 80px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } dd { margin-left: 90px; } } .rowamount { padding-top: 15px !important; } .rowtotal { font-size: 1.3em; } .colfix { width: 12%; } .mono { font-family: monospace; } ================================================ FILE: Lab4/client/Application/src/app/views/orders/detail/detail.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Order } from '../models/order.interface'; import { OrderProduct } from '../models/orderproduct.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-detail', templateUrl: './detail.component.html', styleUrls: ['./detail.component.scss'], }) export class DetailComponent implements OnInit { orderId$: Observable | undefined; order$: Observable | undefined; orderProducts$: Observable | undefined; taxRate = 0.0899; constructor(private route: ActivatedRoute, private orderSvc: OrdersService) {} ngOnInit(): void { this.orderId$ = this.route.params.pipe(map((o) => o['orderId'])); this.order$ = this.orderId$.pipe(switchMap((o) => this.orderSvc.get(o))); this.orderProducts$ = this.order$.pipe(map((o) => o.orderProducts)); } today() { return new Date(); } tenantName() { return ''; } sum(op: OrderProduct) { return op.price * op.quantity; } tax(op: OrderProduct) { return this.sum(op) * this.taxRate; } total(op: OrderProduct) { return this.sum(op) + this.tax(op); } subTotal(order: Order) { return order.orderProducts .map((op) => op.price * op.quantity) .reduce((acc, curr) => acc + curr); } calcTax(order: Order) { return this.subTotal(order) * this.taxRate; } final(order: Order) { return this.subTotal(order) + this.calcTax(order); } } ================================================ FILE: Lab4/client/Application/src/app/views/orders/list/list.component.html ================================================

Order List

Name {{ element.orderName }} Line Items {{ element.orderProducts?.length }} Total {{ sum(element) | currency }}
================================================ FILE: Lab4/client/Application/src/app/views/orders/list/list.component.scss ================================================ .order-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Lab4/client/Application/src/app/views/orders/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { displayedColumns: string[] = ['name', 'lineItems', 'total']; orderData: Order[] = []; isLoading: boolean = true; constructor(private orderSvc: OrdersService, private router: Router) {} ngOnInit(): void { this.orderSvc.fetch().subscribe((data) => { this.isLoading = false; this.orderData = data; }); } sum(order: Order): number { return order.orderProducts .map((p) => p.price * p.quantity) .reduce((acc, curr) => acc + curr); } } ================================================ FILE: Lab4/client/Application/src/app/views/orders/models/order.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { OrderProduct } from './orderproduct.interface'; export interface Order { key: string; shardId: string; orderId: string; orderName: string; orderProducts: OrderProduct[]; } ================================================ FILE: Lab4/client/Application/src/app/views/orders/models/orderproduct.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface OrderProduct { productId: string; price: number; quantity: number; } ================================================ FILE: Lab4/client/Application/src/app/views/orders/orders-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', data: { title: 'All Orders', }, component: ListComponent, }, { path: 'create', data: { title: 'Create Order', }, component: CreateComponent, }, { path: 'detail/:orderId', data: { title: 'View Order Detail', }, component: DetailComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class OrdersRoutingModule {} ================================================ FILE: Lab4/client/Application/src/app/views/orders/orders.module.ts ================================================ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; import { OrdersRoutingModule } from './orders-routing.module'; @NgModule({ declarations: [CreateComponent, ListComponent, DetailComponent], imports: [ CommonModule, OrdersRoutingModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class OrdersModule {} ================================================ FILE: Lab4/client/Application/src/app/views/orders/orders.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Order } from './models/order.interface'; @Injectable({ providedIn: 'root', }) export class OrdersService { orders: Order[] = []; baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; constructor(private http: HttpClient) {} fetch(): Observable { return this.http.get(`${this.baseUrl}/orders`); } get(orderId: string): Observable { const url = `${this.baseUrl}/order/${orderId}`; return this.http.get(url); } create(order: Order): Observable { return this.http.post(`${this.baseUrl}/order`, order); } } ================================================ FILE: Lab4/client/Application/src/app/views/products/create/create.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required SKU Category {{category}}
================================================ FILE: Lab4/client/Application/src/app/views/products/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab4/client/Application/src/app/views/products/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ProductService } from '../product.service'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); } get name() { return this.productForm.get('name'); } get price() { return this.productForm.get('price'); } submit() { this.productSvc.post(this.productForm.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err.message); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Lab4/client/Application/src/app/views/products/edit/edit.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required Description
================================================ FILE: Lab4/client/Application/src/app/views/products/edit/edit.component.scss ================================================ ================================================ FILE: Lab4/client/Application/src/app/views/products/edit/edit.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.scss'], }) export class EditComponent implements OnInit { productForm: FormGroup | undefined; product$: Observable | undefined; productId$: Observable | undefined; productName$: Observable | undefined; constructor( private route: ActivatedRoute, private router: Router, private productSvc: ProductService, private fb: FormBuilder ) {} ngOnInit(): void { this.productId$ = this.route.params.pipe(map((p) => p['productId'])); this.product$ = this.productId$.pipe( switchMap((p) => this.productSvc.get(p)) ); this.productName$ = this.product$.pipe(map((p) => p?.name)); this.productForm = this.fb.group({ productId: [''], name: [''], price: [''], description: [''], }); this.product$.subscribe((val) => { this.productForm?.patchValue({ ...val, }); }); } get name() { return this.productForm?.get('name'); } get price() { return this.productForm?.get('price'); } submit() { this.productSvc.put(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => console.error(err), }); } delete() { this.productSvc.delete(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Lab4/client/Application/src/app/views/products/list/list.component.html ================================================

Product List

Name {{ element.name }} Price {{ element.price | currency }} SKU {{ element.sku }}
================================================ FILE: Lab4/client/Application/src/app/views/products/list/list.component.scss ================================================ .product-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Lab4/client/Application/src/app/views/products/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { productData: Product[] = []; isLoading: boolean = true; displayedColumns: string[] = ['name', 'price', 'sku']; constructor(private productSvc: ProductService, private router: Router) {} ngOnInit(): void { this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onEdit(product: Product) { this.router.navigate(['products', 'edit', product.productId]); return false; } onRemove(product: Product) { this.productSvc.delete(product); this.isLoading = true; this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onCreate() { this.router.navigate(['products', 'create']); } } ================================================ FILE: Lab4/client/Application/src/app/views/products/models/product.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface Product { key: string; shardId: string; productId: string; name: string; price: number; sku: string; category: string; pictureUrl?: string; } ================================================ FILE: Lab4/client/Application/src/app/views/products/product.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Product } from './models/product.interface'; @Injectable({ providedIn: 'root', }) export class ProductService { constructor(private http: HttpClient) {} baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; fetch(): Observable { return this.http.get(`${this.baseUrl}/products`); } get(productId: string): Observable { const url = `${this.baseUrl}/product/${productId}`; return this.http.get(url); } delete(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.delete(url); } put(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.put(url, product); } post(product: Product) { return this.http.post(`${this.baseUrl}/product`, product); } } ================================================ FILE: Lab4/client/Application/src/app/views/products/products-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'Product List', }, component: ListComponent, }, { path: 'create', data: { title: 'Create new Product', }, component: CreateComponent, }, { path: 'edit/:productId', data: { title: 'Edit Product', }, component: EditComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class ProductsRoutingModule {} ================================================ FILE: Lab4/client/Application/src/app/views/products/products.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ProductsRoutingModule } from './products-routing.module'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [CreateComponent, EditComponent, ListComponent], imports: [ CommonModule, ReactiveFormsModule, ProductsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class ProductsModule {} ================================================ FILE: Lab4/client/Application/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
================================================ FILE: Lab4/client/Application/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab4/client/Application/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Lab4/client/Application/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Lab4/client/Application/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab4/client/Application/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Lab4/client/Application/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Lab4/client/Application/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Lab4/client/Application/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Lab4/client/Application/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { find, mergeMap, defaultIfEmpty } from 'rxjs/operators'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.regApiGatewayUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } update(email: string, user: User) {} } ================================================ FILE: Lab4/client/Application/src/assets/.gitkeep ================================================ ================================================ FILE: Lab4/client/Application/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab4/client/Application/src/environments/environment.prod.ts ================================================ export const environment = { production: true, regApiGatewayUrl: 'https://3vby5hwma9.execute-api.us-west-2.amazonaws.com/prod/', apiGatewayUrl: 'https://hj4p6t6ob5.execute-api.us-west-2.amazonaws.com/prod/', userPoolId: 'us-west-2_HAYgKc4Ws', appClientId: '7e7hpl565vdrvkf4ief77bgnm', }; ================================================ FILE: Lab4/client/Application/src/environments/environment.ts ================================================ export const environment = { production: false, regApiGatewayUrl: 'https://k1lecgl9ye.execute-api.us-west-2.amazonaws.com/prod/', apiGatewayUrl: 'https://885cs6u6m8.execute-api.us-west-2.amazonaws.com/prod/', userPoolId: 'us-west-2_bCwYPJqrb', appClientId: '6lv6qhvmh6ivgd94qsftruc994', }; ================================================ FILE: Lab4/client/Application/src/index.html ================================================ Application ================================================ FILE: Lab4/client/Application/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab4/client/Application/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab4/client/Application/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab4/client/Application/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab4/client/Application/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "~bootstrap/scss/functions"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/maps"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; .chart-container canvas { max-height: 250px; width: auto; } .chart-container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; } ================================================ FILE: Lab4/client/Application/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab4/client/Application/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab4/client/Application/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "strictPropertyInitialization": false, "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab4/client/Application/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab4/client/Landing/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab4/client/Landing/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab4/client/Landing/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab4/client/Landing/README.md ================================================ # Landing This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab4/client/Landing/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "Landing": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/custom-theme.scss", "src/styles.scss", "node_modules/font-awesome/css/font-awesome.min.css" ] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "10kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "Landing:build:production" }, "development": { "browserTarget": "Landing:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "Landing:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab4/client/Landing/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab4/client/Landing/package.json ================================================ { "name": "landing", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "bootstrap": "~5.1.3", "font-awesome": "~4.7.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab4/client/Landing/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; export const routes: Routes = [ { path: '', redirectTo: 'landing', pathMatch: 'full', }, { path: 'landing', component: LandingComponent, }, { path: 'register', component: RegisterComponent, }, { path: '**', redirectTo: 'landing', pathMatch: 'full', }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab4/client/Landing/src/app/app.component.scss ================================================ ================================================ FILE: Lab4/client/Landing/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'landing'; } ================================================ FILE: Lab4/client/Landing/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; @NgModule({ declarations: [AppComponent, LandingComponent, RegisterComponent], imports: [ AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, MatButtonModule, MatCardModule, MatIconModule, MatProgressSpinnerModule, MatSidenavModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, FormsModule, HttpClientModule, ReactiveFormsModule, MatSnackBarModule, MatInputModule, MatFormFieldModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab4/client/Landing/src/app/views/landing/landing.component.html ================================================

Serverless SaaS Reference Architecture

It's so nice it blows your mind.

Sign up now!

Serverless SaaS Reference Architecture is so awesome.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere temporibus omnis illum, officia. Architecto voluptatibus commodi voluptatem perspiciatis eos possimus, eius at molestias quaerat magnam? Odio qui quos ipsam natus.

Serverless SaaS Reference Architecture so awesome. Makes you awesome - go sign up!

Serverless SaaS Reference Architecture so great. Makes you even greater - go sign up now. Super cheap deal!

Feel lonely? Go sign up and have a friend!

Take Serverless SaaS Reference Architecture with you everywhere you go.

Serverless SaaS Reference Architecture is all you need. Anywhere - ever. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita sapiente hic voluptatum quo sunt totam accusamus distinctio minus aliquid quis!

Love Serverless SaaS Reference Architecture. So nice! So good! Could not live without!

Satisfied Customer

Reasons to sign up this product:

  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!
  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!

Why you still reading?

Sign up now!
================================================ FILE: Lab4/client/Landing/src/app/views/landing/landing.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // $color-red: #cc615f; $color-grey: #4b4b4b; $color-grey--light: #d3d3d3; $btn-amzn: #ec7211; $color-primary: #232f3e; $bp-s: 65.75em; //700px; $bp-xs: 34.375em; //550px; @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,800,300); *{ box-sizing: border-box; } html{ width: 100%; height:100%; margin: 0; padding: 0; } body{ width: 100%; height:100%; font-family: 'Open Sans','Helvetica Neue',Helvetica, sans-serif; font-size: 100%; line-height: 1.45; color: #141414; } a{ text-decoration: none; &:hover{ text-decoration: none; } } img{ max-width: 100%; } .btn{ display: inline-block; margin: 1rem 0; color: white; font-weight: 500; font-size: 1.3rem; background: $btn-amzn; letter-spacing: .02em; border: none; border-radius: 5px; padding: .8rem 1rem .9rem; text-shadow: 0 1px rgba(black,.3); box-shadow: 0 0 2px rgba(black,.2); @media (max-width: $bp-s){ padding: .5rem .7rem .6rem; font-size: 1rem; } &:hover{ background: lighten($btn-amzn,5%); color: #fff; } &:focus, &:active, &:focus:active{ background: darken($btn-amzn,5%); border-color: darken($btn-amzn,5%); box-shadow: 0 2px 5px 0 rgba(black,.5) inset; } } .container{ margin: 0 auto; width: 90%; max-width: 900px; } header{ color: white; background: #232f3e; padding: 10rem 0; text-align: center; position: relative; z-index: 1; overflow: hidden; @media (max-width: $bp-s){ padding: 2rem 0; } h1{ font-size: 3rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 2rem; } } h2{ font-weight: 300; font-size: 1.5rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } } section{ background: #fff; color: $color-grey; padding: 3.5rem 0; @media (max-width: $bp-s){ padding: 2rem 0; } &.section--primary{ background: $color-primary; color: #fff; } &.section--primary--alt{ background: desaturate(lighten($color-primary,15%),10%); color: #fff; } &.section--primary--light{ background: rgba($color-primary,.1); } &.section--grey{ background: $color-grey; color: #fff; } &.section--grey--light{ background: $color-grey--light; color: #fff; } h3{ text-align: center; font-size: 2rem; font-weight: 300; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } li{ font-size: 1.2rem; font-weight: 300; } p{ font-size: 1.2rem; font-weight: 300; } } .col{ margin: 0 1.5%; display: inline-block; vertical-align: top; } .col-7{ @extend .col; width: 64%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-3{ @extend .col; width: 29%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-5{ @extend .col; width: 30%; @media (max-width: $bp-xs){ width: 60%; margin: 0; } } .details{ text-align: left; h3{ font-size: 2rem; text-align: left; } } .features{ text-align: center; padding: 1rem; &:hover{ background: rgba(white,.1); } @media (max-width: $bp-s){ width: 100%; margin: 0; text-align: left; border-bottom: 1px solid rgba(white,.2); &:last-child{ border: none; } } i{ font-size: 4rem; margin: 0 0 2rem 0; @media (max-width: $bp-s){ font-size: 1.5rem; width: 2rem; text-align: center; margin: 0 0 1rem 0; float: left; } } p{ margin: 0 0 1rem 0; font-size: 1rem; @media (max-width: $bp-s){ margin-left: 3rem; } } } blockquote{ position: relative; margin: 0; padding: 0; text-align: center; &:before{ display: inline-block; color: $color-primary; font-size: 2rem; content: '\201C'; } p{ margin: 0; display: inline; font-size: 1.5rem; @media (max-width: $bp-s){ font-size: 1.2rem; } } cite{ font-size: 1rem; display: block; margin: .5rem 0 0 1.2rem; @media (max-width: $bp-s){ font-size: .8rem; } &:before{ content: '–'; } } } footer{ background: $color-primary; color: #fff; padding: 2rem 0; text-align: center; font-size: .8rem; color: rgba(white,.4); ul{ margin: 0; padding: 0; list-style: none; li{ display: inline-block; a{ display: block; padding: .4rem .7rem; font-size: .9rem; text-decoration: none; color: rgba(white,.7); &:hover{ color: white; } } } } } .text--center{ text-align: center; } .text--left{ text-align: left; } .bg-image{ background: $color-primary; text-align: center; position: relative; z-index: 1; overflow: hidden; //text-shadow: 2px 0 5px black; &:before{ content: ''; display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; z-index: -1; background-size: 100%; background-attachment: fixed; filter: blur(3px); opacity: .8; transform: scale(1.1); } &.bg-image-2:before{ opacity: .6; background-position: center center; } } ================================================ FILE: Lab4/client/Landing/src/app/views/landing/landing.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-landing', templateUrl: './landing.component.html', styleUrls: ['./landing.component.scss'], }) export class LandingComponent implements OnInit { constructor(private router: Router) {} ngOnInit() {} register() { this.router.navigate(['register']); return false; } } ================================================ FILE: Lab4/client/Landing/src/app/views/register/register.component.html ================================================
Provision a new Tenant Login details will be sent to the email provided. Name Email Phone Address Plan
================================================ FILE: Lab4/client/Landing/src/app/views/register/register.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .tenant-form { display: flex; align-items: center; justify-content: center; } .wide-form { min-width: 150px; max-width: 500px; width: 100%; } .full-width { width: 100%; } .mat-form-field { width: 100%; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab4/client/Landing/src/app/views/register/register.component.ts ================================================ import { MatSnackBar } from '@angular/material/snack-bar'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.scss'], }) export class RegisterComponent implements OnInit { submitting: boolean = false; tenantForm: FormGroup = this.fb.group({ tenantName: ['', [Validators.required]], tenantEmail: ['', [Validators.email, Validators.required]], tenantTier: ['', [Validators.required]], tenantPhone: [''], tenantAddress: [''], }); constructor( private fb: FormBuilder, private _snackBar: MatSnackBar, private http: HttpClient ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; this.tenantForm.disable(); const tenant = { ...this.tenantForm.value, }; this.http .post(`${environment.apiGatewayUrl}/registration`, tenant) .subscribe({ next: () => { this.openErrorMessageSnackBar('Successfully created new tenant!'); this.tenantForm.reset(); this.tenantForm.enable(); this.submitting = false; }, error: (err) => { this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); this.tenantForm.enable(); this.submitting = false; }, }); } } ================================================ FILE: Lab4/client/Landing/src/assets/.gitkeep ================================================ ================================================ FILE: Lab4/client/Landing/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab4/client/Landing/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab4/client/Landing/src/environments/environment.ts ================================================ export const environment = { production: false, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab4/client/Landing/src/index.html ================================================ Landing ================================================ FILE: Lab4/client/Landing/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab4/client/Landing/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab4/client/Landing/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab4/client/Landing/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab4/client/Landing/src/styles.scss ================================================ ================================================ FILE: Lab4/client/Landing/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab4/client/Landing/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab4/client/Landing/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab4/client/Landing/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab4/client/dummy.txt ================================================ ================================================ FILE: Lab4/scripts/deployment.sh ================================================ #!/bin/bash if [[ "$#" -eq 0 ]]; then echo "Invalid parameters" echo "Command to deploy client code: deployment.sh -c" echo "Command to deploy bootstrap server code: deployment.sh -b" echo "Command to deploy tenant server code: deployment.sh -t" echo "Command to deploy bootstrap & tenant server code: deployment.sh -s" echo "Command to deploy server & client code: deployment.sh -s -c" exit 1 fi while [[ "$#" -gt 0 ]]; do case $1 in -s) server=1 ;; -b) bootstrap=1 ;; -t) tenant=1 ;; -c) client=1 ;; *) echo "Unknown parameter passed: $1"; exit 1 ;; esac shift done # During AWS hosted events using event engine tool # we pre-provision cloudfront and s3 buckets which hosts UI code. # So that it improves this labs total execution time. # Below code checks if cloudfront and s3 buckets are # pre-provisioned or not and then concludes if the workshop # is running in AWS hosted event through event engine tool or not. IS_RUNNING_IN_EVENT_ENGINE=false PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" IS_RUNNING_IN_EVENT_ENGINE=true ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) fi if [[ $server -eq 1 ]] || [[ $bootstrap -eq 1 ]] || [[ $tenant -eq 1 ]]; then echo "Validating server code using pylint" cd ../server python3 -m pylint -E -d E0401,E1111 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi cd ../scripts fi if [[ $server -eq 1 ]] || [[ $bootstrap -eq 1 ]]; then echo "Bootstrap server code is getting deployed" cd ../server REGION=$(aws configure get region) sam build -t shared-template.yaml --use-container if [ "$IS_RUNNING_IN_EVENT_ENGINE" = true ]; then sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE AdminUserPoolCallbackURLParameter=$ADMIN_SITE_URL TenantUserPoolCallbackURLParameter=$APP_SITE_URL else sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE fi cd ../scripts fi if [[ $server -eq 1 ]] || [[ $tenant -eq 1 ]]; then echo "Tenant server code is getting deployed" cd ../server REGION=$(aws configure get region) sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml --region=$REGION cd ../scripts fi if [[ $client -eq 1 ]]; then # Admin UI and Landing UI are configured in Lab2 # App UI is configured in Lab3 echo "Admin UI and Landing UI are configured in Lab2. App UI is configured in Lab3. So, no UI code is built in this Lab4" if [ "$IS_RUNNING_IN_EVENT_ENGINE" = false ]; then ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" fi ================================================ FILE: Lab4/scripts/geturl.sh ================================================ PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) else ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Lab4/server/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Lab4/server/OrderService/order_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Order: key='' def __init__(self, shardId, orderId, orderName, orderProducts): self.shardId = shardId self.orderId = orderId self.key = shardId + ':' + orderId self.orderName = orderName self.orderProducts = orderProducts class OrderProduct: def __init__(self, productId, price, quantity): self.productId = productId self.price = price self.quantity = quantity ================================================ FILE: Lab4/server/OrderService/order_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import order_service_dal from decimal import Decimal from types import SimpleNamespace from aws_lambda_powertools import Tracer tracer = Tracer() @tracer.capture_lambda_handler def get_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a order") params = event['pathParameters'] key = params['id'] logger.log_with_tenant_context(event, params) order = order_service_dal.get_order(event, key) logger.log_with_tenant_context(event, "Request completed to get a order") metrics_manager.record_metric(event, "SingleOrderRequested", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def create_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) order = order_service_dal.create_order(event, payload) logger.log_with_tenant_context(event, "Request completed to create a order") metrics_manager.record_metric(event, "OrderCreated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def update_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] order = order_service_dal.update_order(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a order") metrics_manager.record_metric(event, "OrderUpdated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def delete_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a order") params = event['pathParameters'] key = params['id'] response = order_service_dal.delete_order(event, key) logger.log_with_tenant_context(event, "Request completed to delete a order") metrics_manager.record_metric(event, "OrderDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the order") @tracer.capture_lambda_handler def get_orders(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all orders") response = order_service_dal.get_orders(event, tenantId) metrics_manager.record_metric(event, "OrdersRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all orders") return utils.generate_response(response) ================================================ FILE: Lab4/server/OrderService/order_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid from order_models import Order import json import utils import metrics_manager from types import SimpleNamespace import logger import random import threading from boto3.dynamodb.conditions import Key table_name = os.environ['ORDER_TABLE_NAME'] dynamodb = None suffix_start = 1 suffix_end = 10 def get_order(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) response = table.get_item(Key={'shardId': shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a order', e) else: return order def delete_order(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a order', e) else: logger.info("DeleteItem succeeded:") return response def create_order(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] table = __get_dynamodb_table(event, dynamodb) suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) order = Order(shardId, str(uuid.uuid4()), payload.orderName, payload.orderProducts) try: response = table.put_item(Item={ 'shardId':shardId, 'orderId': order.orderId, 'orderName': order.orderName, 'orderProducts': get_order_products_dict(order.orderProducts) }, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a order', e) else: logger.info("PutItem succeeded:") return order def update_order(event, payload, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) order = Order(shardId, orderId,payload.orderName, payload.orderProducts) response = table.update_item(Key={'shardId':order.shardId, 'orderId': order.orderId}, UpdateExpression="set orderName=:orderName, " +"orderProducts=:orderProducts", ExpressionAttributeValues={ ':orderName': order.orderName, ':orderProducts': get_order_products_dict(order.orderProducts) }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a order', e) else: logger.info("UpdateItem succeeded:") return order def get_orders(event, tenantId): table = __get_dynamodb_table(event, dynamodb) get_all_products_response = [] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error("Error getting all orders") raise Exception('Error getting all orders', e) else: logger.info("Get orders succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) get_all_products_response.append(order) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def __get_dynamodb_table(event, dynamodb): """ Args: event ([type]): [description] Returns: [type]: [description] """ #TODO: Implement this method def get_order_products_dict(orderProducts): orderProductList = [] for i in range(len(orderProducts)): product = orderProducts[i] orderProductList.append(vars(product)) return orderProductList ================================================ FILE: Lab4/server/OrderService/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Lab4/server/ProductService/product_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Product: key ='' def __init__(self, shardId, productId, sku, name, price, category): self.shardId = shardId self.productId = productId self.key = shardId + ':' + productId self.sku = sku self.name = name self.price = price self.category = category class Category: def __init__(self, id, name): self.id = id self.name = name ================================================ FILE: Lab4/server/ProductService/product_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import product_service_dal from aws_lambda_powertools import Tracer from decimal import Decimal from types import SimpleNamespace tracer = Tracer() @tracer.capture_lambda_handler def get_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a product") params = event['pathParameters'] logger.log_with_tenant_context(event, params) key = params['id'] logger.log_with_tenant_context(event, key) product = product_service_dal.get_product(event, key) logger.log_with_tenant_context(event, "Request completed to get a product") metrics_manager.record_metric(event, "SingleProductRequested", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def create_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) product = product_service_dal.create_product(event, payload) logger.log_with_tenant_context(event, "Request completed to create a product") metrics_manager.record_metric(event, "ProductCreated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def update_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] product = product_service_dal.update_product(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a product") metrics_manager.record_metric(event, "ProductUpdated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def delete_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a product") params = event['pathParameters'] key = params['id'] response = product_service_dal.delete_product(event, key) logger.log_with_tenant_context(event, "Request completed to delete a product") metrics_manager.record_metric(event, "ProductDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the product") @tracer.capture_lambda_handler def get_products(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all products") response = product_service_dal.get_products(event, tenantId) metrics_manager.record_metric(event, "ProductsRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all products") return utils.generate_response(response) ================================================ FILE: Lab4/server/ProductService/product_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid import json import logger import metrics_manager import random import threading from product_models import Product from types import SimpleNamespace from boto3.dynamodb.conditions import Key table_name = os.environ['PRODUCT_TABLE_NAME'] dynamodb = None suffix_start = 1 suffix_end = 10 def get_product(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) response = table.get_item(Key={'shardId': shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a product', e) else: logger.info("GetItem succeeded:"+ str(product)) return product def delete_product(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a product', e) else: logger.info("DeleteItem succeeded:") return response def create_product(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] table = __get_dynamodb_table(event, dynamodb) suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) product = Product(shardId, str(uuid.uuid4()), payload.sku,payload.name, payload.price, payload.category) try: response = table.put_item( Item= { 'shardId': shardId, 'productId': product.productId, 'sku': product.sku, 'name': product.name, 'price': product.price, 'category': product.category }, ReturnConsumedCapacity='TOTAL' ) metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: logger.info("PutItem succeeded:") return product def update_product(event, payload, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) product = Product(shardId,productId,payload.sku, payload.name, payload.price, payload.category) response = table.update_item(Key={'shardId':product.shardId, 'productId': product.productId}, UpdateExpression="set sku=:sku, #n=:productName, price=:price, category=:category", ExpressionAttributeNames= {'#n':'name'}, ExpressionAttributeValues={ ':sku': product.sku, ':productName': product.name, ':price': product.price, ':category': product.category }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a product', e) else: logger.info("UpdateItem succeeded:") return product def get_products(event, tenantId): table = __get_dynamodb_table(event, dynamodb) get_all_products_response =[] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting all products', e) else: logger.info("Get products succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) get_all_products_response.append(product) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def __get_dynamodb_table(event, dynamodb): """ Args: event ([type]): [description] Returns: [type]: [description] """ #TODO: Implement this method ================================================ FILE: Lab4/server/ProductService/requirements.txt ================================================ requests pytest-mock aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Lab4/server/README.md ================================================ sam build -t shared-template.yaml --use-container sam deploy --config-file shared-samconfig.toml sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml ================================================ FILE: Lab4/server/Resources/requirements.txt ================================================ python-jose[cryptography] ================================================ FILE: Lab4/server/Resources/shared_service_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import utils import auth_manager region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] tenant_userpool_id = os.environ['TENANT_USER_POOL'] tenant_appclient_id = os.environ['TENANT_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) if(auth_manager.isSaaSProvider(unauthorized_claims['custom:userRole'])): userpool_id = user_pool_operation_user appclient_id = app_client_operation_user else: #get tenant user pool and app client to validate jwt token against userpool_id = tenant_userpool_id appclient_id = tenant_appclient_id #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] #only tenant admin and system admin can do certain actions like create and disable users if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): policy.allowAllMethods() if (auth_manager.isTenantAdmin(user_role)): policy.denyMethod(HttpVerb.POST, "tenant-activation") policy.denyMethod(HttpVerb.GET, "tenants") else: #if not tenant admin or system admin then only allow to get info and update info policy.allowMethod(HttpVerb.GET, "user/*") policy.allowMethod(HttpVerb.PUT, "user/*") authResponse = policy.build() # Generate STS credentials to be used for FGAC # Important Note: # We are generating STS token inside Authorizer to take advantage of the caching behavior of authorizer # Another option is to generate the STS token inside the lambda function itself, as mentioned in this blog post: https://aws.amazon.com/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/ # Finally, you can also consider creating one Authorizer per microservice in cases where you want the IAM policy specific to that service iam_policy = auth_manager.getPolicyForUser(user_role, utils.Service_Identifier.SHARED_SERVICES.value, tenant_id, region, aws_account_id) logger.info(iam_policy) role_arn = "arn:aws:iam::{}:role/authorizer-access-role".format(aws_account_id) assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName="tenant-aware-session", Policy=iam_policy, ) credentials = assumed_role["Credentials"] #pass sts credentials to lambda context = { 'accesskey': credentials['AccessKeyId'], 'secretkey' : credentials['SecretAccessKey'], 'sessiontoken' : credentials["SessionToken"], 'userName': user_name, 'userPoolId': userpool_id, 'tenantId': tenant_id, 'userRole': user_role } authResponse['context'] = context return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Lab4/server/Resources/tenant_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import auth_manager import utils region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) userpool_id = os.environ['TENANT_USER_POOL'] appclient_id = os.environ['TENANT_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] policy.allowAllMethods() authResponse = policy.build() #TODO : Add code for Fine-Grained-Access-Control return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Lab4/server/TenantManagementService/requirements.txt ================================================ requests aws_requests_auth ================================================ FILE: Lab4/server/TenantManagementService/tenant-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import json import boto3 from boto3.dynamodb.conditions import Key import urllib.parse import utils from botocore.exceptions import ClientError import logger import requests import metrics_manager import auth_manager from aws_lambda_powertools import Tracer tracer = Tracer() region = os.environ['AWS_REGION'] dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') #This method has been locked down to be only called from tenant registration service def create_tenant(event, context): tenant_details = json.loads(event['body']) try: response = table_tenant_details.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'tenantName' : tenant_details['tenantName'], 'tenantAddress': tenant_details['tenantAddress'], 'tenantEmail': tenant_details['tenantEmail'], 'tenantPhone': tenant_details['tenantPhone'], 'tenantTier': tenant_details['tenantTier'], 'isActive': True } ) except Exception as e: raise Exception('Error creating a new tenant', e) else: return utils.create_success_response("Tenant Created") def get_tenants(event, context): try: response = table_tenant_details.scan() except Exception as e: raise Exception('Error getting all tenants', e) else: return utils.generate_response(response['Items']) @tracer.capture_lambda_handler def update_tenant(event, context): requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_details = json.loads(event['body']) tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response_update = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set tenantName = :tenantName, tenantAddress = :tenantAddress, tenantEmail = :tenantEmail, tenantPhone = :tenantPhone, tenantTier=:tenantTier", ExpressionAttributeValues={ ':tenantName' : tenant_details['tenantName'], ':tenantAddress': tenant_details['tenantAddress'], ':tenantEmail': tenant_details['tenantEmail'], ':tenantPhone': tenant_details['tenantPhone'], ':tenantTier': tenant_details['tenantTier'] }, ReturnValues="UPDATED_NEW" ) logger.log_with_tenant_context(event, response_update) logger.log_with_tenant_context(event, "Request completed to update tenant") return utils.create_success_response("Tenant Updated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_tenant(event, context): requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get tenant details") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): tenant_details = table_tenant_details.get_item( Key={ 'tenantId': tenant_id, }, AttributesToGet=[ 'tenantName', 'tenantAddress', 'tenantEmail', 'tenantPhone' ] ) item = tenant_details['Item'] tenant_info = TenantInfo(item['tenantName'], item['tenantAddress'],item['tenantEmail'], item['tenantPhone']) logger.log_with_tenant_context(event, tenant_info) logger.log_with_tenant_context(event, "Request completed to get tenant details") return utils.create_success_response(tenant_info.__dict__) else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def deactivate_tenant(event, context): url_disable_users = os.environ['DISABLE_USERS_BY_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to deactivate tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': False }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) update_details = {} update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_disable_users(update_details, headers, auth, host, stage_name, url_disable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to deactivate tenant") return utils.create_success_response("Tenant Deactivated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def activate_tenant(event, context): url_enable_users = os.environ['ENABLE_USERS_BY_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to activate tenant") if (auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': True }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) update_details = {} update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_enable_users(update_details, headers, auth, host, stage_name, url_enable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to activate tenant") return utils.create_success_response("Tenant Activated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only system admin can activate tenant!") return utils.create_unauthorized_response() def __invoke_disable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url, '/']) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while disabling users for the tenant') except Exception as e: logger.error('Error occured while disabling users for the tenant') raise Exception('Error occured while disabling users for the tenant', e) else: return "Success invoking disable users" def __invoke_enable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url, '/']) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while enabling users for the tenant') except Exception as e: logger.error('Error occured while enabling users for the tenant') raise Exception('Error occured while enabling users for the tenant', e) else: return "Success invoking enable users" class TenantInfo: def __init__(self, tenant_name, tenant_address, tenant_email, tenant_phone): self.tenant_name = tenant_name self.tenant_address = tenant_address self.tenant_email = tenant_email self.tenant_phone = tenant_phone ================================================ FILE: Lab4/server/TenantManagementService/tenant-registration.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import utils import uuid import logger import requests import re region = os.environ['AWS_REGION'] create_tenant_admin_user_resource_path = os.environ['CREATE_TENANT_ADMIN_USER_RESOURCE_PATH'] create_tenant_resource_path = os.environ['CREATE_TENANT_RESOURCE_PATH'] lambda_client = boto3.client('lambda') def register_tenant(event, context): try: tenant_id = uuid.uuid1().hex tenant_details = json.loads(event['body']) tenant_details['tenantId'] = tenant_id logger.info(tenant_details) stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) create_user_response = __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name) logger.info (create_user_response) tenant_details['tenantAdminUserName'] = create_user_response['message']['tenantAdminUserName'] create_tenant_response = __create_tenant(tenant_details, headers, auth, host, stage_name) logger.info (create_tenant_response) except Exception as e: logger.error('Error registering a new tenant') raise Exception('Error registering a new tenant', e) else: return utils.create_success_response("You have been registered in our system") def __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_admin_user_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while calling the create tenant admin user service') raise Exception('Error occured while calling the create tenant admin user service', e) else: return response_json def __create_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_resource_path]) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while creating the tenant record in table') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json ================================================ FILE: Lab4/server/TenantManagementService/user-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import sys import logger import metrics_manager import auth_manager import utils from boto3.dynamodb.conditions import Key from aws_lambda_powertools import Tracer tracer = Tracer() client = boto3.client('cognito-idp') dynamodb = boto3.resource('dynamodb') table_tenant_user_map = dynamodb.Table('ServerlessSaaS-TenantUserMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_id = os.environ['TENANT_USER_POOL_ID'] def create_tenant_admin_user(event, context): app_client_id = os.environ['TENANT_APP_CLIENT_ID'] tenant_details = json.loads(event['body']) tenant_id = tenant_details['tenantId'] logger.info(tenant_details) user_mgmt = UserManagement() #Add tenant admin now based upon user pool tenant_user_group_response = user_mgmt.create_user_group(user_pool_id,tenant_id,"User group for tenant {0}".format(tenant_id)) tenant_admin_user_name = 'tenant-admin-{0}'.format(tenant_details['tenantId']) create_tenant_admin_response = user_mgmt.create_tenant_admin(user_pool_id, tenant_admin_user_name, tenant_details) add_tenant_admin_to_group_response = user_mgmt.add_user_to_group(user_pool_id, tenant_admin_user_name, tenant_user_group_response['Group']['GroupName']) tenant_user_mapping_response = user_mgmt.create_user_tenant_mapping(tenant_admin_user_name,tenant_id) response = {"userPoolId": user_pool_id, "appClientId": app_client_id, "tenantAdminUserName": tenant_admin_user_name} return utils.create_success_response(response) @tracer.capture_lambda_handler #only tenant admin can create users def create_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to create new user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = user_details['tenantId'] else: user_tenant_id = tenant_id if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): metrics_manager.record_metric(event, "UserCreated", "Count", 1) response = client.admin_create_user( Username=user_details['userName'], UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] }, { 'Name': 'custom:tenantId', 'Value': user_tenant_id } ] ) logger.log_with_tenant_context(event, response) user_mgmt = UserManagement() user_mgmt.add_user_to_group(user_pool_id, user_details['userName'], user_tenant_id) response_mapping = user_mgmt.create_user_tenant_mapping(user_details['userName'], user_tenant_id) logger.log_with_tenant_context(event, "Request completed to create new user") return utils.create_success_response("New user created") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can create user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_users(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] users = [] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get users") if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): response = client.list_users( UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) num_of_users = len(response['Users']) metrics_manager.record_metric(event, "Number of users", "Count", num_of_users) if (num_of_users > 0): for user in response['Users']: is_same_tenant_user = False user_info = UserInfo() for attr in user["Attributes"]: if(attr["Name"] == "custom:tenantId" and attr["Value"] == tenant_id): is_same_tenant_user = True user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] if(is_same_tenant_user): user_info.enabled = user["Enabled"] user_info.created = user["UserCreateDate"] user_info.modified = user["UserLastModifiedDate"] user_info.status = user["UserStatus"] user_info.user_name = user["Username"] users.append(user_info) return utils.generate_response(users) else: logger.log_with_tenant_context(event, "Request completed as unauthorized.") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get user") if (auth_manager.isTenantUser(user_role) and user_name != requesting_user_name): logger.log_with_tenant_context(event, "Request completed as unauthorized. User can only get its information.") return utils.create_unauthorized_response() else: user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: logger.log_with_tenant_context(event, "Request completed to get new user") return utils.create_success_response(user_info.__dict__) @tracer.capture_lambda_handler def update_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update user") if (auth_manager.isTenantUser(user_role)): logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update user!") return utils.create_unauthorized_response() else: user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserUpdated", "Count", 1) response = client.admin_update_user_attributes( Username=user_name, UserPoolId=user_pool_id, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] } ] ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to update user") return utils.create_success_response("user updated") @tracer.capture_lambda_handler def disable_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to disable new user") if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserDisabled", "Count", 1) response = client.admin_disable_user( Username=user_name, UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to disable new user") return utils.create_success_response("User disabled") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can disable user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def disable_users_by_tenant(event, context): logger.info("Request received to disable users by tenant") tenantid_to_update = event['tenantId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if ((auth_manager.isTenantAdmin(user_role) and tenantid_to_update == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_disable_user( Username=user['userName'], UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to disable users") return utils.create_success_response("Users disabled") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def enable_users_by_tenant(event, context): logger.info("Request received to enable users by tenant") tenantid_to_update = event['tenantId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if (auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_enable_user( Username=user['userName'], UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to enable users") return utils.create_success_response("Users enables") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() def get_user_info(event, user_pool_id, user_name): metrics_manager.record_metric(event, "UserInfoRequested", "Count", 1) response = client.admin_get_user( UserPoolId=user_pool_id, Username=user_name ) logger.log_with_tenant_context(event, response) user_info = UserInfo() user_info.user_name = response["Username"] for attr in response["UserAttributes"]: if(attr["Name"] == "custom:tenantId"): user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] logger.log_with_tenant_context(event, user_info) return user_info class UserManagement: def create_user_group(self, user_pool_id, group_name, group_description): response = client.create_group( GroupName=group_name, UserPoolId=user_pool_id, Description= group_description, Precedence=0 ) return response def create_tenant_admin(self, user_pool_id, tenant_admin_user_name, user_details): response = client.admin_create_user( Username=tenant_admin_user_name, UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['tenantEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': 'TenantAdmin' }, { 'Name': 'custom:tenantId', 'Value': user_details['tenantId'] } ] ) return response def add_user_to_group(self, user_pool_id, user_name, group_name): response = client.admin_add_user_to_group( UserPoolId=user_pool_id, Username=user_name, GroupName=group_name ) return response def create_user_tenant_mapping(self, user_name, tenant_id): response = table_tenant_user_map.put_item( Item={ 'tenantId': tenant_id, 'userName': user_name } ) return response class UserInfo: def __init__(self, user_name=None, tenant_id=None, user_role=None, email=None, status=None, enabled=None, created=None, modified=None): self.user_name = user_name self.tenant_id = tenant_id self.user_role = user_role self.email = email self.status = status self.enabled = enabled self.created = created self.modified = modified ================================================ FILE: Lab4/server/layers/auth_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils # These are the roles being supported in this reference architecture class UserRoles: SYSTEM_ADMIN = "SystemAdmin" CUSTOMER_SUPPORT = "CustomerSupport" TENANT_ADMIN = "TenantAdmin" TENANT_USER = "TenantUser" def isTenantAdmin(user_role): if (user_role == UserRoles.TENANT_ADMIN): return True else: return False def isSystemAdmin(user_role): if (user_role == UserRoles.SYSTEM_ADMIN): return True else: return False def isSaaSProvider(user_role): if (user_role == UserRoles.SYSTEM_ADMIN or user_role == UserRoles.CUSTOMER_SUPPORT): return True else: return False def isTenantUser(user_role): if (user_role == UserRoles.TENANT_USER): return True else: return False def getPolicyForUser(user_role, service_identifier, tenant_id, region, aws_account_id): """ This method is being used by Authorizer to get appropriate policy by user role Args: user_role (string): UserRoles enum tenant_id (string): region (string): aws_account_id (string): Returns: string: policy that tenant needs to assume """ iam_policy = "" if (isSystemAdmin(user_role)): iam_policy = __getPolicyForSystemAdmin(region, aws_account_id) elif (isTenantAdmin(user_role)): iam_policy = __getPolicyForTenantAdmin(tenant_id, service_identifier, region, aws_account_id) elif (isTenantUser(user_role)): iam_policy = __getPolicyForTenantUser(tenant_id, region, aws_account_id) return iam_policy def __getPolicyForSystemAdmin(region, aws_account_id): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/*".format(region, aws_account_id), ] } ] } return json.dumps(policy) def __getPolicyForTenantAdmin(tenant_id, sevice_identifier, region, aws_account_id): if (sevice_identifier == utils.Service_Identifier.SHARED_SERVICES.value): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantUserMapping".format(region, aws_account_id), "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantDetails".format(region, aws_account_id) ], "Condition": { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [ "{0}".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantStackMapping".format(region, aws_account_id), "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-Settings".format(region, aws_account_id) ] } ] } else: policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Product-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Order-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } } ] } return json.dumps(policy) def __getPolicyForTenantUser(tenant_id, region, aws_account_id): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Product-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Order-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } } ] } return json.dumps(policy) ================================================ FILE: Lab4/server/layers/logger.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from aws_lambda_powertools import Logger logger = Logger() """Log info messages """ def info(log_message): logger.info (log_message) """Log error messages """ def error(log_message): logger.error (log_message) def log_with_tenant_context(event, log_message): logger.structure_logs(append=True, tenant_id= event['requestContext']['authorizer']['tenantId']) logger.info (log_message) ================================================ FILE: Lab4/server/layers/metrics_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json from aws_lambda_powertools import Metrics metrics = Metrics() def record_metric(event, metric_name, metric_unit, metric_value): """ Record the metric in Cloudwatch using EMF format Args: event ([type]): [description] metric_name ([type]): [description] metric_unit ([type]): [description] metric_value ([type]): [description] """ metrics.add_dimension(name="tenant_id", value=event['requestContext']['authorizer']['tenantId']) metrics.add_metric(name=metric_name, unit=metric_unit, value=metric_value) metrics_object = metrics.serialize_metric_set() metrics.clear_metrics() print(json.dumps(metrics_object)) ================================================ FILE: Lab4/server/layers/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] simplejson jsonpickle aws_requests_auth ================================================ FILE: Lab4/server/layers/utils.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import jsonpickle import simplejson import boto3 from aws_requests_auth.aws_auth import AWSRequestsAuth from enum import Enum class StatusCodes(Enum): SUCCESS = 200 UN_AUTHORIZED = 401 NOT_FOUND = 404 class Service_Identifier(Enum): SHARED_SERVICES = "SharedServices" BUSINESS_SERVICES = "BusinessServices" def create_success_response(message): return { "statusCode": StatusCodes.SUCCESS.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def create_unauthorized_response(): return { "statusCode": StatusCodes.UN_AUTHORIZED.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": "User not authorized to perform this action" }), } def generate_response(inputObject): return { "statusCode": 200, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": encode_to_json_object(inputObject), } def encode_to_json_object(inputObject): jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True) jsonpickle.set_preferred_backend('simplejson') return jsonpickle.encode(inputObject, unpicklable=False, use_decimal=True) def get_auth(host, region): session = boto3.Session() credentials = session.get_credentials() auth = AWSRequestsAuth(aws_access_key=credentials.access_key, aws_secret_access_key=credentials.secret_key, aws_token=credentials.token, aws_host=host, aws_region=region, aws_service='execute-api') return auth def get_headers(event): return event['headers'] ================================================ FILE: Lab4/server/nested_templates/apigateway.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway, apis, api keys and usage plan as part of bootstrap Parameters: StageName: Type: String RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String Resources: ApiGatewayCloudWatchLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub apigateway-cloudwatch-publish-role-${AWS::Region} Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole ApiGatewayAttachCloudwatchLogArn: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchLogRole.Arn AdminApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/api-gateway/access-logs-serverless-saas-admin-api RetentionInDays: 30 AdminApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: "/*" HttpMethod: "*" Auth: ResourcePolicy: CustomStatements: - Effect: Allow Principal: "*" Action: "execute-api:Invoke" Resource: ["execute-api:/*/*/*"] - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/tenant" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/user/tenant-admin" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref RegisterTenantLambdaExecutionRoleArn - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/disable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/enable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn AccessLogSetting: DestinationArn: !GetAtt AdminApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ["", ["serverless-saas-admin-api-", !Ref "AWS::Region"]] basePath: !Join ["", ["/", !Ref StageName]] schemes: - https paths: /registration: post: summary: Register a new tenant description: Register a new tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref RegisterTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant/activation/{tenantid}: put: security: - api_key: [] - Authorizer: [] summary: Activate an existing tenant description: Activate an existing tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ActivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenants: get: summary: Returns all tenants description: Returns all tenants produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantsFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant: post: summary: Creates a tenant description: Creates a tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/{tenantid}: get: summary: Returns a tenant description: Return a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Disables a tenant description: Disables a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeactivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy put: summary: Updates a tenant description: Updates a tenant produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user/{username}: get: summary: Returns a user description: Return a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUserFunctionArn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Diables a user description: Disable a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUserFunctionArn - /invocations httpMethod: POST type: aws_proxy /user/tenant-admin: post: summary: Creates a tenant admin user description: Creates a tenant admin user produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantAdminUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user: post: summary: Create a user description: Create a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users: get: summary: Get all users by tenantId description: Get all users by tenantId produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUsersFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/disable: put: summary: disable users by tenant id description: disable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/enable: put: summary: enable users by tenant id description: enable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref EnableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: sigv4Reference: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "awsSigv4" Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref AuthorizerFunctionArn - /invocations authorizerResultTtlInSeconds: 60 type: "token" StageName: prod Outputs: AdminApiGatewayApi: Value: !Ref AdminApiGatewayApi ================================================ FILE: Lab4/server/nested_templates/apigateway_lambdapermissions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway and apis as part of bootstrap Parameters: RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String AdminApiGatewayApi: Type: String Resources: #provide api gateway permissions to call lambda functions RegisterTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref RegisterTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateTenantAdminUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantAdminUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] EnableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref EnableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUsersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUsersFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref AuthorizerFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*" ]] CreateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantsFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeactivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeactivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ActivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ActivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ================================================ FILE: Lab4/server/nested_templates/cognito.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup cognito as part of bootstrap Parameters: AdminEmailParameter: Type: String Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Description: "Enter the role name for system admin" AdminUserPoolCallbackURLParameter: Type: String Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Description: "Enter Tenant Management userpool call back url" Resources: CognitoUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: PooledTenant-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into tenant UI application at " - "https://" - !Ref TenantUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for tenant UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSClient GenerateSecret: false UserPoolId: !Ref CognitoUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [pooledtenant-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoUserPool CognitoOperationUsersUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: OperationUsers-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into admin UI application at " - "https://" - !Ref AdminUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for admin UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoOperationUsersUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSOperationUsersPoolClient GenerateSecret: false UserPoolId: !Ref CognitoOperationUsersUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoOperationUsersUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [operationsusers-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUserGroup: Type: AWS::Cognito::UserPoolGroup Properties: GroupName: SystemAdmins Description: Admin user group Precedence: 0 UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUser: Type: AWS::Cognito::UserPoolUser Properties: Username: admin DesiredDeliveryMediums: - EMAIL ForceAliasCreation: true UserAttributes: - Name: email Value: !Ref AdminEmailParameter - Name: custom:tenantId Value: system_admins - Name: custom:userRole Value: !Ref SystemAdminRoleNameParameter UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAddUserToGroup: Type: AWS::Cognito::UserPoolUserToGroupAttachment Properties: GroupName: !Ref CognitoAdminUserGroup Username: !Ref CognitoAdminUser UserPoolId: !Ref CognitoOperationUsersUserPool Outputs: CognitoUserPoolId: Value: !Ref CognitoUserPool CognitoUserPoolClientId: Value: !Ref CognitoUserPoolClient CognitoOperationUsersUserPoolId: Value: !Ref CognitoOperationUsersUserPool CognitoOperationUsersUserPoolClientId: Value: !Ref CognitoOperationUsersUserPoolClient CognitoOperationUsersUserPoolProviderURL: Value: !GetAtt CognitoOperationUsersUserPool.ProviderURL ================================================ FILE: Lab4/server/nested_templates/lambdafunctions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy lambda functions as part of bootstrap Parameters: CognitoOperationUsersUserPoolId: Type: String CognitoOperationUsersUserPoolClientId: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String TenantDetailsTableArn: Type: String TenantUserMappingTableArn: Type: String Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies Description: Utilities for project ContentUri: ../layers/ CompatibleRuntimes: - python3.9 LicenseInfo: "MIT" RetentionPolicy: Retain Metadata: BuildMethod: python3.9 AuthorizerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: authorizer-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: authorizer-execution-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:List* Resource: - !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn AuthorizerAccessRole: Type: AWS::IAM::Role DependsOn: AuthorizerExecutionRole Properties: RoleName: authorizer-access-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: - !GetAtt 'AuthorizerExecutionRole.Arn' Action: - sts:AssumeRole Policies: - PolicyName: authorizer-access-role-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:BatchGetItem - dynamodb:GetItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:UpdateItem - dynamodb:Query - dynamodb:Scan Resource: - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/* SharedServicesAuthorizerFunction: Type: AWS::Serverless::Function DependsOn: AuthorizerAccessRole Properties: CodeUri: ../Resources/ Handler: shared_service_authorizer.lambda_handler Runtime: python3.9 Role: !GetAtt AuthorizerExecutionRole.Arn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !Ref CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !Ref CognitoOperationUsersUserPoolClientId TENANT_USER_POOL: !Ref CognitoUserPoolId TENANT_APP_CLIENT: !Ref CognitoUserPoolClientId #Create user pool for the tenant TenantUserPoolLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-userpool-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-userpool-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn - Effect: Allow Action: - dynamodb:GetItem - dynamodb:Query Resource: - !Ref TenantUserMappingTableArn CreateUserLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub create-user-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-user-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:PutItem Resource: - !Ref TenantUserMappingTableArn - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn CreateTenantAdminUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_tenant_admin_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId TENANT_APP_CLIENT_ID: !Ref CognitoUserPoolClientId POWERTOOLS_SERVICE_NAME: "UserManagement.CreateTenantAdmin" #User management CreateUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.CreateUser" UpdateUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.update_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.UpdateUser" DisableUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUser" DisableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUsersByTenant" EnableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.enable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.EnableUsersByTenant" GetUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.GetUser" GetUsersFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_users Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.GetUsers" #Tenant Management TenantManagementLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-management-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-tenant-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:Scan - dynamodb:Query Resource: - !Ref TenantDetailsTableArn - !Join ["", [!Ref TenantDetailsTableArn, '/index/*']] CreateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.create_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.CreateTenant" ActivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.activate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.ActivateTenant" ENABLE_USERS_BY_TENANT: "/users/enable" PROVISION_TENANT: "/provisioning/" GetTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.GetTenant" DeactivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.deactivate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.DeactivateTenant" DEPROVISION_TENANT: "/provisioning/" DISABLE_USERS_BY_TENANT: "/users/disable" UpdateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.update_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.UpdateTenant" GetTenantsFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenants Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers #Tenant Registration RegisterTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-registration-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess RegisterTenantFunction: Type: AWS::Serverless::Function DependsOn: RegisterTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-registration.register_tenant Runtime: python3.9 Role: !GetAtt RegisterTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: # Need to find a better way than hard coding resource paths CREATE_TENANT_ADMIN_USER_RESOURCE_PATH: "/user/tenant-admin" CREATE_TENANT_RESOURCE_PATH: "/tenant" PROVISION_TENANT_RESOURCE_PATH: "/provisioning" POWERTOOLS_SERVICE_NAME: "TenantRegistration.RegisterTenant" Outputs: RegisterTenantLambdaExecutionRoleArn: Value: !GetAtt RegisterTenantLambdaExecutionRole.Arn TenantManagementLambdaExecutionRoleArn: Value: !GetAtt TenantManagementLambdaExecutionRole.Arn RegisterTenantFunctionArn: Value: !GetAtt RegisterTenantFunction.Arn ActivateTenantFunctionArn: Value: !GetAtt ActivateTenantFunction.Arn GetTenantsFunctionArn: Value: !GetAtt GetTenantsFunction.Arn CreateTenantFunctionArn: Value: !GetAtt CreateTenantFunction.Arn GetTenantFunctionArn: Value: !GetAtt GetTenantFunction.Arn DeactivateTenantFunctionArn: Value: !GetAtt DeactivateTenantFunction.Arn UpdateTenantFunctionArn: Value: !GetAtt UpdateTenantFunction.Arn GetUsersFunctionArn: Value: !GetAtt GetUsersFunction.Arn GetUserFunctionArn: Value: !GetAtt GetUserFunction.Arn UpdateUserFunctionArn: Value: !GetAtt UpdateUserFunction.Arn DisableUserFunctionArn: Value: !GetAtt DisableUserFunction.Arn CreateTenantAdminUserFunctionArn: Value: !GetAtt CreateTenantAdminUserFunction.Arn CreateUserFunctionArn: Value: !GetAtt CreateUserFunction.Arn DisableUsersByTenantFunctionArn: Value: !GetAtt DisableUsersByTenantFunction.Arn EnableUsersByTenantFunctionArn: Value: !GetAtt EnableUsersByTenantFunction.Arn SharedServicesAuthorizerFunctionArn: Value: !GetAtt SharedServicesAuthorizerFunction.Arn AuthorizerExecutionRoleArn: Value: !GetAtt AuthorizerExecutionRole.Arn ================================================ FILE: Lab4/server/nested_templates/tables.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to create dynamodb tables as part of bootstrap Resources: TenantDetailsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: tenantName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: ServerlessSaas-TenantConfig KeySchema: - AttributeName: tenantName KeyType: HASH Projection: NonKeyAttributes: - userPoolId - appClientId - apiGatewayUrl ProjectionType: INCLUDE TableName: ServerlessSaaS-TenantDetails TenantUserMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: userName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH - AttributeName: userName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantUserMapping GlobalSecondaryIndexes: - IndexName: UserName KeySchema: - AttributeName: userName KeyType: HASH - AttributeName: tenantId KeyType: RANGE Projection: ProjectionType: ALL Outputs: TenantDetailsTableArn: Value: !GetAtt TenantDetailsTable.Arn TenantDetailsTableName: Value: !Ref TenantDetailsTable TenantUserMappingTableArn: Value: !GetAtt TenantUserMappingTable.Arn TenantUserMappingTableName: Value: !Ref TenantUserMappingTable ================================================ FILE: Lab4/server/nested_templates/userinterface.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: IsCloudFrontAndS3PreProvisioned: Type: String Default: false Description: "Tells if cloudfront and s3 buckets are pre-provisioned or not. They get pre-provisioned when the workshop is running as a part of AWS Event through AWS event engine tool." Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref IsCloudFrontAndS3PreProvisioned, true] ] Resources: CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: IsNotRunningInEventEngine Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distributions" AdminAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AdminAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AdminAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AdminAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId AdminAppSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: #Aliases: # - !Sub 'admin.${CustomDomainName}' CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS CachedMethods: - GET - HEAD - OPTIONS Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: adminapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt AdminAppBucket.RegionalDomainName Id: adminapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' LandingAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True LandingAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref LandingAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${LandingAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId LandingApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: landingapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'LandingAppBucket.RegionalDomainName' Id: landingapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' AppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId ApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: tenantapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'AppBucket.RegionalDomainName' Id: tenantapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: AdminBucket: Description: The name of the bucket for uploading the Admin Management site to Value: !Ref AdminAppBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt AdminAppSite.DomainName Condition: IsNotRunningInEventEngine LandingAppBucket: Description: The name of the bucket for uploading the Landing site to Value: !Ref LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt LandingApplicationSite.DomainName Condition: IsNotRunningInEventEngine AppBucket: Description: The name of the bucket for uploading the Tenant Management site to Value: !Ref AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt ApplicationSite.DomainName Condition: IsNotRunningInEventEngine ================================================ FILE: Lab4/server/shared-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas" s3_bucket = "aws-saas-sam-cli-ujwbuket" s3_prefix = "serverless-saas" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" cached="true" parallel="true" ================================================ FILE: Lab4/server/shared-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to Bootstrap the Common Resources Parameters: AdminEmailParameter: Type: String Default: "test@test.com" Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Default: "SystemAdmin" Description: "Enter the role name for system admin" StageName: Type: String Default: "prod" Description: "Stage Name for the api" EventEngineParameter: Type: String Default: false Description: "Tells if this workshop is running as a part of AWS Event through AWS EventEngine or not" AdminUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Tenant Management userpool call back url" Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref EventEngineParameter, true] ] Resources: DynamoDBTables: Type: AWS::Serverless::Application Properties: Location: nested_templates/tables.yaml #Create cloudfront and s3 for UI Cde UserInterface: Type: AWS::Serverless::Application Properties: Location: nested_templates/userinterface.yaml Parameters: IsCloudFrontAndS3PreProvisioned: !Ref EventEngineParameter Cognito: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/cognito.yaml Parameters: AdminEmailParameter: !Ref AdminEmailParameter SystemAdminRoleNameParameter: !Ref SystemAdminRoleNameParameter AdminUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.AdminAppSite, !Ref AdminUserPoolCallbackURLParameter] TenantUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.ApplicationSite, !Ref TenantUserPoolCallbackURLParameter] LambdaFunctions: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/lambdafunctions.yaml Parameters: CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId CognitoOperationUsersUserPoolId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId CognitoOperationUsersUserPoolClientId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId TenantDetailsTableArn: !GetAtt DynamoDBTables.Outputs.TenantDetailsTableArn TenantUserMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantUserMappingTableArn APIs: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway.yaml Parameters: StageName: !Ref StageName RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn APIGatewayLambdaPermissions: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway_lambdapermissions.yaml Parameters: RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn AdminApiGatewayApi: !GetAtt APIs.Outputs.AdminApiGatewayApi Outputs: AdminApi: Description: "API Gateway endpoint URL for Admin API" Value: !Join ["", ["https://", !GetAtt APIs.Outputs.AdminApiGatewayApi, ".execute-api.", !Ref "AWS::Region", ".amazonaws.com/", !Ref StageName]] AdminSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant administration application Value: !GetAtt UserInterface.Outputs.AdminBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt UserInterface.Outputs.AdminAppSite Condition: IsNotRunningInEventEngine LandingApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the landing application Value: !GetAtt UserInterface.Outputs.LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt UserInterface.Outputs.LandingApplicationSite Condition: IsNotRunningInEventEngine ApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant application Value: !GetAtt UserInterface.Outputs.AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt UserInterface.Outputs.ApplicationSite Condition: IsNotRunningInEventEngine CognitoOperationUsersUserPoolProviderURL: Description: The Admin Management userpool provider url Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolProviderURL CognitoOperationUsersUserPoolClientId: Description: The Admin Management userpool client id Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId CognitoTenantUserPoolId: Description: The user pool id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolId Export: Name: "Serverless-SaaS-CognitoTenantUserPoolId" CognitoTenantAppClientId: Description: The app client id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolClientId Export: Name: "Serverless-SaaS-CognitoTenantAppClientId" AuthorizerExecutionRoleArn: Description: The Lambda authorizer execution role Value: !GetAtt LambdaFunctions.Outputs.AuthorizerExecutionRoleArn Export: Name: "Serverless-SaaS-AuthorizerExecutionRoleArn" ================================================ FILE: Lab4/server/tenant-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "stack-pooled" s3_bucket = "aws-saas-sam-cli-ujwbuket" s3_prefix = "serverless-saas-tenant" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM" cached="true" parallel="true" ================================================ FILE: Lab4/server/tenant-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Serverless SaaS Reference Architecture Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Parameters: StageName: Type: String Default: "prod" Description: "Stage Name for the api" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies-pooled Description: Utilities for project ContentUri: layers/ CompatibleRuntimes: - python3.9 LicenseInfo: 'MIT' RetentionPolicy: Retain Metadata: BuildMethod: python3.9 ProductTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: productId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: productId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: Product-pooled OrderTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: orderId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: orderId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: Order-pooled ProductFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: pooled-product-function-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: pooled-product-function-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt ProductTable.Arn GetProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable GetProductsFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_products Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable CreateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.create_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable UpdateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.update_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable DeleteProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.delete_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable OrderFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: pooled-order-function-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: pooled-order-function-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt OrderTable.Arn GetOrdersFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_orders Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable GetOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable CreateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.create_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable UpdateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.update_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable DeleteOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.delete_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable BusinessServicesAuthorizerFunction: Type: AWS::Serverless::Function Properties: CodeUri: Resources/ Handler: tenant_authorizer.lambda_handler Runtime: python3.9 Role: !ImportValue Serverless-SaaS-AuthorizerExecutionRoleArn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL: !ImportValue Serverless-SaaS-CognitoTenantUserPoolId TENANT_APP_CLIENT: !ImportValue Serverless-SaaS-CognitoTenantAppClientId ApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/api-gateway/access-logs-serverless-saas-tenant-api-pooled RetentionInDays: 30 ApiGatewayTenantApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: '/*' HttpMethod: '*' AccessLogSetting: DestinationArn: !GetAtt ApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: 'pooled-serverless-saas-tenant-api' basePath: !Join ['', ['/', !Ref StageName]] x-amazon-apigateway-api-key-source : "AUTHORIZER" schemes: - https paths: /order/{id}: get: summary: Returns a order description: Return a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a order description: Deletes a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /orders: get: summary: Returns all orders description: Returns all orders. produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrdersFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /order: post: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product/{id}: get: summary: Returns a product description: Return a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a product description: Deletes a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /products: get: summary: Returns all products description: Returns all products. produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductsFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product: post: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt BusinessServicesAuthorizerFunction.Arn - /invocations authorizerResultTtlInSeconds: 30 type: "token" StageName: !Ref StageName GetProductsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt GetProductsFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrdersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrdersFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt BusinessServicesAuthorizerFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref ApiGatewayTenantApi, "/*/*" ]] Outputs: TenantApiGatewayId: Description: Id for Tenant API Gateway Value: !Ref ApiGatewayTenantApi TenantAPI: Description: "API Gateway endpoint URL for Tenant API" Value: !Join ['', [!Sub "https://${ApiGatewayTenantApi}.execute-api.${AWS::Region}.amazonaws.com/", !Ref StageName]] ================================================ FILE: Lab5/client/Admin/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab5/client/Admin/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab5/client/Admin/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab5/client/Admin/README.md ================================================ # Admin This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab5/client/Admin/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "admin": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "admin:build:production" }, "development": { "browserTarget": "admin:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "admin:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab5/client/Admin/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab5/client/Admin/package.json ================================================ { "name": "admin", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/flex-layout": "~14.0.0-beta.40", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.1.3", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab5/client/Admin/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Tenants', url: '/tenants', icon: 'groups', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Lab5/client/Admin/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: '', component: NavComponent, data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'tenants', loadChildren: () => import('./views/tenants/tenants.module').then((m) => m.TenantsModule), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab5/client/Admin/src/app/app.component.scss ================================================ ================================================ FILE: Lab5/client/Admin/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Lab5/client/Admin/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { HttpInterceptorProviders } from './interceptors'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; @NgModule({ declarations: [AppComponent, NavComponent, AuthComponent], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, HttpClientModule, HttpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab5/client/Admin/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Lab5/client/Admin/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const HttpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Lab5/client/Admin/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Lab5/client/Admin/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Lab5/client/Admin/src/app/nav/nav.component.css ================================================ .sidenav-container { height: 100%; } .sidenav-content-container { background-color: lightgray; } .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .mat-toolbar.mat-primary { position: sticky; top: 0; z-index: 1; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .spinner-container { height: 80%; display: flex; justify-content: center; align-items: center; box-sizing: border-box; } ================================================ FILE: Lab5/client/Admin/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ username$ | async }}
================================================ FILE: Lab5/client/Admin/src/app/nav/nav.component.spec.ts ================================================ import { LayoutModule } from '@angular/cdk/layout'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { NavComponent } from './nav.component'; describe('NavComponent', () => { let component: NavComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [NavComponent], imports: [ NoopAnimationsModule, LayoutModule, MatButtonModule, MatIconModule, MatListModule, MatSidenavModule, MatToolbarModule, ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(NavComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should compile', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: Lab5/client/Admin/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.css'], }) export class NavComponent implements OnInit { tenantName = ''; loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router ) { this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => err); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map((sesh) => sesh && sesh.isValid()) ); const token$ = session$.pipe(map((sesh) => sesh && sesh.getIdToken())); this.username$ = token$.pipe( map((t) => t && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } } ================================================ FILE: Lab5/client/Admin/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Lab5/client/Admin/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Lab5/client/Admin/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Lab5/client/Admin/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Lab5/client/Admin/src/app/views/dashboard/dashboard.component.html ================================================

Traffic

December 2020
================================================ FILE: Lab5/client/Admin/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } .chart-wrapper { padding-right: 20px; position: relative; margin: auto; height: 80vh; width: 80vw; } ================================================ FILE: Lab5/client/Admin/src/app/views/dashboard/dashboard.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ChartConfiguration, ChartType } from 'chart.js'; import { TenantsService } from '../tenants/tenants.service'; interface DataSet { label: string; data: number[]; } interface ChartData { tenantId: string; dataSet: DataSet[]; totalOrders: number; } @Component({ templateUrl: 'dashboard.component.html', styleUrls: ['./dashboard.component.scss'], selector: 'app-dashboard', }) export class DashboardComponent implements OnInit { constructor(private tenantSvc: TenantsService) {} data: ChartData[] = []; // lineChart public lineChartElements = 27; public lineChartData1: Array = []; public lineChartData2: Array = []; public lineChartData3: Array = []; public lineChartData: Array = [ { data: this.lineChartData1, label: 'Current', backgroundColor: 'rgba(148,159,177,0.2)', borderColor: 'rgba(148,159,177,1)', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, { data: this.lineChartData2, label: 'Previous', backgroundColor: 'rgba(77,83,96,0.2)', borderColor: 'rgba(77,83,96,1)', pointBackgroundColor: 'rgba(77,83,96,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(77,83,96,1)', fill: 'origin', }, { data: this.lineChartData3, label: 'BEP', backgroundColor: 'rgba(255,0,0,0.3)', borderColor: 'red', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, ]; public lineChartLegend = false; /* tslint:disable:max-line-length */ public lineChartLabels: Array = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ]; /* tslint:enable:max-line-length */ public lineChartType: ChartType = 'line'; public lineChartOptions: ChartConfiguration['options'] = { maintainAspectRatio: false, elements: { line: { tension: 0.5, }, }, scales: { // We use this empty structure as a placeholder for dynamic theming. x: {}, 'y-axis-0': { position: 'left', }, 'y-axis-1': { position: 'right', grid: { color: 'rgba(80,80,80,0.3)', }, ticks: { color: '#808080', }, }, }, }; public random(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } ngOnInit(): void { for (let i = 0; i <= this.lineChartElements; i++) { this.lineChartData1.push(this.random(50, 200)); this.lineChartData2.push(this.random(80, 100)); this.lineChartData3.push(65); } } } ================================================ FILE: Lab5/client/Admin/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Lab5/client/Admin/src/app/views/tenants/create/create.component.html ================================================
Provision tenant Name Email Phone Address Plan
================================================ FILE: Lab5/client/Admin/src/app/views/tenants/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab5/client/Admin/src/app/views/tenants/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { TenantsService } from '../tenants.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { submitting = false; tenantForm: FormGroup = this.fb.group({ tenantName: [null, [Validators.required]], tenantEmail: [null, [Validators.email, Validators.required]], tenantTier: [null, [Validators.required]], tenantPhone: [null], tenantAddress: [null], }); constructor( private fb: FormBuilder, private tenantSvc: TenantsService, private router: Router, private _snackBar: MatSnackBar ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; const user = { ...this.tenantForm.value, }; this.tenantSvc.post(this.tenantForm.value).subscribe({ next: () => { this.submitting = false; this.openErrorMessageSnackBar('Successfully created new tenant!'); this.router.navigate(['tenants']); }, error: (err) => { this.submitting = false; this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); }, }); } } ================================================ FILE: Lab5/client/Admin/src/app/views/tenants/list/list.component.html ================================================
Tenant List
Id {{ element.tenantId }} Tenant Name {{ element.tenantName }} E-Mail {{ element.tenantEmail }} Plan {{ element.tenantTier }} Status {{ element.isActive }}
================================================ FILE: Lab5/client/Admin/src/app/views/tenants/list/list.component.scss ================================================ .tenant-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab5/client/Admin/src/app/views/tenants/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Tenant } from '../models/tenant'; import { TenantsService } from '../tenants.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { tenants$ = new Observable(); tenantData: Tenant[] = []; isLoading: boolean = true; displayedColumns = [ 'tenantId', 'tenantName', 'tenantEmail', 'tenantTier', 'isActive', ]; constructor(private tenantSvc: TenantsService) {} ngOnInit(): void { this.tenantSvc.fetch().subscribe((data) => { this.tenantData = data; this.isLoading = false; }); } } ================================================ FILE: Lab5/client/Admin/src/app/views/tenants/models/tenant.ts ================================================ export interface Tenant { tenantId: string; tenantName: string; tenantEmail: string; tenantTier: string; isActive: boolean; } ================================================ FILE: Lab5/client/Admin/src/app/views/tenants/tenants-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', component: ListComponent, data: { title: 'Tenant List', }, }, { path: 'create', component: CreateComponent, data: { title: 'Provision new tenant', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class TenantsRoutingModule {} ================================================ FILE: Lab5/client/Admin/src/app/views/tenants/tenants.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ListComponent } from './list/list.component'; import { TenantsRoutingModule } from './tenants-routing.module'; import { CreateComponent } from './create/create.component'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, TenantsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ReactiveFormsModule, MatSnackBarModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class TenantsModule {} ================================================ FILE: Lab5/client/Admin/src/app/views/tenants/tenants.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Tenant } from './models/tenant'; @Injectable({ providedIn: 'root', }) export class TenantsService { constructor(private http: HttpClient) {} baseUrl = `${environment.apiUrl}`; tenantsApiUrl = `${this.baseUrl}/tenants`; registrationApiUrl = `${this.baseUrl}/registration`; // TODO strongly-type these anys as tenants once we dial in what the tenant call should return fetch(): Observable { return this.http.get(this.tenantsApiUrl); } post(tenant: Tenant): Observable { return this.http.post(this.registrationApiUrl, tenant); } } ================================================ FILE: Lab5/client/Admin/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
Tenant Id folder_special
================================================ FILE: Lab5/client/Admin/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab5/client/Admin/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], tenantId: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Lab5/client/Admin/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Lab5/client/Admin/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab5/client/Admin/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Lab5/client/Admin/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Lab5/client/Admin/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Lab5/client/Admin/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Lab5/client/Admin/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.apiUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } } ================================================ FILE: Lab5/client/Admin/src/assets/.gitkeep ================================================ ================================================ FILE: Lab5/client/Admin/src/aws-exports.ts ================================================ const awsmobile = { aws_project_region: 'us-west-2', aws_cognito_region: 'us-west-2', aws_user_pools_id: 'us-west-2_nWhwjijMc', aws_user_pools_web_client_id: '61ipvhmvdrfso0cftpil9irk8k', }; export default awsmobile; ================================================ FILE: Lab5/client/Admin/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab5/client/Admin/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab5/client/Admin/src/environments/environment.ts ================================================ export const environment = { production: false, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab5/client/Admin/src/index.html ================================================ Dashboard ================================================ FILE: Lab5/client/Admin/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { Amplify } from 'aws-amplify'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import aws_exports from './aws-exports'; Amplify.configure(aws_exports); if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab5/client/Admin/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab5/client/Admin/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab5/client/Admin/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab5/client/Admin/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; ================================================ FILE: Lab5/client/Admin/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab5/client/Admin/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab5/client/Admin/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab5/client/Admin/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab5/client/Application/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab5/client/Application/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab5/client/Application/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end #cypress cypress/videos/* cypress.env.json # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab5/client/Application/README.md ================================================ # Application This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab5/client/Application/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "application": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "2mb", "maximumError": "2mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "6kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "application:build:production" }, "development": { "browserTarget": "application:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "application:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab5/client/Application/cypress/README.md ================================================ # Application End-to-End Testing ## Instructions To run End-to-End (e2e) tests against the Sample Application, take the following steps: 1. Make a copy of the example env file (`cypress.env.json.example`): ```bash cp cypress.env.json.example cypress.env.json ``` 2. Edit the new file and replace the sample values with real values. The following should help when deciding what to use in place of the sample values provided: - `host`: The URL where the Sample Application is running. If testing locally, this is usually `"http://localhost:4200"`. - `tenantName`: The name of the tenant used to identify the appropriate Cognito User Pool to use for Authentication. - `tenantUsername`: The username to use when logging in. - `tenantUserPassword`: The password to use when logging in. - `email`: The email address to use for testing. (This should be a valid email address.) 3. Navigate to the root of the Application project (`aws-saas-factory-ref-solution-serverless-saas/clients/Application/`) and run the following: ```bash npx cypress run ``` This will run the tests located in the `cypress/e2e` folder. The documentation [here](https://docs.cypress.io/guides/guides/command-line#cypress-run) has more information on what can be passed in as arguments when running the Cypress tests. For example, running the following will show the Cypress UI and what is happening as each of the tests are run: ```bash npx cypress run --headed ``` ================================================ FILE: Lab5/client/Application/cypress/e2e/1-getting-started/basic-access.cy.js ================================================ /// describe('check that the app redirects to /unauthorized when tenant is not set', () => { it('redirects to unauthorized when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); }) it('redirects to unauthorized when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); }) it('redirects to unauthorized when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); }) }) describe('check that the app redirects to a page with a sign-in form when tenant is set and user is not logged in', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').should('exist') cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.location('href').should('contain', '/dashboard') }) it('redirects when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) }) ================================================ FILE: Lab5/client/Application/cypress/e2e/1-getting-started/product-testing.cy.js ================================================ /// describe('check that product, order and user functionality works as expected', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.get('form input[name="username"]').type(Cypress.env('tenantUsername')) cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() cy.wait(1500) cy.get('body').then(body => { if (body.find('form input[name="confirm_password"]').length > 0) { cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form input[name="confirm_password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() } }) }) it('can create new users and display them', () => { const email_username = Cypress.env('email').split('@')[0]; const email_domain = Cypress.env('email').split('@')[1]; const random_suffix = '+test'+Date.now().toString().slice(-3); const myUser = { name: "myUser-"+Date.now(), email: email_username + random_suffix + '@' + email_domain, role: 'userRole'+Date.now().toString().slice(-5) } cy.get("a").contains("Users").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.get("button[color='primary'").contains("Add User").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="userName"]').type(myUser.name) cy.get('input[formcontrolname="userEmail"]').type(myUser.email) cy.get('input[formcontrolname="userRole"]').type(myUser.role) }) cy.intercept({ method: 'POST', url: '**/user', }).as('postUser') cy.intercept({ method: 'GET', url: '**/users', }).as('getUsers') cy.get("button").contains("Create").click() cy.wait('@postUser') cy.go('back') cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.wait('@getUsers') cy.get('table td').contains(myUser.email) }) it('can create a new order with a new product and see them listed', () => { const myProduct = { name: "myProduct-"+Date.now(), price: Date.now().toString().slice(-3), sku: Date.now().toString().slice(-5), category: "category3", } const myOrder = { name: "myOrder-"+Date.now(), } // NOW TESTING PRODUCT CREATION // cy.get("a").contains("Products").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.get("button[color='primary'").contains("Create Product").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="name"]').type(myProduct.name) cy.get('input[formcontrolname="price"]').type(myProduct.price) cy.get('input[formcontrolname="sku"]').type(myProduct.sku) cy.get('mat-select[formcontrolname="category"]').click() }) cy.get('.mat-option-text').contains(myProduct.category).click() cy.get("button").contains("Submit").should('not.be.disabled') cy.intercept({ method: 'POST', url: '**/product', }).as('postProduct') cy.intercept({ method: 'GET', url: '**/products', }).as('getProducts') cy.get("button").contains("Submit").click() cy.wait('@postProduct') cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.wait('@getProducts') cy.get('table td').contains(myProduct.name) cy.get('table td').contains(myProduct.price) cy.get("a").contains("Orders").click() // DONE TESTING PRODUCT CREATION // // NOW TESTING ORDER CREATION // cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.get("button[color='primary']").contains("Create Order").click() cy.location().should((loc) => { expect(loc.href).to.contain('/orders/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="orderName"]').type(myOrder.name) }) cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.intercept({ method: 'POST', url: '**/order', }).as('postOrder') cy.intercept({ method: 'GET', url: '**/orders', }).as('getOrders') cy.get("button").contains("Submit").click() cy.wait('@postOrder') cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.wait('@getOrders') cy.get('table td').contains(myOrder.name) cy.get('table td').contains(new Intl.NumberFormat().format(myProduct.price * 2)) // DONE TESTING ORDER CREATION // }) }) ================================================ FILE: Lab5/client/Application/cypress.config.ts ================================================ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { supportFile: false, setupNodeEvents(on, config) { // implement node event listeners here }, }, }); ================================================ FILE: Lab5/client/Application/cypress.env.json.example ================================================ { "host": "http://localhost:4200", "tenantName": "UPDATE_ME!", "tenantUsername": "UPDATE_ME!", "tenantUserPassword": "UPDATE_ME!", "email": "test@example.com" } ================================================ FILE: Lab5/client/Application/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/application'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab5/client/Application/package.json ================================================ { "name": "application", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "e2e": "npx cypress run" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.2.0", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "cypress": "~10.3.1", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab5/client/Application/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Products', url: '/products', icon: 'sell', }, { name: 'Orders', url: '/orders', icon: 'shopping_cart', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Lab5/client/Application/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { CognitoGuard } from './cognito.guard'; export const routes: Routes = [ { path: '', redirectTo: 'unauthorized', pathMatch: 'full', }, { path: '', component: NavComponent, canActivate: [CognitoGuard], data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'orders', loadChildren: () => import('./views/orders/orders.module').then((m) => m.OrdersModule), }, { path: 'products', loadChildren: () => import('./views/products/products.module').then( (m) => m.ProductsModule ), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, { path: 'unauthorized', component: UnauthorizedComponent, }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab5/client/Application/src/app/app.component.scss ================================================ ================================================ FILE: Lab5/client/Application/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Lab5/client/Application/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { httpInterceptorProviders } from './interceptors'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; @NgModule({ declarations: [ AppComponent, NavComponent, AuthComponent, UnauthorizedComponent, ], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, MatSnackBarModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatIconModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, httpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab5/client/Application/src/app/cognito.guard.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, } from '@angular/router'; import { Auth } from 'aws-amplify'; import { AuthConfigurationService } from './views/auth/auth-configuration.service'; @Injectable({ providedIn: 'root' }) export class CognitoGuard implements CanActivate { constructor( private router: Router, private authConfigService: AuthConfigurationService ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Promise { if (!this.authConfigService.configureAmplifyAuth()) { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return new Promise((res, rej) => { res(false); }); } return Auth.currentSession() .then((u) => { if (u.isValid()) { return true; } else { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return false; } }) .catch((e) => { if (state.url === '/dashboard') { // if we're going to the dashboard and we're not logged in, // don't stop the flow as the amplify-authenticator will // route requests going to the dashboard to the sign-in page. return true; } console.log('Error getting current session', e); this.router.navigate(['/unauthorized']); return false; }); } } ================================================ FILE: Lab5/client/Application/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Lab5/client/Application/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const httpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Lab5/client/Application/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Lab5/client/Application/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Lab5/client/Application/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ (companyName$ | async) || "" }} {{ (username$ | async) || user.username}}
================================================ FILE: Lab5/client/Application/src/app/nav/nav.component.scss ================================================ .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .sidebar-icon-container { height: 64px; background-color: whitesmoke; display: flex; align-items: center; justify-content: center; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .footer { position: fixed; bottom: 0; width: 100%; height: 40px; background-color: whitesmoke; color: #2d3337; // text-align: center; margin: 0px; } .footer-text { display: flex; } .content { position: absolute; width: 100%; height: 90%; max-height: 90%; overflow: auto; background-color: lightgray; } ================================================ FILE: Lab5/client/Application/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; import { AuthConfigurationService } from './../views/auth/auth-configuration.service'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.scss'], }) export class NavComponent implements OnInit { loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router, private authConfigService: AuthConfigurationService ) { // this.configSvc.loadConfigurations().subscribe((val) => console.log(val)); this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => { console.log('Failed to get current session. Err: ', err); return err; }); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map( (sesh) => sesh && typeof sesh.isValid === 'function' && sesh.isValid() ) ); const token$ = session$.pipe( map( (sesh) => sesh && typeof sesh.getIdToken === 'function' && sesh.getIdToken() ) ); this.username$ = token$.pipe( map((t) => t && t.payload && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload && t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } async logout() { await Auth.signOut({ global: true }) .then((e) => { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); }) .catch((err) => { console.error('Error logging out: ', err); }); } } ================================================ FILE: Lab5/client/Application/src/app/views/auth/auth-configuration.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpParams, HttpParamsOptions, } from '@angular/common/http'; import { Injectable, OnInit } from '@angular/core'; import { map, switchMap, catchError } from 'rxjs/operators'; import { throwError } from 'rxjs'; import { environment } from '../../../environments/environment'; import { ConfigParams } from './models/config-params'; import { ActivatedRoute } from '@angular/router'; import Amplify from 'aws-amplify'; import { Auth } from 'aws-amplify'; import { Router } from '@angular/router'; import { from, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AuthConfigurationService { params$: Observable; params: ConfigParams; tenantName: string; constructor( private http: HttpClient, private route: ActivatedRoute, private router: Router ) {} public setTenantConfig(tenantName: string): Promise { const url = `${environment.regApiGatewayUrl}/tenant/init/` + tenantName; this.params$ = this.http.get(url); const setup$ = this.params$.pipe( map((val) => { // remove trailing slash (/) if present val.apiGatewayUrl = val.apiGatewayUrl.replace(/\/$/, ''); localStorage.setItem('userPoolId', val.userPoolId); localStorage.setItem('tenantName', tenantName); localStorage.setItem('appClientId', val.appClientId); localStorage.setItem('apiGatewayUrl', val.apiGatewayUrl); return 'success'; }), catchError((error) => { console.log('Error setting tenant config: ', error); return throwError(error); }) ); return setup$.toPromise(); } configureAmplifyAuth(): boolean { try { const userPoolId = localStorage.getItem('userPoolId'); const appClientId = localStorage.getItem('appClientId'); if (!userPoolId || !appClientId) { return false; } const region = userPoolId?.split('_')[0]; const awsmobile = { aws_project_region: region, aws_cognito_region: region, aws_user_pools_id: userPoolId, aws_user_pools_web_client_id: appClientId, }; Amplify.configure(awsmobile); return true; } catch (err) { console.error('Unable to initialize amplify auth.', err); return false; } } cleanLocalStorage() { localStorage.removeItem('tenantName'); localStorage.removeItem('userPoolId'); localStorage.removeItem('appClientId'); localStorage.removeItem('apiGatewayUrl'); } } ================================================ FILE: Lab5/client/Application/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Lab5/client/Application/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Lab5/client/Application/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Lab5/client/Application/src/app/views/auth/models/config-params.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ export interface ConfigParams { appClientId: string; userPoolId: string; apiGatewayUrl: string; } ================================================ FILE: Lab5/client/Application/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Lab5/client/Application/src/app/views/dashboard/dashboard.component.html ================================================

Dashboard

{{ card.title }}
================================================ FILE: Lab5/client/Application/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } ================================================ FILE: Lab5/client/Application/src/app/views/dashboard/dashboard.component.ts ================================================ import { Component, ViewChild } from '@angular/core'; import { map } from 'rxjs/operators'; import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js'; import { BaseChartDirective } from 'ng2-charts'; import DataLabelsPlugin from 'chartjs-plugin-datalabels'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'], }) export class DashboardComponent { @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; public barChartOptions: ChartConfiguration['options'] = { responsive: true, maintainAspectRatio: false, // We use these empty structures as placeholders for dynamic theming. scales: { x: {}, y: { min: 10, }, }, plugins: { legend: { display: true, }, datalabels: { anchor: 'end', align: 'end', }, }, }; public barChartType: ChartType = 'bar'; public barChartPlugins = [DataLabelsPlugin]; public barChartData: ChartData<'bar'> = { labels: ['2006', '2007', '2008', '2009', '2010', '2011', '2012'], datasets: [ { data: [65, 59, 80, 81, 56, 55, 40], label: 'Series A' }, { data: [28, 48, 40, 19, 86, 27, 90], label: 'Series B' }, ], }; // events public chartClicked({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public chartHovered({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public randomize(): void { // Only Change 3 values this.barChartData.datasets[0].data = [ Math.round(Math.random() * 100), 59, 80, Math.round(Math.random() * 100), 56, Math.round(Math.random() * 100), 40, ]; this.chart?.update(); } /** Based on the screen size, switch from standard to one column per row */ cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe( map(({ matches }) => { if (matches) { return [ { title: 'Card 1', cols: 1, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 1 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; } return [ { title: 'Card 1', cols: 2, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 2 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; }) ); constructor(private breakpointObserver: BreakpointObserver) {} } ================================================ FILE: Lab5/client/Application/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Lab5/client/Application/src/app/views/error/404.component.html ================================================

404

Oops! You're lost.

The page you are looking for was not found.

================================================ FILE: Lab5/client/Application/src/app/views/error/404.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '404.component.html', }) export class P404Component { constructor() {} } ================================================ FILE: Lab5/client/Application/src/app/views/error/500.component.html ================================================

500

Houston, we have a problem!

The page you are looking for is temporarily unavailable.

================================================ FILE: Lab5/client/Application/src/app/views/error/500.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '500.component.html', }) export class P500Component { constructor() {} } ================================================ FILE: Lab5/client/Application/src/app/views/error/unauthorized.component.html ================================================
Unauthorized Enter your tenant name and click submit below Tenant Name home
================================================ FILE: Lab5/client/Application/src/app/views/error/unauthorized.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .center-screen { display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; min-height: 100vh; } ================================================ FILE: Lab5/client/Application/src/app/views/error/unauthorized.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AuthConfigurationService } from './../auth/auth-configuration.service'; import { Observable } from 'rxjs'; import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-unauthorized', templateUrl: './unauthorized.component.html', styleUrls: ['./unauthorized.component.scss'], }) export class UnauthorizedComponent implements OnInit { tenantForm: FormGroup; params$: Observable; error = false; errorMessage: string; tenantNameRequired: boolean = true; constructor( private fb: FormBuilder, private authConfigService: AuthConfigurationService, private _snackBar: MatSnackBar, private router: Router ) { if ( 'userPoolId' in environment && 'appClientId' in environment && 'apiGatewayUrl' in environment ) { // If a tenant's cognito configuration is provided in the // "environment" object, then we take that instead of asking // the visitor to provide the name of their tenant in order // to do a look-up for that tenant's cognito configuration. localStorage.setItem('tenantName', 'PooledTenants'); localStorage.setItem('userPoolId', (environment as any).userPoolId); localStorage.setItem('appClientId', (environment as any).appClientId); localStorage.setItem('apiGatewayUrl', (environment as any).apiGatewayUrl); this.tenantNameRequired = false; } } ngOnInit(): void { this.tenantForm = this.fb.group({ tenantName: [null, [Validators.required]], }); } isFieldInvalid(field: string) { const formField = this.tenantForm.get(field); return ( formField && formField.invalid && (formField.dirty || formField.touched) ); } displayFieldCss(field: string) { return { 'is-invalid': this.isFieldInvalid(field), }; } hasRequiredError(field: string) { return !!this.tenantForm.get(field)?.hasError('required'); } openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } login() { if (!this.tenantNameRequired) { this.router.navigate(['/dashboard']); return true; } let tenantName = this.tenantForm.value.tenantName; if (!tenantName) { this.errorMessage = 'No tenant name provided.'; this.error = true; this.openErrorMessageSnackBar(this.errorMessage); return false; } this.authConfigService .setTenantConfig(tenantName) .then((val) => { this.router.navigate(['/dashboard']); }) .catch((errorResponse) => { this.error = true; this.errorMessage = errorResponse.error.message || 'An unexpected error occurred!'; this.openErrorMessageSnackBar(this.errorMessage); }); return false; } } ================================================ FILE: Lab5/client/Application/src/app/views/orders/create/create.component.html ================================================
Create new order Enter order name Name is required
{{ op.product.name }}
{{ op.product.price | currency }}
{{ op.quantity || 0 }}
================================================ FILE: Lab5/client/Application/src/app/views/orders/create/create.component.scss ================================================ // .card { // margin: 20px; // display: flex; // flex-direction: column; // align-items: flex-start; // } .order-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab5/client/Application/src/app/views/orders/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { Product } from '../../products/models/product.interface'; import { ProductService } from '../../products/product.service'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; interface LineItem { product: Product; quantity?: number; } @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { displayedColumns = []; orderForm: FormGroup; orderProducts: LineItem[] = []; isLoadingProducts: boolean = true; error = ''; constructor( private fb: FormBuilder, private productSvc: ProductService, private orderSvc: OrdersService, private router: Router ) { this.orderForm = this.fb.group({ name: ['', Validators.required], }); } ngOnInit(): void { this.productSvc.fetch().subscribe((products) => { this.orderProducts = products.map((p) => ({ product: p })); this.isLoadingProducts = false; }); this.orderForm = this.fb.group({ orderName: ['', Validators.required], }); } get name() { return this.orderForm.get('name'); } get productQuantity() { return this.orderProducts .filter((p) => !!p.quantity).length > 0; } add(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity ? orderProduct.quantity + 1 : 1, }; } return p; }); } remove(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity && orderProduct.quantity > 1 ? orderProduct.quantity - 1 : undefined, }; } return p; }); } submit() { const val: Order = { ...this.orderForm?.value, orderProducts: this.orderProducts .filter((p) => !!p.quantity) .map((p) => ({ productId: p.product.productId, price: p.product.price, quantity: p.quantity, })), }; this.orderSvc.create(val).subscribe(() => this.router.navigate(['orders'])); } } ================================================ FILE: Lab5/client/Application/src/app/views/orders/detail/detail.component.html ================================================

Order Details mostly made up

NO: {{ orderId$ | async }} | Date: {{ today() | date }}
  • {{ tenantName() }}

Services / Products

Item / Details Unit Cost Sum Cost Discount Tax Total
{{ op.productId }}
{{ op.price | currency }}
Before Tax
{{ sum(op) | currency }}
{{ op.quantity }} Units
$0.00
None
{{ tax(op) | currency }}
Sales Tax 8.9%
{{ total(op) | currency }}
Sub Total Discount Total Tax Final
{{ subTotal(order) | currency }} -$0.00 {{ subTotal(order) | currency }} {{ calcTax(order) | currency }} {{ final(order) | currency }}
Comments / Notes
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Odit repudiandae numquam sit facere blanditiis, quasi distinctio ipsam? Libero odit ex expedita, facere sunt, possimus consectetur dolore, nobis iure amet vero.
================================================ FILE: Lab5/client/Application/src/app/views/orders/detail/detail.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .no-bottom-margin { margin-bottom: 0; } .nowrap { white-space: nowrap; } .invoice { font-family: Arial, Helvetica, sans-serif; width: 970px !important; margin: 50px auto; .invoice-header { padding: 25px 25px 15px; h1 { margin: 0; } .media { .media-body { font-size: 0.9em; margin: 0; } } } .invoice-body { border-radius: 10px; padding: 25px; background: #fff; } .invoice-footer { padding: 15px; font-size: 0.9em; text-align: center; color: #999; } } .logo { max-height: 70px; border-radius: 10px; } .dl-horizontal { margin: 0; dt { float: left; width: 80px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } dd { margin-left: 90px; } } .rowamount { padding-top: 15px !important; } .rowtotal { font-size: 1.3em; } .colfix { width: 12%; } .mono { font-family: monospace; } ================================================ FILE: Lab5/client/Application/src/app/views/orders/detail/detail.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Order } from '../models/order.interface'; import { OrderProduct } from '../models/orderproduct.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-detail', templateUrl: './detail.component.html', styleUrls: ['./detail.component.scss'], }) export class DetailComponent implements OnInit { orderId$: Observable | undefined; order$: Observable | undefined; orderProducts$: Observable | undefined; taxRate = 0.0899; constructor(private route: ActivatedRoute, private orderSvc: OrdersService) {} ngOnInit(): void { this.orderId$ = this.route.params.pipe(map((o) => o['orderId'])); this.order$ = this.orderId$.pipe(switchMap((o) => this.orderSvc.get(o))); this.orderProducts$ = this.order$.pipe(map((o) => o.orderProducts)); } today() { return new Date(); } tenantName() { return ''; } sum(op: OrderProduct) { return op.price * op.quantity; } tax(op: OrderProduct) { return this.sum(op) * this.taxRate; } total(op: OrderProduct) { return this.sum(op) + this.tax(op); } subTotal(order: Order) { return order.orderProducts .map((op) => op.price * op.quantity) .reduce((acc, curr) => acc + curr); } calcTax(order: Order) { return this.subTotal(order) * this.taxRate; } final(order: Order) { return this.subTotal(order) + this.calcTax(order); } } ================================================ FILE: Lab5/client/Application/src/app/views/orders/list/list.component.html ================================================

Order List

Name {{ element.orderName }} Line Items {{ element.orderProducts?.length }} Total {{ sum(element) | currency }}
================================================ FILE: Lab5/client/Application/src/app/views/orders/list/list.component.scss ================================================ .order-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Lab5/client/Application/src/app/views/orders/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { displayedColumns: string[] = ['name', 'lineItems', 'total']; orderData: Order[] = []; isLoading: boolean = true; constructor(private orderSvc: OrdersService, private router: Router) {} ngOnInit(): void { this.orderSvc.fetch().subscribe((data) => { this.isLoading = false; this.orderData = data; }); } sum(order: Order): number { return order.orderProducts .map((p) => p.price * p.quantity) .reduce((acc, curr) => acc + curr); } } ================================================ FILE: Lab5/client/Application/src/app/views/orders/models/order.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { OrderProduct } from './orderproduct.interface'; export interface Order { key: string; shardId: string; orderId: string; orderName: string; orderProducts: OrderProduct[]; } ================================================ FILE: Lab5/client/Application/src/app/views/orders/models/orderproduct.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface OrderProduct { productId: string; price: number; quantity: number; } ================================================ FILE: Lab5/client/Application/src/app/views/orders/orders-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', data: { title: 'All Orders', }, component: ListComponent, }, { path: 'create', data: { title: 'Create Order', }, component: CreateComponent, }, { path: 'detail/:orderId', data: { title: 'View Order Detail', }, component: DetailComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class OrdersRoutingModule {} ================================================ FILE: Lab5/client/Application/src/app/views/orders/orders.module.ts ================================================ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; import { OrdersRoutingModule } from './orders-routing.module'; @NgModule({ declarations: [CreateComponent, ListComponent, DetailComponent], imports: [ CommonModule, OrdersRoutingModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class OrdersModule {} ================================================ FILE: Lab5/client/Application/src/app/views/orders/orders.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Order } from './models/order.interface'; @Injectable({ providedIn: 'root', }) export class OrdersService { orders: Order[] = []; baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; constructor(private http: HttpClient) {} fetch(): Observable { return this.http.get(`${this.baseUrl}/orders`); } get(orderId: string): Observable { const url = `${this.baseUrl}/order/${orderId}`; return this.http.get(url); } create(order: Order): Observable { return this.http.post(`${this.baseUrl}/order`, order); } } ================================================ FILE: Lab5/client/Application/src/app/views/products/create/create.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required SKU Category {{category}}
================================================ FILE: Lab5/client/Application/src/app/views/products/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab5/client/Application/src/app/views/products/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ProductService } from '../product.service'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); } get name() { return this.productForm.get('name'); } get price() { return this.productForm.get('price'); } submit() { this.productSvc.post(this.productForm.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err.message); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Lab5/client/Application/src/app/views/products/edit/edit.component.html ================================================
Edit Product Product ID Enter product name Name is required Enter product price Price is required SKU Category {{ category }}
================================================ FILE: Lab5/client/Application/src/app/views/products/edit/edit.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab5/client/Application/src/app/views/products/edit/edit.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.scss'], }) export class EditComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; product$: Observable | undefined; productId$: Observable | undefined; productName$: Observable | undefined; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router, private route: ActivatedRoute ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ shardId: [], productId: [], name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); this.productId$ = this.route.params.pipe(map((p) => p['productId'])); this.product$ = this.productId$.pipe( switchMap((p) => this.productSvc.get(p)) ); this.productName$ = this.product$.pipe(map((p) => p?.name)); this.product$.subscribe((val) => { this.productForm?.patchValue({ ...val, }); }); } get name() { return this.productForm?.get('name'); } get price() { return this.productForm?.get('price'); } submit() { this.productSvc.put(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => console.error(err), }); } delete() { this.productSvc.delete(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Lab5/client/Application/src/app/views/products/list/list.component.html ================================================

Product List

Name {{ element.name }} Price {{ element.price | currency }} SKU {{ element.sku }}
================================================ FILE: Lab5/client/Application/src/app/views/products/list/list.component.scss ================================================ .product-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Lab5/client/Application/src/app/views/products/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { productData: Product[] = []; isLoading: boolean = true; displayedColumns: string[] = ['name', 'price', 'sku']; constructor(private productSvc: ProductService, private router: Router) {} ngOnInit(): void { this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onEdit(product: Product) { this.router.navigate(['products', 'edit', product.productId]); return false; } onRemove(product: Product) { this.productSvc.delete(product); this.isLoading = true; this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onCreate() { this.router.navigate(['products', 'create']); } } ================================================ FILE: Lab5/client/Application/src/app/views/products/models/product.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface Product { key: string; shardId: string; productId: string; name: string; price: number; sku: string; category: string; pictureUrl?: string; } ================================================ FILE: Lab5/client/Application/src/app/views/products/product.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Product } from './models/product.interface'; @Injectable({ providedIn: 'root', }) export class ProductService { constructor(private http: HttpClient) {} baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; fetch(): Observable { return this.http.get(`${this.baseUrl}/products`); } get(productId: string): Observable { const url = `${this.baseUrl}/product/${productId}`; return this.http.get(url); } delete(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.delete(url); } put(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.put(url, product); } post(product: Product) { return this.http.post(`${this.baseUrl}/product`, product); } } ================================================ FILE: Lab5/client/Application/src/app/views/products/products-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'Product List', }, component: ListComponent, }, { path: 'create', data: { title: 'Create new Product', }, component: CreateComponent, }, { path: 'edit/:productId', data: { title: 'Edit Product', }, component: EditComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class ProductsRoutingModule {} ================================================ FILE: Lab5/client/Application/src/app/views/products/products.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ProductsRoutingModule } from './products-routing.module'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [CreateComponent, EditComponent, ListComponent], imports: [ CommonModule, ReactiveFormsModule, ProductsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class ProductsModule {} ================================================ FILE: Lab5/client/Application/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
================================================ FILE: Lab5/client/Application/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab5/client/Application/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Lab5/client/Application/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Lab5/client/Application/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab5/client/Application/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Lab5/client/Application/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Lab5/client/Application/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Lab5/client/Application/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Lab5/client/Application/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { find, mergeMap, defaultIfEmpty } from 'rxjs/operators'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.regApiGatewayUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } update(email: string, user: User) {} } ================================================ FILE: Lab5/client/Application/src/assets/.gitkeep ================================================ ================================================ FILE: Lab5/client/Application/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab5/client/Application/src/environments/environment.prod.ts ================================================ export const environment = { production: false, regApiGatewayUrl: 'https://ulp15c9bv2.execute-api.us-west-2.amazonaws.com/prod/', }; ================================================ FILE: Lab5/client/Application/src/environments/environment.ts ================================================ export const environment = { production: false, regApiGatewayUrl: 'https://ulp15c9bv2.execute-api.us-west-2.amazonaws.com/prod/', }; ================================================ FILE: Lab5/client/Application/src/index.html ================================================ Application ================================================ FILE: Lab5/client/Application/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab5/client/Application/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab5/client/Application/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab5/client/Application/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab5/client/Application/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "~bootstrap/scss/functions"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/maps"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; .chart-container canvas { max-height: 250px; width: auto; } .chart-container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; } ================================================ FILE: Lab5/client/Application/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab5/client/Application/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab5/client/Application/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "strictPropertyInitialization": false, "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab5/client/Application/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab5/client/Landing/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab5/client/Landing/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab5/client/Landing/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab5/client/Landing/README.md ================================================ # Landing This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab5/client/Landing/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "Landing": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/custom-theme.scss", "src/styles.scss", "node_modules/font-awesome/css/font-awesome.min.css" ] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "10kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "Landing:build:production" }, "development": { "browserTarget": "Landing:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "Landing:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab5/client/Landing/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab5/client/Landing/package.json ================================================ { "name": "landing", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "bootstrap": "~5.1.3", "font-awesome": "~4.7.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab5/client/Landing/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; export const routes: Routes = [ { path: '', redirectTo: 'landing', pathMatch: 'full', }, { path: 'landing', component: LandingComponent, }, { path: 'register', component: RegisterComponent, }, { path: '**', redirectTo: 'landing', pathMatch: 'full', }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab5/client/Landing/src/app/app.component.scss ================================================ ================================================ FILE: Lab5/client/Landing/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'landing'; } ================================================ FILE: Lab5/client/Landing/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; @NgModule({ declarations: [AppComponent, LandingComponent, RegisterComponent], imports: [ AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, MatButtonModule, MatCardModule, MatIconModule, MatProgressSpinnerModule, MatSidenavModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, FormsModule, HttpClientModule, ReactiveFormsModule, MatSnackBarModule, MatInputModule, MatFormFieldModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab5/client/Landing/src/app/views/landing/landing.component.html ================================================

Serverless SaaS Reference Architecture

It's so nice it blows your mind.

Sign up now!

Serverless SaaS Reference Architecture is so awesome.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere temporibus omnis illum, officia. Architecto voluptatibus commodi voluptatem perspiciatis eos possimus, eius at molestias quaerat magnam? Odio qui quos ipsam natus.

Serverless SaaS Reference Architecture so awesome. Makes you awesome - go sign up!

Serverless SaaS Reference Architecture so great. Makes you even greater - go sign up now. Super cheap deal!

Feel lonely? Go sign up and have a friend!

Take Serverless SaaS Reference Architecture with you everywhere you go.

Serverless SaaS Reference Architecture is all you need. Anywhere - ever. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita sapiente hic voluptatum quo sunt totam accusamus distinctio minus aliquid quis!

Love Serverless SaaS Reference Architecture. So nice! So good! Could not live without!

Satisfied Customer

Reasons to sign up this product:

  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!
  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!

Why you still reading?

Sign up now!
================================================ FILE: Lab5/client/Landing/src/app/views/landing/landing.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // $color-red: #cc615f; $color-grey: #4b4b4b; $color-grey--light: #d3d3d3; $btn-amzn: #ec7211; $color-primary: #232f3e; $bp-s: 65.75em; //700px; $bp-xs: 34.375em; //550px; @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,800,300); *{ box-sizing: border-box; } html{ width: 100%; height:100%; margin: 0; padding: 0; } body{ width: 100%; height:100%; font-family: 'Open Sans','Helvetica Neue',Helvetica, sans-serif; font-size: 100%; line-height: 1.45; color: #141414; } a{ text-decoration: none; &:hover{ text-decoration: none; } } img{ max-width: 100%; } .btn{ display: inline-block; margin: 1rem 0; color: white; font-weight: 500; font-size: 1.3rem; background: $btn-amzn; letter-spacing: .02em; border: none; border-radius: 5px; padding: .8rem 1rem .9rem; text-shadow: 0 1px rgba(black,.3); box-shadow: 0 0 2px rgba(black,.2); @media (max-width: $bp-s){ padding: .5rem .7rem .6rem; font-size: 1rem; } &:hover{ background: lighten($btn-amzn,5%); color: #fff; } &:focus, &:active, &:focus:active{ background: darken($btn-amzn,5%); border-color: darken($btn-amzn,5%); box-shadow: 0 2px 5px 0 rgba(black,.5) inset; } } .container{ margin: 0 auto; width: 90%; max-width: 900px; } header{ color: white; background: #232f3e; padding: 10rem 0; text-align: center; position: relative; z-index: 1; overflow: hidden; @media (max-width: $bp-s){ padding: 2rem 0; } h1{ font-size: 3rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 2rem; } } h2{ font-weight: 300; font-size: 1.5rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } } section{ background: #fff; color: $color-grey; padding: 3.5rem 0; @media (max-width: $bp-s){ padding: 2rem 0; } &.section--primary{ background: $color-primary; color: #fff; } &.section--primary--alt{ background: desaturate(lighten($color-primary,15%),10%); color: #fff; } &.section--primary--light{ background: rgba($color-primary,.1); } &.section--grey{ background: $color-grey; color: #fff; } &.section--grey--light{ background: $color-grey--light; color: #fff; } h3{ text-align: center; font-size: 2rem; font-weight: 300; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } li{ font-size: 1.2rem; font-weight: 300; } p{ font-size: 1.2rem; font-weight: 300; } } .col{ margin: 0 1.5%; display: inline-block; vertical-align: top; } .col-7{ @extend .col; width: 64%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-3{ @extend .col; width: 29%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-5{ @extend .col; width: 30%; @media (max-width: $bp-xs){ width: 60%; margin: 0; } } .details{ text-align: left; h3{ font-size: 2rem; text-align: left; } } .features{ text-align: center; padding: 1rem; &:hover{ background: rgba(white,.1); } @media (max-width: $bp-s){ width: 100%; margin: 0; text-align: left; border-bottom: 1px solid rgba(white,.2); &:last-child{ border: none; } } i{ font-size: 4rem; margin: 0 0 2rem 0; @media (max-width: $bp-s){ font-size: 1.5rem; width: 2rem; text-align: center; margin: 0 0 1rem 0; float: left; } } p{ margin: 0 0 1rem 0; font-size: 1rem; @media (max-width: $bp-s){ margin-left: 3rem; } } } blockquote{ position: relative; margin: 0; padding: 0; text-align: center; &:before{ display: inline-block; color: $color-primary; font-size: 2rem; content: '\201C'; } p{ margin: 0; display: inline; font-size: 1.5rem; @media (max-width: $bp-s){ font-size: 1.2rem; } } cite{ font-size: 1rem; display: block; margin: .5rem 0 0 1.2rem; @media (max-width: $bp-s){ font-size: .8rem; } &:before{ content: '–'; } } } footer{ background: $color-primary; color: #fff; padding: 2rem 0; text-align: center; font-size: .8rem; color: rgba(white,.4); ul{ margin: 0; padding: 0; list-style: none; li{ display: inline-block; a{ display: block; padding: .4rem .7rem; font-size: .9rem; text-decoration: none; color: rgba(white,.7); &:hover{ color: white; } } } } } .text--center{ text-align: center; } .text--left{ text-align: left; } .bg-image{ background: $color-primary; text-align: center; position: relative; z-index: 1; overflow: hidden; //text-shadow: 2px 0 5px black; &:before{ content: ''; display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; z-index: -1; background-size: 100%; background-attachment: fixed; filter: blur(3px); opacity: .8; transform: scale(1.1); } &.bg-image-2:before{ opacity: .6; background-position: center center; } } ================================================ FILE: Lab5/client/Landing/src/app/views/landing/landing.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-landing', templateUrl: './landing.component.html', styleUrls: ['./landing.component.scss'], }) export class LandingComponent implements OnInit { constructor(private router: Router) {} ngOnInit() {} register() { this.router.navigate(['register']); return false; } } ================================================ FILE: Lab5/client/Landing/src/app/views/register/register.component.html ================================================
Provision a new Tenant Login details will be sent to the email provided. Name Email Phone Address Plan
================================================ FILE: Lab5/client/Landing/src/app/views/register/register.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .tenant-form { display: flex; align-items: center; justify-content: center; } .wide-form { min-width: 150px; max-width: 500px; width: 100%; } .full-width { width: 100%; } .mat-form-field { width: 100%; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab5/client/Landing/src/app/views/register/register.component.ts ================================================ import { MatSnackBar } from '@angular/material/snack-bar'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.scss'], }) export class RegisterComponent implements OnInit { submitting: boolean = false; tenantForm: FormGroup = this.fb.group({ tenantName: ['', [Validators.required]], tenantEmail: ['', [Validators.email, Validators.required]], tenantTier: ['', [Validators.required]], tenantPhone: [''], tenantAddress: [''], }); constructor( private fb: FormBuilder, private _snackBar: MatSnackBar, private http: HttpClient ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; this.tenantForm.disable(); const tenant = { ...this.tenantForm.value, }; this.http .post(`${environment.apiGatewayUrl}/registration`, tenant) .subscribe({ next: () => { this.openErrorMessageSnackBar('Successfully created new tenant!'); this.tenantForm.reset(); this.tenantForm.enable(); this.submitting = false; }, error: (err) => { this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); this.tenantForm.enable(); this.submitting = false; }, }); } } ================================================ FILE: Lab5/client/Landing/src/assets/.gitkeep ================================================ ================================================ FILE: Lab5/client/Landing/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab5/client/Landing/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab5/client/Landing/src/environments/environment.ts ================================================ export const environment = { production: false, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab5/client/Landing/src/index.html ================================================ Landing ================================================ FILE: Lab5/client/Landing/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab5/client/Landing/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab5/client/Landing/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab5/client/Landing/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab5/client/Landing/src/styles.scss ================================================ ================================================ FILE: Lab5/client/Landing/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab5/client/Landing/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab5/client/Landing/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab5/client/Landing/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab5/scripts/deploy-updates.sh ================================================ #!/bin/bash cd ../server || exit # stop execution if cd fails rm -rf .aws-sam/ python3 -m pylint -E -d E0401,E0606 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi #Deploying shared services changes echo "Deploying shared services changes" echo Y | sam sync --stack-name serverless-saas -t shared-template.yaml --code --resource-id LambdaFunctions/CreateTenantAdminUserFunction --resource-id LambdaFunctions/ProvisionTenantFunction -u cd ../scripts || exit ./geturl.sh ================================================ FILE: Lab5/scripts/deployment.sh ================================================ #!/bin/bash if [[ "$#" -eq 0 ]]; then echo "Invalid parameters" echo "Command to deploy client code: deployment.sh -c" echo "Command to deploy bootstrap server code: deployment.sh -b" echo "Command to deploy CI/CD pipeline code: deployment.sh -p" echo "Command to deploy CI/CD pipeline, bootstrap & tenant server code: deployment.sh -s" echo "Command to deploy server & client code: deployment.sh -s -c" exit 1 fi while [[ "$#" -gt 0 ]]; do case $1 in -s) server=1 ;; -b) bootstrap=1 ;; -p) pipeline=1 ;; -c) client=1 ;; *) echo "Unknown parameter passed: $1"; exit 1 ;; esac shift done # During AWS hosted events using event engine tool # we pre-provision cloudfront and s3 buckets which hosts UI code. # So that it improves this labs total execution time. # Below code checks if cloudfront and s3 buckets are # pre-provisioned or not and then concludes if the workshop # is running in AWS hosted event through event engine tool or not. IS_RUNNING_IN_EVENT_ENGINE=false PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" IS_RUNNING_IN_EVENT_ENGINE=true ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_BUCKET=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSiteBucket'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) fi if [[ $server -eq 1 ]] || [[ $pipeline -eq 1 ]]; then echo "CI/CD pipeline code is getting deployed" #Create CodeCommit repo REGION=$(aws configure get region) REPO=$(aws codecommit get-repository --repository-name aws-serverless-saas-workshop) if [[ $? -ne 0 ]]; then echo "aws-serverless-saas-workshop codecommit repo is not present, will create one now" CREATE_REPO=$(aws codecommit create-repository --repository-name aws-serverless-saas-workshop --repository-description "Serverless SaaS workshop repository") echo $CREATE_REPO REPO_URL="codecommit::${REGION}://aws-serverless-saas-workshop" git remote add cc $REPO_URL if [[ $? -ne 0 ]]; then echo "Setting url to remote cc" git remote set-url cc $REPO_URL fi git push --set-upstream cc main fi #Deploying CI/CD pipeline cd ../server/TenantPipeline/ npm install && npm run build cdk bootstrap cdk deploy --require-approval never cd ../../scripts fi if [[ $server -eq 1 ]] || [[ $bootstrap -eq 1 ]]; then echo "Bootstrap server code is getting deployed" cd ../server REGION=$(aws configure get region) echo "Validating server code using pylint" python3 -m pylint -E -d E0401,E0606 $(find . -iname "*.py" -not -path "./.aws-sam/*" -not -path "./TenantPipeline/node_modules/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi sam build -t shared-template.yaml --use-container if [ "$IS_RUNNING_IN_EVENT_ENGINE" = true ]; then sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE AdminUserPoolCallbackURLParameter=$ADMIN_SITE_URL TenantUserPoolCallbackURLParameter=$APP_SITE_URL else sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE fi cd ../scripts fi if [ "$IS_RUNNING_IN_EVENT_ENGINE" = false ]; then ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_BUCKET=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSiteBucket'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi if [[ $client -eq 1 ]]; then echo "Client code is getting deployed" ADMIN_APIGATEWAYURL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminApi'].OutputValue" --output text) # Admin UI and Landing UI are configured in Lab2 echo "Admin UI and Landing UI are configured in Lab2. Only App UI will be reconfigured in this Lab5." # Configuring app UI echo "aws s3 ls s3://$APP_SITE_BUCKET" aws s3 ls s3://$APP_SITE_BUCKET if [ $? -ne 0 ]; then echo "Error! S3 Bucket: $APP_SITE_BUCKET not readable" exit 1 fi cd ../client/Application echo "Configuring environment for App Client" cat << EoF > ./src/environments/environment.prod.ts export const environment = { production: true, regApiGatewayUrl: '$ADMIN_APIGATEWAYURL' }; EoF cat << EoF > ./src/environments/environment.ts export const environment = { production: true, regApiGatewayUrl: '$ADMIN_APIGATEWAYURL' }; EoF npm install --legacy-peer-deps && npm run build echo "aws s3 sync --delete --cache-control no-store dist s3://$APP_SITE_BUCKET" aws s3 sync --delete --cache-control no-store dist s3://$APP_SITE_BUCKET if [[ $? -ne 0 ]]; then exit 1 fi echo "Completed configuring environment for App Client" echo "Successfully completed redeploying Application UI" fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Lab5/scripts/geturl.sh ================================================ PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) else ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Lab5/server/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Lab5/server/OrderService/order_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Order: key='' def __init__(self, shardId, orderId, orderName, orderProducts): self.shardId = shardId self.orderId = orderId self.key = shardId + ':' + orderId self.orderName = orderName self.orderProducts = orderProducts class OrderProduct: def __init__(self, productId, price, quantity): self.productId = productId self.price = price self.quantity = quantity ================================================ FILE: Lab5/server/OrderService/order_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import order_service_dal from decimal import Decimal from types import SimpleNamespace from aws_lambda_powertools import Tracer tracer = Tracer() @tracer.capture_lambda_handler def get_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a order") params = event['pathParameters'] key = params['id'] logger.log_with_tenant_context(event, params) order = order_service_dal.get_order(event, key) logger.log_with_tenant_context(event, "Request completed to get a order") metrics_manager.record_metric(event, "SingleOrderRequested", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def create_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) order = order_service_dal.create_order(event, payload) logger.log_with_tenant_context(event, "Request completed to create a order") metrics_manager.record_metric(event, "OrderCreated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def update_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] order = order_service_dal.update_order(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a order") metrics_manager.record_metric(event, "OrderUpdated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def delete_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a order") params = event['pathParameters'] key = params['id'] response = order_service_dal.delete_order(event, key) logger.log_with_tenant_context(event, "Request completed to delete a order") metrics_manager.record_metric(event, "OrderDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the order") @tracer.capture_lambda_handler def get_orders(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all orders") response = order_service_dal.get_orders(event, tenantId) metrics_manager.record_metric(event, "OrdersRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all orders") return utils.generate_response(response) ================================================ FILE: Lab5/server/OrderService/order_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid from order_models import Order import json import utils import metrics_manager from types import SimpleNamespace import logger import random import threading from boto3.dynamodb.conditions import Key is_pooled_deploy = os.environ['IS_POOLED_DEPLOY'] table_name = os.environ['ORDER_TABLE_NAME'] dynamodb = None suffix_start = 1 suffix_end = 10 def get_order(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) response = table.get_item(Key={'shardId': shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a order', e) else: return order def delete_order(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a order', e) else: logger.info("DeleteItem succeeded:") return response def create_order(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] table = __get_dynamodb_table(event, dynamodb) suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) order = Order(shardId, str(uuid.uuid4()), payload.orderName, payload.orderProducts) try: response = table.put_item(Item={ 'shardId':shardId, 'orderId': order.orderId, 'orderName': order.orderName, 'orderProducts': get_order_products_dict(order.orderProducts) }, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a order', e) else: logger.info("PutItem succeeded:") return order def update_order(event, payload, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) order = Order(shardId, orderId,payload.orderName, payload.orderProducts) response = table.update_item(Key={'shardId':order.shardId, 'orderId': order.orderId}, UpdateExpression="set orderName=:orderName, " +"orderProducts=:orderProducts", ExpressionAttributeValues={ ':orderName': order.orderName, ':orderProducts': get_order_products_dict(order.orderProducts) }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a order', e) else: logger.info("UpdateItem succeeded:") return order def get_orders(event, tenantId): table = __get_dynamodb_table(event, dynamodb) get_all_products_response = [] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error("Error getting all orders") raise Exception('Error getting all orders', e) else: logger.info("Get orders succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) get_all_products_response.append(order) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def __get_dynamodb_table(event, dynamodb): """ Determine the table name based upo pooled vs silo model Args: event ([type]): [description] Returns: [type]: [description] """ if (is_pooled_deploy=='true'): accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken ) else: if not dynamodb: dynamodb = boto3.resource('dynamodb') return dynamodb.Table(table_name) def get_order_products_dict(orderProducts): orderProductList = [] for i in range(len(orderProducts)): product = orderProducts[i] orderProductList.append(vars(product)) return orderProductList ================================================ FILE: Lab5/server/OrderService/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Lab5/server/ProductService/product_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Product: key ='' def __init__(self, shardId, productId, sku, name, price, category): self.shardId = shardId self.productId = productId self.key = shardId + ':' + productId self.sku = sku self.name = name self.price = price self.category = category class Category: def __init__(self, id, name): self.id = id self.name = name ================================================ FILE: Lab5/server/ProductService/product_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import product_service_dal from aws_lambda_powertools import Tracer from decimal import Decimal from types import SimpleNamespace tracer = Tracer() @tracer.capture_lambda_handler def get_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a product") params = event['pathParameters'] logger.log_with_tenant_context(event, params) key = params['id'] logger.log_with_tenant_context(event, key) product = product_service_dal.get_product(event, key) logger.log_with_tenant_context(event, "Request completed to get a product") metrics_manager.record_metric(event, "SingleProductRequested", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def create_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) product = product_service_dal.create_product(event, payload) logger.log_with_tenant_context(event, "Request completed to create a product") metrics_manager.record_metric(event, "ProductCreated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def update_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] product = product_service_dal.update_product(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a product") metrics_manager.record_metric(event, "ProductUpdated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def delete_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a product") params = event['pathParameters'] key = params['id'] response = product_service_dal.delete_product(event, key) logger.log_with_tenant_context(event, "Request completed to delete a product") metrics_manager.record_metric(event, "ProductDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the product") @tracer.capture_lambda_handler def get_products(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all products") response = product_service_dal.get_products(event, tenantId) metrics_manager.record_metric(event, "ProductsRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all products") return utils.generate_response(response) ================================================ FILE: Lab5/server/ProductService/product_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid import json import logger import metrics_manager import random import threading from product_models import Product from types import SimpleNamespace from boto3.dynamodb.conditions import Key is_pooled_deploy = os.environ['IS_POOLED_DEPLOY'] table_name = os.environ['PRODUCT_TABLE_NAME'] dynamodb = None suffix_start = 1 suffix_end = 10 def get_product(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) response = table.get_item(Key={'shardId': shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a product', e) else: logger.info("GetItem succeeded:"+ str(product)) return product def delete_product(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a product', e) else: logger.info("DeleteItem succeeded:") return response def create_product(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] table = __get_dynamodb_table(event, dynamodb) suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) product = Product(shardId, str(uuid.uuid4()), payload.sku,payload.name, payload.price, payload.category) try: response = table.put_item( Item= { 'shardId': shardId, 'productId': product.productId, 'sku': product.sku, 'name': product.name, 'price': product.price, 'category': product.category }, ReturnConsumedCapacity='TOTAL' ) metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: logger.info("PutItem succeeded:") return product def update_product(event, payload, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) product = Product(shardId,productId,payload.sku, payload.name, payload.price, payload.category) response = table.update_item(Key={'shardId':product.shardId, 'productId': product.productId}, UpdateExpression="set sku=:sku, #n=:productName, price=:price, category=:category", ExpressionAttributeNames= {'#n':'name'}, ExpressionAttributeValues={ ':sku': product.sku, ':productName': product.name, ':price': product.price, ':category': product.category }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a product', e) else: logger.info("UpdateItem succeeded:") return product def get_products(event, tenantId): table = __get_dynamodb_table(event, dynamodb) get_all_products_response =[] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting all products', e) else: logger.info("Get products succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) get_all_products_response.append(product) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def __get_dynamodb_table(event, dynamodb): if (is_pooled_deploy=='true'): accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken ) else: if not dynamodb: dynamodb = boto3.resource('dynamodb') return dynamodb.Table(table_name) ================================================ FILE: Lab5/server/ProductService/requirements.txt ================================================ requests pytest-mock aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Lab5/server/README.md ================================================ sam build -t shared-template.yaml --use-container sam deploy --config-file shared-samconfig.toml sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml ================================================ FILE: Lab5/server/Resources/requirements.txt ================================================ python-jose[cryptography] ================================================ FILE: Lab5/server/Resources/shared_service_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import auth_manager import utils region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) if(auth_manager.isSaaSProvider(unauthorized_claims['custom:userRole'])): userpool_id = user_pool_operation_user appclient_id = app_client_operation_user else: #get tenant user pool and app client to validate jwt token against tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': unauthorized_claims['custom:tenantId'] } ) logger.info(tenant_details) userpool_id = tenant_details['Item']['userPoolId'] appclient_id = tenant_details['Item']['appClientId'] #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] #only tenant admin and system admin can do certain actions like create and disable users if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): policy.allowAllMethods() if (auth_manager.isTenantAdmin(user_role)): policy.denyMethod(HttpVerb.POST, "tenant-activation") policy.denyMethod(HttpVerb.GET, "tenants") else: #if not tenant admin or system admin then only allow to get info and update info policy.allowMethod(HttpVerb.GET, "user/*") policy.allowMethod(HttpVerb.PUT, "user/*") authResponse = policy.build() # Generate STS credentials to be used for FGAC # Important Note: # We are generating STS token inside Authorizer to take advantage of the caching behavior of authorizer # Another option is to generate the STS token inside the lambda function itself, as mentioned in this blog post: https://aws.amazon.com/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/ # Finally, you can also consider creating one Authorizer per microservice in cases where you want the IAM policy specific to that service iam_policy = auth_manager.getPolicyForUser(user_role, utils.Service_Identifier.SHARED_SERVICES.value, tenant_id, region, aws_account_id) logger.info(iam_policy) role_arn = "arn:aws:iam::{}:role/authorizer-access-role".format(aws_account_id) assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName="tenant-aware-session", Policy=iam_policy, ) credentials = assumed_role["Credentials"] #pass sts credentials to lambda context = { 'accesskey': credentials['AccessKeyId'], # $context.authorizer.key -> value 'secretkey' : credentials['SecretAccessKey'], 'sessiontoken' : credentials["SessionToken"], 'userName': user_name, 'tenantId': tenant_id, 'userPoolId': userpool_id, 'userRole': user_role } authResponse['context'] = context return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Lab5/server/Resources/tenant_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import auth_manager import utils region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) if(auth_manager.isSaaSProvider(unauthorized_claims['custom:userRole'])): userpool_id = user_pool_operation_user appclient_id = app_client_operation_user else: #get tenant user pool and app client to validate jwt token against tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': unauthorized_claims['custom:tenantId'] } ) logger.info(tenant_details) userpool_id = tenant_details['Item']['userPoolId'] appclient_id = tenant_details['Item']['appClientId'] apigateway_url = tenant_details['Item']['apiGatewayUrl'] #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] if (auth_manager.isSaaSProvider(user_role) == False): if (isTenantAuthorizedForThisAPI(apigateway_url, api_gateway_arn_tmp[0]) == False): logger.error('Unauthorized') raise Exception('Unauthorized') #roles are not fine-grained enough to allow selectively policy.allowAllMethods() authResponse = policy.build() # Generate STS credentials to be used for FGAC # Important Note: # We are generating STS token inside Authorizer to take advantage of the caching behavior of authorizer # Another option is to generate the STS token inside the lambda function itself, as mentioned in this blog post: https://aws.amazon.com/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/ # Finally, you can also consider creating one Authorizer per microservice in cases where you want the IAM policy specific to that service iam_policy = auth_manager.getPolicyForUser(user_role, utils.Service_Identifier.BUSINESS_SERVICES.value, tenant_id, region, aws_account_id) logger.info(iam_policy) role_arn = "arn:aws:iam::{}:role/authorizer-access-role".format(aws_account_id) assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName="tenant-aware-session", Policy=iam_policy, ) credentials = assumed_role["Credentials"] #pass sts credentials to lambda context = { 'accesskey': credentials['AccessKeyId'], # $context.authorizer.key -> value 'secretkey' : credentials['SecretAccessKey'], 'sessiontoken' : credentials["SessionToken"], 'userName': user_name, 'tenantId': tenant_id, 'userPoolId': userpool_id, 'userRole': user_role } authResponse['context'] = context return authResponse def isTenantAuthorizedForThisAPI(apigateway_url, current_api_id): if(apigateway_url.split('.')[0] != 'https://' + current_api_id): return False else: return True def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Lab5/server/TenantManagementService/requirements.txt ================================================ requests aws_requests_auth ================================================ FILE: Lab5/server/TenantManagementService/tenant-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import json import boto3 from boto3.dynamodb.conditions import Key import urllib.parse import utils from botocore.exceptions import ClientError import logger import metrics_manager import auth_manager import requests from aws_requests_auth.aws_auth import AWSRequestsAuth from aws_lambda_powertools import Tracer tracer = Tracer() region = os.environ['AWS_REGION'] #This method has been locked down to be only def create_tenant(event, context): api_gateway_url = '' tenant_details = json.loads(event['body']) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails')#TODO: read table names from env vars table_system_settings = dynamodb.Table('ServerlessSaaS-Settings') try: # for pooled tenants the apigateway url is saving in settings during stack creation # update from there during tenant creation if(tenant_details['dedicatedTenancy'].lower()!= 'true'): settings_response = table_system_settings.get_item( Key={ 'settingName': 'apiGatewayUrl-Pooled' } ) api_gateway_url = settings_response['Item']['settingValue'] response = table_tenant_details.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'tenantName' : tenant_details['tenantName'], 'tenantAddress': tenant_details['tenantAddress'], 'tenantEmail': tenant_details['tenantEmail'], 'tenantPhone': tenant_details['tenantPhone'], 'tenantTier': tenant_details['tenantTier'], 'userPoolId': tenant_details['userPoolId'], 'appClientId': tenant_details['appClientId'], 'dedicatedTenancy': tenant_details['dedicatedTenancy'], 'isActive': True, 'apiGatewayUrl': api_gateway_url } ) except Exception as e: raise Exception('Error creating a new tenant', e) else: return utils.create_success_response("Tenant Created") def get_tenants(event, context): table_tenant_details = __getTenantManagementTable(event) try: response = table_tenant_details.scan() except Exception as e: raise Exception('Error getting all tenants', e) else: return utils.generate_response(response['Items']) @tracer.capture_lambda_handler def update_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_details = json.loads(event['body']) tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response_update = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set tenantName = :tenantName, tenantAddress = :tenantAddress, tenantEmail = :tenantEmail, tenantPhone = :tenantPhone, tenantTier=:tenantTier", ExpressionAttributeValues={ ':tenantName' : tenant_details['tenantName'], ':tenantAddress': tenant_details['tenantAddress'], ':tenantEmail': tenant_details['tenantEmail'], ':tenantPhone': tenant_details['tenantPhone'], ':tenantTier': tenant_details['tenantTier'] }, ReturnValues="UPDATED_NEW" ) logger.log_with_tenant_context(event, response_update) logger.log_with_tenant_context(event, "Request completed to update tenant") return utils.create_success_response("Tenant Updated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get tenant details") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): tenant_details = table_tenant_details.get_item( Key={ 'tenantId': tenant_id, }, AttributesToGet=[ 'tenantName', 'tenantAddress', 'tenantEmail', 'tenantPhone' ] ) item = tenant_details['Item'] tenant_info = TenantInfo(item['tenantName'], item['tenantAddress'],item['tenantEmail'], item['tenantPhone']) logger.log_with_tenant_context(event, tenant_info) logger.log_with_tenant_context(event, "Request completed to get tenant details") return utils.create_success_response(tenant_info.__dict__) else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def deactivate_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) url_disable_users = os.environ['DISABLE_USERS_BY_TENANT'] url_deprovision_tenant = os.environ['DEPROVISION_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to deactivate tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': False }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) if (response["Attributes"]["dedicatedTenancy"].upper() == "TRUE"): update_details = {} update_details['tenantId'] = tenant_id update_user_response = __invoke_deprovision_tenant(update_details, headers, auth, host, stage_name, url_deprovision_tenant) update_details = {} update_details['userPoolId'] = response["Attributes"]['userPoolId'] update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_disable_users(update_details, headers, auth, host, stage_name, url_disable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to deactivate tenant") return utils.create_success_response("Tenant Deactivated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def activate_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) url_enable_users = os.environ['ENABLE_USERS_BY_TENANT'] url_provision_tenant = os.environ['PROVISION_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to activate tenant") if (auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': True }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) if (response["Attributes"]["dedicatedTenancy"].upper() == "TRUE"): update_details = {} update_details['tenantId'] = tenant_id provision_response = __invoke_provision_tenant(update_details, headers, auth, host, stage_name, url_provision_tenant) logger.log_with_tenant_context(event, provision_response) update_details = {} update_details['userPoolId'] = response["Attributes"]['userPoolId'] update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_enable_users(update_details, headers, auth, host, stage_name, url_enable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to activate tenant") return utils.create_success_response("Tenant Activated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only system admin can activate tenant!") return utils.create_unauthorized_response() def load_tenant_config(event, context): params = event['pathParameters'] tenantName = urllib.parse.unquote(params['tenantname']) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails')#TODO: read table names from env vars try: response = table_tenant_details.query( IndexName="ServerlessSaas-TenantConfig", KeyConditionExpression=Key('tenantName').eq(tenantName), ProjectionExpression="userPoolId, appClientId, apiGatewayUrl" ) except Exception as e: raise Exception('Error getting tenant config', e) else: if (response['Count'] == 0): return utils.create_notfound_response("Tenant not found."+ "Please enter exact tenant name used during tenant registration.") else: return utils.generate_response(response['Items'][0]) def __invoke_disable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url]) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while disabling users for the tenant') except Exception as e: logger.error('Error occured while disabling users for the tenant') raise Exception('Error occured while disabling users for the tenant', e) else: return "Success invoking disable users" def __invoke_deprovision_tenant(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url + update_details['tenantId']]) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while deprovisioning tenant') except Exception as e: logger.error('Error occured while deprovisioning tenant') raise Exception('Error occured while deprovisioning tenant', e) else: return "Success invoking deprovision tenant" def __invoke_enable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url]) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while enabling users for the tenant') except Exception as e: logger.error('Error occured while enabling users for the tenant') raise Exception('Error occured while enabling users for the tenant', e) else: return "Success invoking enable users" def __invoke_provision_tenant(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url]) response = requests.post(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while provisioning tenant') except Exception as e: logger.error('Error occured while provisioning tenant') raise Exception('Error occured while provisioning tenant', e) else: return "Success invoking provision tenant" def __getTenantManagementTable(event): accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken) table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails')#TODO: read table names from env vars return table_tenant_details class TenantInfo: def __init__(self, tenant_name, tenant_address, tenant_email, tenant_phone): self.tenant_name = tenant_name self.tenant_address = tenant_address self.tenant_email = tenant_email self.tenant_phone = tenant_phone ================================================ FILE: Lab5/server/TenantManagementService/tenant-provisioning.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import utils from botocore.exceptions import ClientError import logger import os from aws_lambda_powertools import Tracer tracer = Tracer() tenant_stack_mapping_table_name = os.environ['TENANT_STACK_MAPPING_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') codepipeline = boto3.client('codepipeline') cloudformation = boto3.client('cloudformation') table_tenant_stack_mapping = dynamodb.Table(tenant_stack_mapping_table_name) stack_name = 'stack-{0}' @tracer.capture_lambda_handler def provision_tenant(event, context): tenant_details = json.loads(event['body']) try: #TODO: Add missing code to kick off the pipeline pass except Exception as e: raise else: return utils.create_success_response("Tenant Provisioning Started") @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def deprovision_tenant(event, context): logger.info("Request received to deprovision a tenant") tenantid_to_deprovision = event['tenantId'] try: response_ddb = table_tenant_stack_mapping.delete_item( Key={ 'tenantId': tenantid_to_deprovision } ) logger.info(response_ddb) response_cloudformation = cloudformation.delete_stack( StackName=stack_name.format(tenantid_to_deprovision) ) logger.info(response_cloudformation) except Exception as e: raise else: return utils.create_success_response("Tenant Deprovisioning Started") ================================================ FILE: Lab5/server/TenantManagementService/tenant-registration.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import utils import uuid import logger import requests import re region = os.environ['AWS_REGION'] create_tenant_admin_user_resource_path = os.environ['CREATE_TENANT_ADMIN_USER_RESOURCE_PATH'] create_tenant_resource_path = os.environ['CREATE_TENANT_RESOURCE_PATH'] provision_tenant_resource_path = os.environ['PROVISION_TENANT_RESOURCE_PATH'] lambda_client = boto3.client('lambda') def register_tenant(event, context): try: tenant_id = uuid.uuid1().hex tenant_details = json.loads(event['body']) tenant_details['dedicatedTenancy'] = 'false' if (tenant_details['tenantTier'].upper() == utils.TenantTier.PLATINUM.value.upper()): tenant_details['dedicatedTenancy'] = 'true' tenant_details['tenantId'] = tenant_id logger.info(tenant_details) stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) create_user_response = __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name) logger.info (create_user_response) tenant_details['userPoolId'] = create_user_response['message']['userPoolId'] tenant_details['appClientId'] = create_user_response['message']['appClientId'] tenant_details['tenantAdminUserName'] = create_user_response['message']['tenantAdminUserName'] create_tenant_response = __create_tenant(tenant_details, headers, auth, host, stage_name) logger.info (create_tenant_response) if (tenant_details['dedicatedTenancy'].upper() == 'TRUE'): provision_tenant_response = __provision_tenant(tenant_details, headers, auth, host, stage_name) logger.info(provision_tenant_response) except Exception as e: logger.error('Error registering a new tenant') raise Exception('Error registering a new tenant', e) else: return utils.create_success_response("You have been registered in our system") def __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_admin_user_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while calling the create tenant admin user service') raise Exception('Error occured while calling the create tenant admin user service', e) else: return response_json def __create_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_resource_path]) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while creating the tenant record in table') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json def __provision_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, provision_tenant_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json()['message'] except Exception as e: logger.error('Error occured while provisioning the tenant') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json ================================================ FILE: Lab5/server/TenantManagementService/user-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import sys import logger import utils import metrics_manager import auth_manager from boto3.dynamodb.conditions import Key from aws_lambda_powertools import Tracer tracer = Tracer() client = boto3.client('cognito-idp') dynamodb = boto3.resource('dynamodb') table_tenant_user_map = dynamodb.Table('ServerlessSaaS-TenantUserMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') def create_tenant_admin_user(event, context): tenant_user_pool_id = os.environ['TENANT_USER_POOL_ID'] tenant_app_client_id = os.environ['TENANT_APP_CLIENT_ID'] tenant_details = json.loads(event['body']) tenant_id = tenant_details['tenantId'] logger.info(tenant_details) user_mgmt = UserManagement() if (tenant_details['dedicatedTenancy'] == 'true'): #TODO: add code to provision new user pool pass else: user_pool_id = tenant_user_pool_id app_client_id = tenant_app_client_id #Add tenant admin now based upon user pool tenant_user_group_response = user_mgmt.create_user_group(user_pool_id,tenant_id,"User group for tenant {0}".format(tenant_id)) tenant_admin_user_name = 'tenant-admin-{0}'.format(tenant_details['tenantId']) create_tenant_admin_response = user_mgmt.create_tenant_admin(user_pool_id, tenant_admin_user_name, tenant_details) add_tenant_admin_to_group_response = user_mgmt.add_user_to_group(user_pool_id, tenant_admin_user_name, tenant_user_group_response['Group']['GroupName']) tenant_user_mapping_response = user_mgmt.create_user_tenant_mapping(tenant_admin_user_name,tenant_id) response = {"userPoolId": user_pool_id, "appClientId": app_client_id, "tenantAdminUserName": tenant_admin_user_name} return utils.create_success_response(response) @tracer.capture_lambda_handler #only tenant admin can create users def create_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to create new user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = user_details['tenantId'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] else: user_tenant_id = tenant_id if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): metrics_manager.record_metric(event, "UserCreated", "Count", 1) response = client.admin_create_user( Username=user_details['userName'], UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] }, { 'Name': 'custom:tenantId', 'Value': user_tenant_id } ] ) logger.log_with_tenant_context(event, response) user_mgmt = UserManagement() user_mgmt.add_user_to_group(user_pool_id, user_details['userName'], user_tenant_id) response_mapping = user_mgmt.create_user_tenant_mapping(user_details['userName'], user_tenant_id) logger.log_with_tenant_context(event, "Request completed to create new user") return utils.create_success_response("New user created") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can create user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_users(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] users = [] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get users") if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): response = client.list_users( UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) num_of_users = len(response['Users']) metrics_manager.record_metric(event, "Number of users", "Count", num_of_users) if (num_of_users > 0): for user in response['Users']: is_same_tenant_user = False user_info = UserInfo() for attr in user["Attributes"]: if(attr["Name"] == "custom:tenantId" and attr["Value"] == tenant_id): is_same_tenant_user = True user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] if(is_same_tenant_user): user_info.enabled = user["Enabled"] user_info.created = user["UserCreateDate"] user_info.modified = user["UserLastModifiedDate"] user_info.status = user["UserStatus"] user_info.user_name = user["Username"] users.append(user_info) return utils.generate_response(users) else: logger.log_with_tenant_context(event, "Request completed as unauthorized.") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = event['queryStringParameters']['tenantid'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] if (auth_manager.isTenantUser(user_role) and user_name != requesting_user_name): logger.log_with_tenant_context(event, "Request completed as unauthorized. User can only get its information.") return utils.create_unauthorized_response() else: user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: logger.log_with_tenant_context(event, "Request completed to get new user") return utils.create_success_response(user_info.__dict__) @tracer.capture_lambda_handler def update_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = user_details['tenantId'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] if (auth_manager.isTenantUser(user_role)): logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update user!") return utils.create_unauthorized_response() else: user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserUpdated", "Count", 1) response = client.admin_update_user_attributes( Username=user_name, UserPoolId=user_pool_id, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] } ] ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to update user") return utils.create_success_response("user updated") @tracer.capture_lambda_handler def disable_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to disable new user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = event['queryStringParameters']['tenantid'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserDisabled", "Count", 1) response = client.admin_disable_user( Username=user_name, UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to disable new user") return utils.create_success_response("User disabled") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can disable user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def disable_users_by_tenant(event, context): logger.info("Request received to disable users by tenant") tenantid_to_update = event['tenantId'] tenant_user_pool_id = event['userPoolId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if ((auth_manager.isTenantAdmin(user_role) and tenantid_to_update == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_disable_user( Username=user['userName'], UserPoolId=tenant_user_pool_id ) logger.info(response) logger.info("Request completed to disable users") return utils.create_success_response("Users disabled") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def enable_users_by_tenant(event, context): logger.info("Request received to enable users by tenant") tenantid_to_update = event['tenantId'] tenant_user_pool_id = event['userPoolId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if (auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_enable_user( Username=user['userName'], UserPoolId=tenant_user_pool_id ) logger.info(response) logger.info("Request completed to enable users") return utils.create_success_response("Users enables") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() def get_user_info(event, user_pool_id, user_name): metrics_manager.record_metric(event, "UserInfoRequested", "Count", 1) response = client.admin_get_user( UserPoolId=user_pool_id, Username=user_name ) logger.log_with_tenant_context(event, response) user_info = UserInfo() user_info.user_name = response["Username"] for attr in response["UserAttributes"]: if(attr["Name"] == "custom:tenantId"): user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] logger.log_with_tenant_context(event, user_info) return user_info class UserManagement: def create_user_pool(self, tenant_id): application_site_url = os.environ['TENANT_USER_POOL_CALLBACK_URL'] email_message = ''.join(["Login into tenant UI application at ", application_site_url, " with username {username} and temporary password {####}"]) email_subject = "Your temporary password for tenant UI application" response = client.create_user_pool( PoolName= tenant_id + '-ServerlessSaaSUserPool', AutoVerifiedAttributes=['email'], AccountRecoverySetting={ 'RecoveryMechanisms': [ { 'Priority': 1, 'Name': 'verified_email' }, ] }, Schema=[ { 'Name': 'email', 'AttributeDataType': 'String', 'Required': True, }, { 'Name': 'tenantId', 'AttributeDataType': 'String', 'Required': False, }, { 'Name': 'userRole', 'AttributeDataType': 'String', 'Required': False, } ], AdminCreateUserConfig={ 'InviteMessageTemplate': { 'EmailMessage': email_message, 'EmailSubject': email_subject } } ) return response def create_user_pool_client(self, user_pool_id): user_pool_callback_url = os.environ['TENANT_USER_POOL_CALLBACK_URL'] response = client.create_user_pool_client( UserPoolId= user_pool_id, ClientName= 'ServerlessSaaSClient', GenerateSecret= False, AllowedOAuthFlowsUserPoolClient= True, AllowedOAuthFlows=[ 'code', 'implicit' ], SupportedIdentityProviders=[ 'COGNITO', ], CallbackURLs=[ user_pool_callback_url, ], LogoutURLs= [ user_pool_callback_url, ], AllowedOAuthScopes=[ 'email', 'openid', 'profile' ], WriteAttributes=[ 'email', 'custom:tenantId' ] ) return response def create_user_pool_domain(self, user_pool_id, tenant_id): response = client.create_user_pool_domain( Domain= tenant_id + '-serverlesssaas', UserPoolId=user_pool_id ) return response def create_user_group(self, user_pool_id, group_name, group_description): response = client.create_group( GroupName=group_name, UserPoolId=user_pool_id, Description= group_description, Precedence=0 ) return response def create_tenant_admin(self, user_pool_id, tenant_admin_user_name, user_details): response = client.admin_create_user( Username=tenant_admin_user_name, UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['tenantEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': 'TenantAdmin' }, { 'Name': 'custom:tenantId', 'Value': user_details['tenantId'] } ] ) return response def add_user_to_group(self, user_pool_id, user_name, group_name): response = client.admin_add_user_to_group( UserPoolId=user_pool_id, Username=user_name, GroupName=group_name ) return response def create_user_tenant_mapping(self, user_name, tenant_id): response = table_tenant_user_map.put_item( Item={ 'tenantId': tenant_id, 'userName': user_name } ) return response class UserInfo: def __init__(self, user_name=None, tenant_id=None, user_role=None, email=None, status=None, enabled=None, created=None, modified=None): self.user_name = user_name self.tenant_id = tenant_id self.user_role = user_role self.email = email self.status = status self.enabled = enabled self.created = created self.modified = modified ================================================ FILE: Lab5/server/TenantPipeline/.gitignore ================================================ *.js !jest.config.js *.d.ts node_modules # CDK asset staging directory .cdk.staging cdk.out # Parcel default cache directory .parcel-cache ================================================ FILE: Lab5/server/TenantPipeline/.npmignore ================================================ *.ts !*.d.ts # CDK asset staging directory .cdk.staging cdk.out ================================================ FILE: Lab5/server/TenantPipeline/README.md ================================================ # Welcome to your CDK TypeScript project! This is a blank project for TypeScript development with CDK. The `cdk.json` file tells the CDK Toolkit how to execute your app. ## Useful commands * `npm run build` compile typescript to js * `npm run watch` watch for changes and compile * `npm run test` perform the jest unit tests * `cdk deploy` deploy this stack to your default AWS account/region * `cdk diff` compare deployed stack with current state * `cdk synth` emits the synthesized CloudFormation template ================================================ FILE: Lab5/server/TenantPipeline/bin/pipeline.ts ================================================ #!/usr/bin/env node import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; import { ServerlessSaaSStack } from '../lib/serverless-saas-stack'; const app = new cdk.App(); new ServerlessSaaSStack(app, 'serverless-saas-pipeline'); ================================================ FILE: Lab5/server/TenantPipeline/cdk.json ================================================ { "app": "npx ts-node bin/pipeline.ts", "context": {} } ================================================ FILE: Lab5/server/TenantPipeline/jest.config.js ================================================ module.exports = { roots: ['/test'], testMatch: ['**/*.test.ts'], transform: { '^.+\\.tsx?$': 'ts-jest' } }; ================================================ FILE: Lab5/server/TenantPipeline/lib/serverless-saas-stack.ts ================================================ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 import { Construct } from 'constructs'; import * as cdk from 'aws-cdk-lib'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as codecommit from 'aws-cdk-lib/aws-codecommit'; import * as codepipeline from 'aws-cdk-lib/aws-codepipeline'; import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions'; import * as codebuild from 'aws-cdk-lib/aws-codebuild'; import { Function, Runtime, AssetCode } from 'aws-cdk-lib/aws-lambda'; import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Duration } from 'aws-cdk-lib'; export class ServerlessSaaSStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const artifactsBucket = new s3.Bucket(this, "ArtifactsBucket", { encryption: s3.BucketEncryption.S3_MANAGED, }); //Since this lambda is invoking cloudformation which is inturn deploying AWS resources, we are giving overly permissive permissions to this lambda. //You can limit this based upon your use case and AWS Resources you need to deploy. const lambdaPolicy = new PolicyStatement() lambdaPolicy.addActions("*") lambdaPolicy.addResources("*") const lambdaFunction = new Function(this, "deploy-tenant-stack", { handler: "lambda-deploy-tenant-stack.lambda_handler", runtime: Runtime.PYTHON_3_9, code: new AssetCode(`./resources`), memorySize: 512, timeout: Duration.seconds(10), environment: { BUCKET: artifactsBucket.bucketName, }, initialPolicy: [lambdaPolicy], }) // Pipeline creation starts const pipeline = new codepipeline.Pipeline(this, 'Pipeline', { pipelineName: 'serverless-saas-pipeline', artifactBucket: artifactsBucket }); // Import existing CodeCommit sam-app repository const codeRepo = codecommit.Repository.fromRepositoryName( this, 'AppRepository', 'aws-serverless-saas-workshop' ); // Declare source code as an artifact const sourceOutput = new codepipeline.Artifact(); // Add source stage to pipeline pipeline.addStage({ stageName: 'Source', actions: [ new codepipeline_actions.CodeCommitSourceAction({ actionName: 'CodeCommit_Source', repository: codeRepo, branch: 'main', output: sourceOutput, variablesNamespace: 'SourceVariables' }), ], }); // Declare build output as artifacts const buildOutput = new codepipeline.Artifact(); //Declare a new CodeBuild project const buildProject = new codebuild.PipelineProject(this, 'Build', { buildSpec : codebuild.BuildSpec.fromSourceFilename("Lab5/server/tenant-buildspec.yml"), environment: { buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_4 }, environmentVariables: { 'PACKAGE_BUCKET': { value: artifactsBucket.bucketName } } }); // Add the build stage to our pipeline pipeline.addStage({ stageName: 'Build', actions: [ new codepipeline_actions.CodeBuildAction({ actionName: 'Build-Serverless-SaaS', project: buildProject, input: sourceOutput, outputs: [buildOutput], }), ], }); const deployOutput = new codepipeline.Artifact(); //Add the Lambda function that will deploy the tenant stack in a multitenant way pipeline.addStage({ stageName: 'Deploy', actions: [ new codepipeline_actions.LambdaInvokeAction({ actionName: 'DeployTenantStack', lambda: lambdaFunction, inputs: [buildOutput], outputs: [deployOutput], userParameters: { 'artifact': 'Artifact_Build_Build-Serverless-SaaS', 'template_file': 'packaged.yaml', 'commit_id': '#{SourceVariables.CommitId}' } }), ], }); } } ================================================ FILE: Lab5/server/TenantPipeline/package.json ================================================ { "name": "pipeline", "version": "0.1.0", "bin": { "pipeline": "bin/pipeline.js" }, "scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "cdk": "cdk" }, "devDependencies": { "@aws-cdk/assert": "1.64.1", "@types/jest": "^26.0.10", "@types/node": "10.17.27", "aws-cdk-lib": "^2.0.0", "constructs": "^10.0.0", "jest": "^26.4.2", "node-notifier": "^8.0.1", "ts-jest": "^26.2.0", "ts-node": "^8.1.0", "typescript": "4.9.5", "@types/prettier": "2.6.0" }, "dependencies": { "aws-cdk-lib": "^2.0.0", "constructs": "^10.0.0", "source-map-support": "^0.5.19" } } ================================================ FILE: Lab5/server/TenantPipeline/resources/lambda-deploy-tenant-stack.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from boto3.session import Session import json import boto3 import zipfile import tempfile import botocore import traceback import time print('Loading function') cf = boto3.client('cloudformation') code_pipeline = boto3.client('codepipeline') dynamodb = boto3.resource('dynamodb') table_tenant_stack_mapping = dynamodb.Table('ServerlessSaaS-TenantStackMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') table_tenant_settings = dynamodb.Table('ServerlessSaaS-Settings') def find_artifact(artifacts, name): """Finds the artifact 'name' among the 'artifacts' Args: artifacts: The list of artifacts available to the function name: The artifact we wish to use Returns: The artifact dictionary found Raises: Exception: If no matching artifact is found """ for artifact in artifacts: if artifact['name'] == name: return artifact raise Exception('Input artifact named "{0}" not found in event'.format(name)) def get_template_url(s3, artifact, file_in_zip): """Gets the template artifact Downloads the artifact from the S3 artifact store to a temporary file then extracts the zip and returns the file containing the CloudFormation template. Args: artifact: The artifact to download file_in_zip: The path to the file within the zip containing the template Returns: The CloudFormation template as a string Raises: Exception: Any exception thrown while downloading the artifact or unzipping it """ tmp_file = tempfile.NamedTemporaryFile() bucket = artifact['location']['s3Location']['bucketName'] print(bucket) key = artifact['location']['s3Location']['objectKey'] print(key) with tempfile.NamedTemporaryFile() as tmp_file: s3.download_file(bucket, key, tmp_file.name) with zipfile.ZipFile(tmp_file.name, 'r') as zip: extracted_file = zip.extract(file_in_zip, '/tmp/') s3.upload_file(extracted_file, bucket, file_in_zip) template_url =''.join(['https://', bucket,'.s3.amazonaws.com/',file_in_zip]) return template_url def update_stack(stack, template_url, params): """Start a CloudFormation stack update Args: stack: The stack to update template_url: The template to apply Returns: True if an update was started, false if there were no changes to the template since the last update. Raises: Exception: Any exception besides "No updates are to be performed." """ try: cf.update_stack(StackName=stack, TemplateURL=template_url, Capabilities=['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], Parameters=params) return True except botocore.exceptions.ClientError as e: if e.response['Error']['Message'] == 'No updates are to be performed.': return False else: raise Exception('Error updating CloudFormation stack "{0}"'.format(stack), e) def stack_exists(stack): """Check if a stack exists or not Args: stack: The stack to check Returns: True or False depending on whether the stack exists Raises: Any exceptions raised .describe_stacks() besides that the stack doesn't exist. """ try: cf.describe_stacks(StackName=stack) return True except botocore.exceptions.ClientError as e: if "does not exist" in e.response['Error']['Message']: return False else: raise e def create_stack(stack, template_url, params): """Starts a new CloudFormation stack creation Args: stack: The stack to be created template_url: The template for the stack to be created with Throws: Exception: Any exception thrown by .create_stack() """ cf.create_stack(StackName=stack, TemplateURL=template_url, Capabilities=['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], Parameters=params) def get_stack_status(stack): """Get the status of an existing CloudFormation stack Args: stack: The name of the stack to check Returns: The CloudFormation status string of the stack such as CREATE_COMPLETE Raises: Exception: Any exception thrown by .describe_stacks() """ stack_description = cf.describe_stacks(StackName=stack) return stack_description['Stacks'][0]['StackStatus'] def put_job_success(job, message): """Notify CodePipeline of a successful job Args: job: The CodePipeline job ID message: A message to be logged relating to the job status Raises: Exception: Any exception thrown by .put_job_success_result() """ print('Putting job success') print(message) code_pipeline.put_job_success_result(jobId=job) def put_job_failure(job, message): """Notify CodePipeline of a failed job Args: job: The CodePipeline job ID message: A message to be logged relating to the job status Raises: Exception: Any exception thrown by .put_job_failure_result() """ print('Putting job failure') print(message) code_pipeline.put_job_failure_result(jobId=job, failureDetails={'message': message, 'type': 'JobFailed'}) def continue_job_later(job, message): """Notify CodePipeline of a continuing job This will cause CodePipeline to invoke the function again with the supplied continuation token. Args: job: The JobID message: A message to be logged relating to the job status continuation_token: The continuation token Raises: Exception: Any exception thrown by .put_job_success_result() """ # Use the continuation token to keep track of any job execution state # This data will be available when a new job is scheduled to continue the current execution continuation_token = json.dumps({'previous_job_id': job}) print('Putting job continuation') print(message) code_pipeline.put_job_success_result(jobId=job, continuationToken=continuation_token) def start_update_or_create(job_id, stack, template_url, params): """Starts the stack update or create process If the stack exists then update, otherwise create. Args: job_id: The ID of the CodePipeline job stack: The stack to create or update template_url: The template to create/update the stack with """ if stack_exists(stack): status = get_stack_status(stack) if status not in ['CREATE_COMPLETE', 'ROLLBACK_COMPLETE', 'UPDATE_COMPLETE']: # If the CloudFormation stack is not in a state where # it can be updated again then fail the job right away. put_job_failure(job_id, 'Stack cannot be updated when status is: ' + status) return were_updates = update_stack(stack, template_url, params) if were_updates: # If there were updates then continue the job so it can monitor # the progress of the update. continue_job_later(job_id, 'Stack update started') else: # If there were no updates then succeed the job immediately put_job_success(job_id, 'There were no stack updates') else: # If the stack doesn't already exist then create it instead # of updating it. create_stack(stack, template_url, params) # Continue the job so the pipeline will wait for the CloudFormation # stack to be created. continue_job_later(job_id, 'Stack create started') def check_stack_update_status(job_id, stack): """Monitor an already-running CloudFormation update/create Succeeds, fails or continues the job depending on the stack status. Args: job_id: The CodePipeline job ID stack: The stack to monitor """ status = get_stack_status(stack) if status in ['UPDATE_COMPLETE', 'CREATE_COMPLETE']: # If the update/create finished successfully then # succeed the job and don't continue. put_job_success(job_id, 'Stack update complete') elif status in ['UPDATE_IN_PROGRESS', 'UPDATE_ROLLBACK_IN_PROGRESS', 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', 'CREATE_IN_PROGRESS', 'ROLLBACK_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS']: # If the job isn't finished yet then continue it continue_job_later(job_id, 'Stack update still in progress') else: # If the Stack is a state which isn't "in progress" or "complete" # then the stack update/create has failed so end the job with # a failed result. put_job_failure(job_id, 'Update failed: ' + status) def get_user_params(job_data): """Decodes the JSON user parameters and validates the required properties. Args: job_data: The job data structure containing the UserParameters string which should be a valid JSON structure Returns: The JSON parameters decoded as a dictionary. Raises: Exception: The JSON can't be decoded or a property is missing. """ try: # Get the user parameters which contain the stack, artifact and file settings user_parameters = job_data['actionConfiguration']['configuration']['UserParameters'] decoded_parameters = json.loads(user_parameters) except Exception: # We're expecting the user parameters to be encoded as JSON # so we can pass multiple values. If the JSON can't be decoded # then fail the job with a helpful message. raise Exception('UserParameters could not be decoded as JSON') if 'artifact' not in decoded_parameters: # Validate that the artifact name is provided, otherwise fail the job # with a helpful message. raise Exception('Your UserParameters JSON must include the artifact name') if 'template_file' not in decoded_parameters: # Validate that the template file is provided, otherwise fail the job # with a helpful message. raise Exception('Your UserParameters JSON must include the template file name') return decoded_parameters def setup_s3_client(job_data): """Creates an S3 client Uses the credentials passed in the event by CodePipeline. These credentials can be used to access the artifact bucket. Args: job_data: The job data structure Returns: An S3 client with the appropriate credentials """ # Could not use the artifact credentials to put object to artifacts s3 bucket. # We are running into issue as described in https://github.com/aws/aws-cdk/issues/3274 # key_id = job_data['artifactCredentials']['accessKeyId'] # key_secret = job_data['artifactCredentials']['secretAccessKey'] # session_token = job_data['artifactCredentials']['sessionToken'] # session = Session(aws_access_key_id=key_id, # aws_secret_access_key=key_secret, # aws_session_token=session_token) # return session.client('s3') return boto3.client('s3') def get_tenant_params(tenantId): """Get tenant details to be supplied to Cloud formation Args: tenantId (str): tenantId for which details are needed Returns: params from tenant management table """ params = [] param_tenantid = {} param_tenantid['ParameterKey'] = 'TenantIdParameter' param_tenantid['ParameterValue'] = tenantId params.append(param_tenantid) return params def add_parameter(params, parameter_key, parameter_value): parameter = {} parameter['ParameterKey'] = parameter_key parameter['ParameterValue'] = parameter_value params.append(parameter) def update_tenantstackmapping(tenantId, commit_id): """Update the tenant stack mapping table with the code pipeline job id Args: tenantId ([string]): tenant id for which data needs to be updated job_id ([type]): current code pipeline job id Returns: [type]: [description] """ response = table_tenant_stack_mapping.update_item( Key={'tenantId': tenantId}, UpdateExpression="set codeCommitId=:codeCommitId", ExpressionAttributeValues={ ':codeCommitId': commit_id }, ReturnValues="NONE") return response def lambda_handler(event, context): """The Lambda function handler If a continuing job then checks the CloudFormation stack status and updates the job accordingly. If a new job then kick of an update or creation of the target CloudFormation stack. Args: event: The event passed by Lambda context: The context passed by Lambda """ try: # Extract the Job ID job_id = event['CodePipeline.job']['id'] # Extract the Job Data job_data = event['CodePipeline.job']['data'] # Extract the params params = get_user_params(job_data) # Get the list of artifacts passed to the function artifacts = job_data['inputArtifacts'] artifact = params['artifact'] template_file = params['template_file'] commit_id = params['commit_id'] # Get all the stacks for each tenant to be updated/created from tenant stack mapping table mappings = table_tenant_stack_mapping.scan() print (mappings) #Update/Create stacks for all tenants for mapping in mappings['Items']: stack = mapping['stackName'] tenantId = mapping['tenantId'] applyLatestRelease = mapping['applyLatestRelease'] if (applyLatestRelease): # Get the parameters to be passed to the Cloudformation from tenant table params = get_tenant_params(tenantId) if 'continuationToken' in job_data: # If we're continuing then the create/update has already been triggered # we just need to check if it has finished. check_stack_update_status(job_id, stack) else: # Get the artifact details artifact_data = find_artifact(artifacts, artifact) # Get S3 client to access artifact with s3 = setup_s3_client(job_data) # Get the JSON template file out of the artifact template_url = get_template_url(s3, artifact_data, template_file) # Kick off a stack update or create start_update_or_create(job_id, stack, template_url, params) # If we are applying the release, update tenant stack mapping with the pipe line id update_tenantstackmapping(tenantId, commit_id) except Exception as e: # If any other exceptions which we didn't expect are raised # then fail the job and log the exception message. print('Function failed due to exception.') print(e) traceback.print_exc() put_job_failure(job_id, 'Function exception: ' + str(e)) #put_job_success(job_id, "Changeset executed successfully") print('Function complete.') return "Complete." ================================================ FILE: Lab5/server/TenantPipeline/test/pipeline.test.ts ================================================ // import { SynthUtils } from '@aws-cdk/assert'; // import { Stack, App } from 'aws-cdk-lib'; // import { Template } from 'aws-cdk-lib/assertions'; // import * as Pipeline from '../lib/serverless-saas-stack'; // test('synthesized cloudformation template should match original template', () => { // const app = new App(); // const stack = new Pipeline.ServerlessSaaSStack(app, 'MyTestStack'); // const template = Template.fromStack(stack); // expect(template).toMatchSnapshot(); // }); ================================================ FILE: Lab5/server/TenantPipeline/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2018", "module": "commonjs", "lib": ["es2018"], "declaration": true, "strict": true, "noImplicitAny": true, "strictNullChecks": true, "noImplicitThis": true, "alwaysStrict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": false, "inlineSourceMap": true, "inlineSources": true, "experimentalDecorators": true, "strictPropertyInitialization": false, "typeRoots": ["./node_modules/@types"] }, "exclude": ["cdk.out"] } ================================================ FILE: Lab5/server/custom_resources/requirements.txt ================================================ requests crhelper ================================================ FILE: Lab5/server/custom_resources/update_settings_table.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import logger from crhelper import CfnResource helper = CfnResource() try: dynamodb = boto3.resource('dynamodb') except Exception as e: helper.init_failure(e) @helper.create @helper.update def do_action(event, _): """ Called as part of bootstrap template. Inserts/Updates Settings table based upon the resources deployed inside bootstrap template We use these settings inside tenant template Args: event ([type]): [description] _ ([type]): [description] """ logger.info("Updating settings") settings_table_name = event['ResourceProperties']['SettingsTableName'] cognitoUserPoolId = event['ResourceProperties']['cognitoUserPoolId'] cognitoUserPoolClientId = event['ResourceProperties']['cognitoUserPoolClientId'] table_system_settings = dynamodb.Table(settings_table_name) response = table_system_settings.put_item( Item={ 'settingName': 'userPoolId-pooled', 'settingValue' : cognitoUserPoolId } ) response = table_system_settings.put_item( Item={ 'settingName': 'appClientId-pooled', 'settingValue' : cognitoUserPoolClientId } ) @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Lab5/server/custom_resources/update_tenant_apigatewayurl.py ================================================ import json import boto3 import logger from boto3.dynamodb.conditions import Key from crhelper import CfnResource helper = CfnResource() try: client = boto3.client('dynamodb') dynamodb = boto3.resource('dynamodb') except Exception as e: helper.init_failure(e) @helper.create @helper.update def do_action(event, _): """ The URL for Tenant APIs(Product/Order) can differ by tenant. For Pooled tenants it is shared and for Silo (Platinum tier tenants) it is unique to them. This method keeps the URL for Pooled tenants inside Settings Table, since it is shared across multiple tenants, And for Silo tenants inside the tenant management table along with other tenant settings, for that tenant Args: event ([type]): [description] _ ([type]): [description] """ logger.info("Updating Tenant Details table") tenant_details_table_name = event['ResourceProperties']['TenantDetailsTableName'] settings_table_name = event['ResourceProperties']['SettingsTableName'] tenant_id = event['ResourceProperties']['TenantId'] tenant_api_gateway_url = event['ResourceProperties']['TenantApiGatewayUrl'] if(tenant_id.lower() =='pooled'): # Note: Tenant management service will use below setting to update apiGatewayUrl for pooled tenants in TenantDetails table settings_table = dynamodb.Table(settings_table_name) settings_table.put_item(Item={ 'settingName': 'apiGatewayUrl-Pooled', 'settingValue' : tenant_api_gateway_url }) else: tenant_details = dynamodb.Table(tenant_details_table_name) response = tenant_details.update_item( Key={'tenantId': tenant_id}, UpdateExpression="set apiGatewayUrl=:apiGatewayUrl", ExpressionAttributeValues={ ':apiGatewayUrl': tenant_api_gateway_url }, ReturnValues="NONE") @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Lab5/server/custom_resources/update_tenantstackmap_table.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import logger from crhelper import CfnResource helper = CfnResource() try: dynamodb = boto3.resource('dynamodb') except Exception as e: helper.init_failure(e) @helper.create @helper.update def do_action(event, _): """ One time entry for pooled tenants inside tenant stack mapping table. This ensures that when code pipeline for tenant template is kicked off, it always create a default stack for pooled tenants. Args: event ([type]): [description] _ ([type]): [description] """ logger.info("Updating Tenant Stack Map") tenantstackmap_table_name = event['ResourceProperties']['TenantStackMappingTableName'] table_stack_mapping = dynamodb.Table(tenantstackmap_table_name) response = table_stack_mapping.put_item( Item={ 'tenantId': 'pooled', 'stackName' : 'stack-pooled', 'applyLatestRelease': True, 'codeCommitId': '' } ) @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Lab5/server/layers/auth_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils # These are the roles being supported in this reference architecture class UserRoles: SYSTEM_ADMIN = "SystemAdmin" CUSTOMER_SUPPORT = "CustomerSupport" TENANT_ADMIN = "TenantAdmin" TENANT_USER = "TenantUser" def isTenantAdmin(user_role): if (user_role == UserRoles.TENANT_ADMIN): return True else: return False def isSystemAdmin(user_role): if (user_role == UserRoles.SYSTEM_ADMIN): return True else: return False def isSaaSProvider(user_role): if (user_role == UserRoles.SYSTEM_ADMIN or user_role == UserRoles.CUSTOMER_SUPPORT): return True else: return False def isTenantUser(user_role): if (user_role == UserRoles.TENANT_USER): return True else: return False def getPolicyForUser(user_role, service_identifier, tenant_id, region, aws_account_id): """ This method is being used by Authorizer to get appropriate policy by user role Args: user_role (string): UserRoles enum tenant_id (string): region (string): aws_account_id (string): Returns: string: policy that tenant needs to assume """ iam_policy = "" if (isSystemAdmin(user_role)): iam_policy = __getPolicyForSystemAdmin(region, aws_account_id) elif (isTenantAdmin(user_role)): iam_policy = __getPolicyForTenantAdmin(tenant_id, service_identifier, region, aws_account_id) elif (isTenantUser(user_role)): iam_policy = __getPolicyForTenantUser(tenant_id, region, aws_account_id) return iam_policy def __getPolicyForSystemAdmin(region, aws_account_id): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/*".format(region, aws_account_id), ] } ] } return json.dumps(policy) def __getPolicyForTenantAdmin(tenant_id, sevice_identifier, region, aws_account_id): if (sevice_identifier == utils.Service_Identifier.SHARED_SERVICES.value): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantUserMapping".format(region, aws_account_id), "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantDetails".format(region, aws_account_id) ], "Condition": { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [ "{0}".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantStackMapping".format(region, aws_account_id), "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-Settings".format(region, aws_account_id) ] } ] } else: policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Product-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Order-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } } ] } return json.dumps(policy) def __getPolicyForTenantUser(tenant_id, region, aws_account_id): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Product-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Order-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } } ] } return json.dumps(policy) ================================================ FILE: Lab5/server/layers/logger.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from aws_lambda_powertools import Logger logger = Logger() """Log info messages """ def info(log_message): #logger.structure_logs(append=True, tenant_id=tenant_id) logger.info (log_message) """Log error messages """ def error(log_message): #logger.structure_logs(append=True, tenant_id=tenant_id) logger.error (log_message) """Log with tenant context. Extracts tenant context from the lambda events """ def log_with_tenant_context(event, log_message): logger.structure_logs(append=True, tenant_id= event['requestContext']['authorizer']['tenantId']) logger.info (log_message) ================================================ FILE: Lab5/server/layers/metrics_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json from aws_lambda_powertools import Metrics metrics = Metrics() def record_metric(event, metric_name, metric_unit, metric_value): """ Record the metric in Cloudwatch using EMF format Args: event ([type]): [description] metric_name ([type]): [description] metric_unit ([type]): [description] metric_value ([type]): [description] """ metrics.add_dimension(name="tenant_id", value=event['requestContext']['authorizer']['tenantId']) metrics.add_metric(name=metric_name, unit=metric_unit, value=metric_value) metrics_object = metrics.serialize_metric_set() metrics.clear_metrics() print(json.dumps(metrics_object)) ================================================ FILE: Lab5/server/layers/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] simplejson jsonpickle aws_requests_auth python-jose[cryptography] aws_requests_auth ================================================ FILE: Lab5/server/layers/utils.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import jsonpickle import simplejson import boto3 from aws_requests_auth.aws_auth import AWSRequestsAuth from enum import Enum class TenantTier(Enum): PLATINUM = "Platinum" PREMIUM = "Premium" STANDARD = "Standard" BASIC = "Basic" class StatusCodes(Enum): SUCCESS = 200 UN_AUTHORIZED = 401 NOT_FOUND = 404 class Service_Identifier(Enum): SHARED_SERVICES = "SharedServices" BUSINESS_SERVICES = "BusinessServices" def create_success_response(message): return { "statusCode": StatusCodes.SUCCESS.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def create_unauthorized_response(): return { "statusCode": StatusCodes.UN_AUTHORIZED.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": "User not authorized to perform this action" }), } def create_notfound_response(message): return { "statusCode": StatusCodes.NOT_FOUND.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def get_auth(host, region): session = boto3.Session() credentials = session.get_credentials() auth = AWSRequestsAuth(aws_access_key=credentials.access_key, aws_secret_access_key=credentials.secret_key, aws_token=credentials.token, aws_host=host, aws_region=region, aws_service='execute-api') return auth def get_headers(event): return event['headers'] def generate_response(inputObject): return { "statusCode": 200, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": encode_to_json_object(inputObject), } def encode_to_json_object(inputObject): jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True) jsonpickle.set_preferred_backend('simplejson') return jsonpickle.encode(inputObject, unpicklable=False, use_decimal=True) ================================================ FILE: Lab5/server/nested_templates/apigateway.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway, apis, api keys and usage plan as part of bootstrap Parameters: StageName: Type: String RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ProvisionTenantFunctionArn: Type: String DeProvisionTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetTenantConfigFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String Resources: ApiGatewayCloudWatchLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub apigateway-cloudwatch-publish-role-${AWS::Region} Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole ApiGatewayAttachCloudwatchLogArn: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchLogRole.Arn AdminApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/api-gateway/access-logs-serverless-saas-admin-api RetentionInDays: 30 AdminApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: "/*" HttpMethod: "*" Auth: ResourcePolicy: CustomStatements: - Effect: Allow Principal: "*" Action: "execute-api:Invoke" Resource: ["execute-api:/*/*/*"] - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/tenant" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/user/tenant-admin" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/provisioning" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref RegisterTenantLambdaExecutionRoleArn - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/disable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/enable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/provisioning/{tenantid}" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn AccessLogSetting: DestinationArn: !GetAtt AdminApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ["", ["serverless-saas-admin-api-", !Ref "AWS::Region"]] basePath: !Join ["", ["/", !Ref StageName]] schemes: - https paths: /registration: post: summary: Register a new tenant description: Register a new tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref RegisterTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /provisioning: post: summary: provisions resource for new tenant description: provisions resource for new tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ProvisionTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /provisioning/{tenantid}: put: summary: deprovision by tenant description: deprovision by tenant produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeProvisionTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/activation/{tenantid}: put: security: - Authorizer: [] summary: Activate an existing tenant description: Activate an existing tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ActivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenants: get: summary: Returns all tenants description: Returns all tenants produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantsFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant: post: summary: Creates a tenant description: Creates a tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/init/{tenantname}: get: summary: Returns a tenant config description: Return a tenant config by a tenant name produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantConfigFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/{tenantid}: get: summary: Returns a tenant description: Return a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Disables a tenant description: Disables a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeactivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy put: summary: Updates a tenant description: Updates a tenant produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user/{username}: get: summary: Returns a user description: Return a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUserFunctionArn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Diables a user description: Disable a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUserFunctionArn - /invocations httpMethod: POST type: aws_proxy /user/tenant-admin: post: summary: Creates a tenant admin user description: Creates a tenant admin user produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantAdminUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user: post: summary: Create a user description: Create a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users: get: summary: Get all users by tenantId description: Get all users by tenantId produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUsersFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/disable: put: summary: disable users by tenant id description: disable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/enable: put: summary: enable users by tenant id description: enable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref EnableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: api_key: type: "apiKey" name: "x-api-key" in: "header" sigv4Reference: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "awsSigv4" Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref AuthorizerFunctionArn - /invocations authorizerResultTtlInSeconds: 60 type: "token" StageName: prod Outputs: AdminApiGatewayApi: Value: !Ref AdminApiGatewayApi ================================================ FILE: Lab5/server/nested_templates/apigateway_lambdapermissions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway and apis as part of bootstrap Parameters: RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ProvisionTenantFunctionArn: Type: String DeProvisionTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetTenantConfigFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String AdminApiGatewayApi: Type: String Resources: #provide api gateway permissions to call lambda functions RegisterTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref RegisterTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateTenantAdminUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantAdminUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ProvisionTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ProvisionTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeProvisionTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeProvisionTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] EnableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref EnableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUsersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUsersFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref AuthorizerFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*" ]] CreateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantsFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeactivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeactivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ActivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ActivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantConfigLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantConfigFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ================================================ FILE: Lab5/server/nested_templates/cognito.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup cognito as part of bootstrap Parameters: AdminEmailParameter: Type: String Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Description: "Enter the role name for system admin" AdminUserPoolCallbackURLParameter: Type: String Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Description: "Enter Tenant Management userpool call back url" Resources: CognitoUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: PooledTenant-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into tenant UI application at " - "https://" - !Ref TenantUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for tenant UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSClient GenerateSecret: false UserPoolId: !Ref CognitoUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [pooledtenant-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoUserPool CognitoOperationUsersUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: OperationUsers-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into admin UI application at " - "https://" - !Ref AdminUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for admin UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoOperationUsersUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSOperationUsersPoolClient GenerateSecret: false UserPoolId: !Ref CognitoOperationUsersUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoOperationUsersUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [operationsusers-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUserGroup: Type: AWS::Cognito::UserPoolGroup Properties: GroupName: SystemAdmins Description: Admin user group Precedence: 0 UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUser: Type: AWS::Cognito::UserPoolUser Properties: Username: admin DesiredDeliveryMediums: - EMAIL ForceAliasCreation: true UserAttributes: - Name: email Value: !Ref AdminEmailParameter - Name: custom:tenantId Value: system_admins - Name: custom:userRole Value: !Ref SystemAdminRoleNameParameter UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAddUserToGroup: Type: AWS::Cognito::UserPoolUserToGroupAttachment Properties: GroupName: !Ref CognitoAdminUserGroup Username: !Ref CognitoAdminUser UserPoolId: !Ref CognitoOperationUsersUserPool Outputs: CognitoUserPoolId: Value: !Ref CognitoUserPool CognitoUserPoolClientId: Value: !Ref CognitoUserPoolClient CognitoOperationUsersUserPoolId: Value: !Ref CognitoOperationUsersUserPool CognitoOperationUsersUserPoolClientId: Value: !Ref CognitoOperationUsersUserPoolClient CognitoOperationUsersUserPoolProviderURL: Value: !GetAtt CognitoOperationUsersUserPool.ProviderURL ================================================ FILE: Lab5/server/nested_templates/custom_resources.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: ServerlessSaaSSettingsTableArn: Type: String ServerlessSaaSSettingsTableName: Type: String TenantStackMappingTableArn: Type: String TenantStackMappingTableName: Type: String UpdateSettingsTableFunctionArn: Type: String UpdateTenantStackMapTableFunctionArn: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String Resources: #Custom resources UpdateSettingsTable: Type: Custom::UpdateSettingsTable Properties: ServiceToken: !Ref UpdateSettingsTableFunctionArn SettingsTableName: !Ref ServerlessSaaSSettingsTableName cognitoUserPoolId: !Ref CognitoUserPoolId cognitoUserPoolClientId: !Ref CognitoUserPoolClientId UpdateTenantStackMap: Type: Custom::UpdateTenantStackMap Properties: ServiceToken: !Ref UpdateTenantStackMapTableFunctionArn TenantStackMappingTableName: !Ref TenantStackMappingTableName ================================================ FILE: Lab5/server/nested_templates/lambdafunctions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy lambda functions as part of bootstrap Parameters: CognitoOperationUsersUserPoolId: Type: String CognitoOperationUsersUserPoolClientId: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String TenantDetailsTableArn: Type: String ServerlessSaaSSettingsTableArn: Type: String TenantStackMappingTableArn: Type: String TenantUserMappingTableArn: Type: String TenantStackMappingTableName: Type: String TenantUserPoolCallbackURLParameter: Type: String Description: "Enter Tenant Management userpool call back url" Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies Description: Utilities for project ContentUri: ../layers/ CompatibleRuntimes: - python3.9 LicenseInfo: "MIT" RetentionPolicy: Retain Metadata: BuildMethod: python3.9 #Tenant Authorizer AuthorizerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: authorizer-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: authorizer-execution-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:List* Resource: - !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn AuthorizerAccessRole: Type: AWS::IAM::Role DependsOn: AuthorizerExecutionRole Properties: RoleName: authorizer-access-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: - !GetAtt 'AuthorizerExecutionRole.Arn' Action: - sts:AssumeRole Policies: - PolicyName: authorizer-access-role-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:BatchGetItem - dynamodb:GetItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:UpdateItem - dynamodb:Query - dynamodb:Scan Resource: - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/* SharedServicesAuthorizerFunction: Type: AWS::Serverless::Function DependsOn: AuthorizerAccessRole Properties: CodeUri: ../Resources/ Handler: shared_service_authorizer.lambda_handler Runtime: python3.9 Role: !GetAtt AuthorizerExecutionRole.Arn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !Ref CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !Ref CognitoOperationUsersUserPoolClientId #Create user pool for the tenant TenantUserPoolLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-userpool-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-userpool-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn - Effect: Allow Action: - dynamodb:GetItem - dynamodb:Query Resource: - !Ref TenantUserMappingTableArn CreateUserLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub create-user-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-user-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:PutItem Resource: - !Ref TenantUserMappingTableArn - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn CreateTenantAdminUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_tenant_admin_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId TENANT_APP_CLIENT_ID: !Ref CognitoUserPoolClientId TENANT_USER_POOL_CALLBACK_URL: !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] POWERTOOLS_SERVICE_NAME: "UserManagement.CreateTenantAdmin" #User management CreateUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.CreateUser" UpdateUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.update_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.UpdateUser" DisableUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUser" DisableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUsersByTenant" EnableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.enable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.EnableUsersByTenant" GetUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.GetUser" GetUsersFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_users Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.GetUsers" #Tenant Management TenantManagementLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-management-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-tenant-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:Scan - dynamodb:Query Resource: - !Ref TenantDetailsTableArn - !Join ["", [!Ref TenantDetailsTableArn, '/index/*']] - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref ServerlessSaaSSettingsTableArn CreateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.create_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.CreateTenant" ActivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.activate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.ActivateTenant" ENABLE_USERS_BY_TENANT: "/users/enable" PROVISION_TENANT: "/provisioning/" GetTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.GetTenant" DeactivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.deactivate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.DeactivateTenant" DEPROVISION_TENANT: "/provisioning/" DISABLE_USERS_BY_TENANT: "/users/disable" UpdateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.update_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.UpdateTenant" GetTenantsFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenants Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers GetTenantConfigFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.load_tenant_config Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers #Tenant Registration RegisterTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-registration-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess RegisterTenantFunction: Type: AWS::Serverless::Function DependsOn: RegisterTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-registration.register_tenant Runtime: python3.9 Role: !GetAtt RegisterTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: # Need to find a better way than hard coding resource paths CREATE_TENANT_ADMIN_USER_RESOURCE_PATH: "/user/tenant-admin" CREATE_TENANT_RESOURCE_PATH: "/tenant" PROVISION_TENANT_RESOURCE_PATH: "/provisioning" POWERTOOLS_SERVICE_NAME: "TenantRegistration.RegisterTenant" #Tenant Provisioning ProvisionTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-provisioning-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-provisioning-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:DeleteItem Resource: - !Ref TenantStackMappingTableArn - Effect: Allow Action: - codepipeline:StartPipelineExecution Resource: - !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:serverless-saas-pipeline - Effect: Allow Action: - cloudformation:DeleteStack Resource: "*" ProvisionTenantFunction: Type: AWS::Serverless::Function DependsOn: ProvisionTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-provisioning.provision_tenant Runtime: python3.9 Role: !GetAtt ProvisionTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_STACK_MAPPING_TABLE_NAME: !Ref TenantStackMappingTableName DeProvisionTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-deprovisioning-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-deprovisioning-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 #Since this lambda is invoking cloudformation which is inturn removing AWS resources, we are giving overly permissive permissions to this lambda. #You can limit this based upon your use case and AWS Resources you need to remove. Statement: - Effect: Allow Action: "*" Resource: "*" DeProvisionTenantFunction: Type: AWS::Serverless::Function DependsOn: DeProvisionTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-provisioning.deprovision_tenant Runtime: python3.9 Role: !GetAtt DeProvisionTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_STACK_MAPPING_TABLE_NAME: !Ref TenantStackMappingTableName UpdateSettingsTableLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub update-settingstable-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub update-settingstable-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: !Ref ServerlessSaaSSettingsTableArn UpdateSettingsTableFunction: Type: AWS::Serverless::Function DependsOn: UpdateSettingsTableLambdaExecutionRole Properties: CodeUri: ../custom_resources/ Handler: update_settings_table.handler Runtime: python3.9 Role: !GetAtt UpdateSettingsTableLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers UpdateTenantStackMapTableLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub update-tenantstackmap-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub update-tenantstackmap-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: !Ref TenantStackMappingTableArn UpdateTenantStackMapTableFunction: Type: AWS::Serverless::Function DependsOn: UpdateTenantStackMapTableLambdaExecutionRole Properties: CodeUri: ../custom_resources/ Handler: update_tenantstackmap_table.handler Runtime: python3.9 Role: !GetAtt UpdateTenantStackMapTableLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Outputs: RegisterTenantLambdaExecutionRoleArn: Value: !GetAtt RegisterTenantLambdaExecutionRole.Arn TenantManagementLambdaExecutionRoleArn: Value: !GetAtt TenantManagementLambdaExecutionRole.Arn RegisterTenantFunctionArn: Value: !GetAtt RegisterTenantFunction.Arn ProvisionTenantFunctionArn: Value: !GetAtt ProvisionTenantFunction.Arn DeProvisionTenantFunctionArn: Value: !GetAtt DeProvisionTenantFunction.Arn ActivateTenantFunctionArn: Value: !GetAtt ActivateTenantFunction.Arn GetTenantConfigFunctionArn: Value: !GetAtt GetTenantConfigFunction.Arn GetTenantsFunctionArn: Value: !GetAtt GetTenantsFunction.Arn CreateTenantFunctionArn: Value: !GetAtt CreateTenantFunction.Arn GetTenantFunctionArn: Value: !GetAtt GetTenantFunction.Arn DeactivateTenantFunctionArn: Value: !GetAtt DeactivateTenantFunction.Arn UpdateTenantFunctionArn: Value: !GetAtt UpdateTenantFunction.Arn GetUsersFunctionArn: Value: !GetAtt GetUsersFunction.Arn GetUserFunctionArn: Value: !GetAtt GetUserFunction.Arn UpdateUserFunctionArn: Value: !GetAtt UpdateUserFunction.Arn DisableUserFunctionArn: Value: !GetAtt DisableUserFunction.Arn CreateTenantAdminUserFunctionArn: Value: !GetAtt CreateTenantAdminUserFunction.Arn CreateUserFunctionArn: Value: !GetAtt CreateUserFunction.Arn DisableUsersByTenantFunctionArn: Value: !GetAtt DisableUsersByTenantFunction.Arn EnableUsersByTenantFunctionArn: Value: !GetAtt EnableUsersByTenantFunction.Arn SharedServicesAuthorizerFunctionArn: Value: !GetAtt SharedServicesAuthorizerFunction.Arn AuthorizerExecutionRoleArn: Value: !GetAtt AuthorizerExecutionRole.Arn UpdateSettingsTableFunctionArn: Value: !GetAtt UpdateSettingsTableFunction.Arn UpdateTenantStackMapTableFunctionArn: Value: !GetAtt UpdateTenantStackMapTableFunction.Arn ================================================ FILE: Lab5/server/nested_templates/tables.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to create dynamodb tables as part of bootstrap Resources: ServerlessSaaSSettingsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: settingName AttributeType: S KeySchema: - AttributeName: settingName KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-Settings TenantStackMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantStackMapping TenantDetailsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: tenantName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: ServerlessSaas-TenantConfig KeySchema: - AttributeName: tenantName KeyType: HASH Projection: NonKeyAttributes: - userPoolId - appClientId - apiGatewayUrl ProjectionType: INCLUDE TableName: ServerlessSaaS-TenantDetails TenantUserMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: userName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH - AttributeName: userName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantUserMapping GlobalSecondaryIndexes: - IndexName: UserName KeySchema: - AttributeName: userName KeyType: HASH - AttributeName: tenantId KeyType: RANGE Projection: ProjectionType: ALL Outputs: ServerlessSaaSSettingsTableArn: Value: !GetAtt ServerlessSaaSSettingsTable.Arn ServerlessSaaSSettingsTableName: Value: !Ref ServerlessSaaSSettingsTable TenantStackMappingTableArn: Value: !GetAtt TenantStackMappingTable.Arn TenantStackMappingTableName: Value: !Ref TenantStackMappingTable TenantDetailsTableArn: Value: !GetAtt TenantDetailsTable.Arn TenantDetailsTableName: Value: !Ref TenantDetailsTable TenantUserMappingTableArn: Value: !GetAtt TenantUserMappingTable.Arn TenantUserMappingTableName: Value: !Ref TenantUserMappingTable ================================================ FILE: Lab5/server/nested_templates/userinterface.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: IsCloudFrontAndS3PreProvisioned: Type: String Default: false Description: "Tells if cloudfront and s3 buckets are pre-provisioned or not. They get pre-provisioned when the workshop is running as a part of AWS Event through AWS event engine tool." Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref IsCloudFrontAndS3PreProvisioned, true] ] Resources: CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: IsNotRunningInEventEngine Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distributions" AdminAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AdminAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AdminAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AdminAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId AdminAppSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: #Aliases: # - !Sub 'admin.${CustomDomainName}' CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS CachedMethods: - GET - HEAD - OPTIONS Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: adminapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt AdminAppBucket.RegionalDomainName Id: adminapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' AppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId ApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: tenantapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'AppBucket.RegionalDomainName' Id: tenantapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' LandingAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True LandingAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref LandingAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${LandingAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId LandingApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: landingapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'LandingAppBucket.RegionalDomainName' Id: landingapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: AdminBucket: Description: The name of the bucket for uploading the Admin Management site to Value: !Ref AdminAppBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt AdminAppSite.DomainName Condition: IsNotRunningInEventEngine AppBucket: Description: The name of the bucket for uploading the Tenant Management site to Value: !Ref AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt ApplicationSite.DomainName Condition: IsNotRunningInEventEngine LandingAppBucket: Description: The name of the bucket for uploading the Landing site to Value: !Ref LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt LandingApplicationSite.DomainName Condition: IsNotRunningInEventEngine ================================================ FILE: Lab5/server/shared-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas" s3_bucket = "aws-saas-sam-cli-ujwbuket" s3_prefix = "serverless-saas" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" cached="true" parallel="true" ================================================ FILE: Lab5/server/shared-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to Bootstrap the Common Resources Parameters: AdminEmailParameter: Type: String Default: "test@test.com" Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Default: "SystemAdmin" Description: "Enter the role name for system admin" StageName: Type: String Default: "prod" Description: "Stage Name for the api" EventEngineParameter: Type: String Default: false Description: "Tells if this workshop is running as a part of AWS Event through AWS EventEngine or not" AdminUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Tenant Management userpool call back url" Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref EventEngineParameter, true] ] Resources: DynamoDBTables: Type: AWS::Serverless::Application Properties: Location: nested_templates/tables.yaml #Create cloudfront and s3 for UI Cde UserInterface: Type: AWS::Serverless::Application Properties: Location: nested_templates/userinterface.yaml Parameters: IsCloudFrontAndS3PreProvisioned: !Ref EventEngineParameter Cognito: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/cognito.yaml Parameters: AdminEmailParameter: !Ref AdminEmailParameter SystemAdminRoleNameParameter: !Ref SystemAdminRoleNameParameter AdminUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.AdminAppSite, !Ref AdminUserPoolCallbackURLParameter] TenantUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.ApplicationSite, !Ref TenantUserPoolCallbackURLParameter] LambdaFunctions: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/lambdafunctions.yaml Parameters: CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId CognitoOperationUsersUserPoolId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId CognitoOperationUsersUserPoolClientId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId TenantDetailsTableArn: !GetAtt DynamoDBTables.Outputs.TenantDetailsTableArn ServerlessSaaSSettingsTableArn: !GetAtt DynamoDBTables.Outputs.ServerlessSaaSSettingsTableArn TenantStackMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableArn TenantUserMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantUserMappingTableArn TenantStackMappingTableName: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableName TenantUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.ApplicationSite, !Ref TenantUserPoolCallbackURLParameter] APIs: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway.yaml Parameters: StageName: !Ref StageName RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ProvisionTenantFunctionArn DeProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeProvisionTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn GetTenantConfigFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantConfigFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn APIGatewayLambdaPermissions: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway_lambdapermissions.yaml Parameters: RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ProvisionTenantFunctionArn DeProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeProvisionTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantConfigFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantConfigFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn AdminApiGatewayApi: !GetAtt APIs.Outputs.AdminApiGatewayApi #setup custom resources CustomResources: Type: AWS::Serverless::Application DependsOn: APIs Properties: Location: nested_templates/custom_resources.yaml Parameters: ServerlessSaaSSettingsTableArn: !GetAtt DynamoDBTables.Outputs.ServerlessSaaSSettingsTableArn ServerlessSaaSSettingsTableName: !GetAtt DynamoDBTables.Outputs.ServerlessSaaSSettingsTableName TenantStackMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableArn TenantStackMappingTableName: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableName UpdateSettingsTableFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateSettingsTableFunctionArn UpdateTenantStackMapTableFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantStackMapTableFunctionArn CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId Outputs: AdminApi: Description: "API Gateway endpoint URL for Admin API" Value: !Join ["", ["https://", !GetAtt APIs.Outputs.AdminApiGatewayApi, ".execute-api.", !Ref "AWS::Region", ".amazonaws.com/", !Ref StageName]] AdminSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant administration application Value: !GetAtt UserInterface.Outputs.AdminBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt UserInterface.Outputs.AdminAppSite Condition: IsNotRunningInEventEngine LandingApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the landing application Value: !GetAtt UserInterface.Outputs.LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt UserInterface.Outputs.LandingApplicationSite Condition: IsNotRunningInEventEngine ApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant application Value: !GetAtt UserInterface.Outputs.AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt UserInterface.Outputs.ApplicationSite Condition: IsNotRunningInEventEngine CognitoOperationUsersUserPoolId: Description: The user pool id of Admin Management userpool Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId Export: Name: "Serverless-SaaS-CognitoOperationUsersUserPoolId" CognitoOperationUsersUserPoolProviderURL: Description: The Admin Management userpool provider url Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolProviderURL CognitoOperationUsersUserPoolClientId: Description: The Admin Management userpool client id Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId Export: Name: "Serverless-SaaS-CognitoOperationUsersUserPoolClientId" CognitoTenantUserPoolId: Description: The user pool id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolId Export: Name: "Serverless-SaaS-CognitoTenantUserPoolId" CognitoTenantAppClientId: Description: The app client id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolClientId Export: Name: "Serverless-SaaS-CognitoTenantAppClientId" AuthorizerExecutionRoleArn: Description: The Lambda authorizer execution role Value: !GetAtt LambdaFunctions.Outputs.AuthorizerExecutionRoleArn Export: Name: "Serverless-SaaS-AuthorizerExecutionRoleArn" ================================================ FILE: Lab5/server/tenant-buildspec.yml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 version: 0.2 phases: install: runtime-versions: python: 3.9 commands: # Install packages or any pre-reqs in this phase. # Upgrading SAM CLI to 1.33.0 version - python -m pip install aws-sam-cli==1.33.0 - sam --version # Installing project dependencies - cd Lab5/server/ProductService - python -m pip install -r requirements.txt - cd ../OrderService - python -m pip install -r requirements.txt pre_build: commands: # Run tests, lint scripts or any other pre-build checks. - cd .. - export PYTHONPATH=./ProductService/ # unit tests needs to be fixed. Commenting for now #- python -m pytest tests/unit/ProductService-test_handler.py build: commands: # Use Build phase to build your artifacts (compile, etc.) - sam build -t tenant-template.yaml post_build: commands: # Use Post-Build for notifications, git tags, upload artifacts to S3 - sam package --s3-bucket $PACKAGE_BUCKET --output-template-file packaged.yaml artifacts: discard-paths: yes files: # List of local artifacts that will be passed down the pipeline - Lab5/server/packaged.yaml ================================================ FILE: Lab5/server/tenant-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "stack-pooled" s3_bucket = "aws-saas-sam-cli-ujwbuket" s3_prefix = "serverless-saas-tenant" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM" cached="true" parallel="true" ================================================ FILE: Lab5/server/tenant-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Serverless SaaS Reference Architecture Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Parameters: TenantIdParameter: Type: String Default: pooled Description: Tenant ID for the stack StageName: Type: String Default: "prod" Description: "Stage Name for the api" Conditions: IsPooledDeploy: !Equals [ !Ref TenantIdParameter, pooled] IsSiloDeploy: !Not [!Equals [ !Ref TenantIdParameter, pooled]] Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: !Join ['-', [serverless-saas-dependencies, !Ref TenantIdParameter]] Description: Utilities for project ContentUri: layers/ CompatibleRuntimes: - python3.9 LicenseInfo: 'MIT' RetentionPolicy: Retain Metadata: BuildMethod: python3.9 ProductTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: productId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: productId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: !Join ['-', [Product, !Ref TenantIdParameter]] Tags: - Key: "TenantId" Value: !Ref TenantIdParameter OrderTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: orderId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: orderId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: !Join ['-', [Order, !Ref TenantIdParameter]] Tags: - Key: "TenantId" Value: !Ref TenantIdParameter ProductFunctionExecutionRolePolicy: Condition: IsSiloDeploy Type: AWS::IAM::Policy Properties: PolicyName: !Join ['-', [!Ref TenantIdParameter, product-function-policy]] Roles: - !Ref ProductFunctionExecutionRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt ProductTable.Arn ProductFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, product-function-execution-role]] Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess GetProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter GetProductsFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_products Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter CreateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.create_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter UpdateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.update_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter DeleteProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.delete_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter OrderFunctionExecutionRolePolicy: Condition: IsSiloDeploy Type: AWS::IAM::Policy Properties: PolicyName: !Join ['-', [!Ref TenantIdParameter, order-function-policy]] Roles: - !Ref OrderFunctionExecutionRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt OrderTable.Arn OrderFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, order-function-execution-role]] Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess GetOrdersFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_orders Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter GetOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter CreateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.create_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter UpdateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.update_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter DeleteOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.delete_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter BusinessServicesAuthorizerFunction: Type: AWS::Serverless::Function Properties: CodeUri: Resources/ Handler: tenant_authorizer.lambda_handler Runtime: python3.9 Role: !ImportValue Serverless-SaaS-AuthorizerExecutionRoleArn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !ImportValue Serverless-SaaS-CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !ImportValue Serverless-SaaS-CognitoOperationUsersUserPoolClientId ApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Join ['-', [/aws/api-gateway/access-logs-serverless-saas-tenant-api-, !Ref TenantIdParameter]] RetentionInDays: 30 ApiGatewayTenantApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: '/*' HttpMethod: '*' AccessLogSetting: DestinationArn: !GetAtt ApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ['-', [!Ref TenantIdParameter, 'serverless-saas-tenant-api']] basePath: !Join ['', ['/', !Ref StageName]] x-amazon-apigateway-api-key-source : "AUTHORIZER" schemes: - https paths: /order/{id}: get: summary: Returns a order description: Return a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a order description: Deletes a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /orders: get: summary: Returns all orders description: Returns all orders. produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrdersFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /order: post: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product/{id}: get: summary: Returns a product description: Return a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a product description: Deletes a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /products: get: summary: Returns all products description: Returns all products. produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductsFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product: post: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt BusinessServicesAuthorizerFunction.Arn - /invocations authorizerResultTtlInSeconds: 30 type: "token" StageName: !Ref StageName GetProductsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt GetProductsFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrdersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrdersFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt BusinessServicesAuthorizerFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref ApiGatewayTenantApi, "/*/*" ]] UpdateTenantApiGatewayUrlLambdaExecutionRole: Type: AWS::IAM::Role DependsOn: ApiGatewayTenantApi Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, apigwurl-lambda-exec-role]] Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Join ['-', [!Ref TenantIdParameter, apigwurl-lambda-exe-policy ]] PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/ServerlessSaaS-Settings - Effect: Allow Action: - dynamodb:UpdateItem Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/ServerlessSaaS-TenantDetails UpdateTenantApiGatewayUrlFunction: Type: AWS::Serverless::Function DependsOn: UpdateTenantApiGatewayUrlLambdaExecutionRole Properties: CodeUri: custom_resources/ Handler: update_tenant_apigatewayurl.handler Runtime: python3.9 Role: !GetAtt UpdateTenantApiGatewayUrlLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers UpdateTenantApiGatewayUrl: Type: Custom::UpdateTenantApiGatewayUrl DependsOn: UpdateTenantApiGatewayUrlFunction Properties: ServiceToken: !GetAtt UpdateTenantApiGatewayUrlFunction.Arn TenantDetailsTableName: ServerlessSaaS-TenantDetails SettingsTableName: ServerlessSaaS-Settings TenantId: !Ref TenantIdParameter TenantApiGatewayUrl: !Sub "https://${ApiGatewayTenantApi}.execute-api.${AWS::Region}.amazonaws.com/prod/" Outputs: TenantApiGatewayId: Description: Id for Tenant API Gateway Value: !Ref ApiGatewayTenantApi TenantAPI: Description: "API Gateway endpoint URL for Tenant API" Value: !Join ['', [!Sub "https://${ApiGatewayTenantApi}.execute-api.${AWS::Region}.amazonaws.com/", !Ref StageName]] ================================================ FILE: Lab6/client/Admin/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab6/client/Admin/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab6/client/Admin/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab6/client/Admin/README.md ================================================ # Admin This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab6/client/Admin/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "admin": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "admin:build:production" }, "development": { "browserTarget": "admin:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "admin:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab6/client/Admin/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab6/client/Admin/package.json ================================================ { "name": "admin", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/flex-layout": "~14.0.0-beta.40", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.1.3", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab6/client/Admin/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Tenants', url: '/tenants', icon: 'groups', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Lab6/client/Admin/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: '', component: NavComponent, data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'tenants', loadChildren: () => import('./views/tenants/tenants.module').then((m) => m.TenantsModule), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab6/client/Admin/src/app/app.component.scss ================================================ ================================================ FILE: Lab6/client/Admin/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Lab6/client/Admin/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { HttpInterceptorProviders } from './interceptors'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; @NgModule({ declarations: [AppComponent, NavComponent, AuthComponent], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, HttpClientModule, HttpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab6/client/Admin/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Lab6/client/Admin/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const HttpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Lab6/client/Admin/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Lab6/client/Admin/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Lab6/client/Admin/src/app/nav/nav.component.css ================================================ .sidenav-container { height: 100%; } .sidenav-content-container { background-color: lightgray; } .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .mat-toolbar.mat-primary { position: sticky; top: 0; z-index: 1; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .spinner-container { height: 80%; display: flex; justify-content: center; align-items: center; box-sizing: border-box; } ================================================ FILE: Lab6/client/Admin/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ username$ | async }}
================================================ FILE: Lab6/client/Admin/src/app/nav/nav.component.spec.ts ================================================ import { LayoutModule } from '@angular/cdk/layout'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { NavComponent } from './nav.component'; describe('NavComponent', () => { let component: NavComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [NavComponent], imports: [ NoopAnimationsModule, LayoutModule, MatButtonModule, MatIconModule, MatListModule, MatSidenavModule, MatToolbarModule, ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(NavComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should compile', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: Lab6/client/Admin/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.css'], }) export class NavComponent implements OnInit { tenantName = ''; loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router ) { this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => err); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map((sesh) => sesh && sesh.isValid()) ); const token$ = session$.pipe(map((sesh) => sesh && sesh.getIdToken())); this.username$ = token$.pipe( map((t) => t && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } } ================================================ FILE: Lab6/client/Admin/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Lab6/client/Admin/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Lab6/client/Admin/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Lab6/client/Admin/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Lab6/client/Admin/src/app/views/dashboard/dashboard.component.html ================================================

Traffic

December 2020
================================================ FILE: Lab6/client/Admin/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } .chart-wrapper { padding-right: 20px; position: relative; margin: auto; height: 80vh; width: 80vw; } ================================================ FILE: Lab6/client/Admin/src/app/views/dashboard/dashboard.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ChartConfiguration, ChartType } from 'chart.js'; import { TenantsService } from '../tenants/tenants.service'; interface DataSet { label: string; data: number[]; } interface ChartData { tenantId: string; dataSet: DataSet[]; totalOrders: number; } @Component({ templateUrl: 'dashboard.component.html', styleUrls: ['./dashboard.component.scss'], selector: 'app-dashboard', }) export class DashboardComponent implements OnInit { constructor(private tenantSvc: TenantsService) {} data: ChartData[] = []; // lineChart public lineChartElements = 27; public lineChartData1: Array = []; public lineChartData2: Array = []; public lineChartData3: Array = []; public lineChartData: Array = [ { data: this.lineChartData1, label: 'Current', backgroundColor: 'rgba(148,159,177,0.2)', borderColor: 'rgba(148,159,177,1)', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, { data: this.lineChartData2, label: 'Previous', backgroundColor: 'rgba(77,83,96,0.2)', borderColor: 'rgba(77,83,96,1)', pointBackgroundColor: 'rgba(77,83,96,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(77,83,96,1)', fill: 'origin', }, { data: this.lineChartData3, label: 'BEP', backgroundColor: 'rgba(255,0,0,0.3)', borderColor: 'red', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, ]; public lineChartLegend = false; /* tslint:disable:max-line-length */ public lineChartLabels: Array = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ]; /* tslint:enable:max-line-length */ public lineChartType: ChartType = 'line'; public lineChartOptions: ChartConfiguration['options'] = { maintainAspectRatio: false, elements: { line: { tension: 0.5, }, }, scales: { // We use this empty structure as a placeholder for dynamic theming. x: {}, 'y-axis-0': { position: 'left', }, 'y-axis-1': { position: 'right', grid: { color: 'rgba(80,80,80,0.3)', }, ticks: { color: '#808080', }, }, }, }; public random(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } ngOnInit(): void { for (let i = 0; i <= this.lineChartElements; i++) { this.lineChartData1.push(this.random(50, 200)); this.lineChartData2.push(this.random(80, 100)); this.lineChartData3.push(65); } } } ================================================ FILE: Lab6/client/Admin/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Lab6/client/Admin/src/app/views/tenants/create/create.component.html ================================================
Provision tenant Name Email Phone Address Plan
================================================ FILE: Lab6/client/Admin/src/app/views/tenants/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab6/client/Admin/src/app/views/tenants/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { TenantsService } from '../tenants.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { submitting = false; tenantForm: FormGroup = this.fb.group({ tenantName: [null, [Validators.required]], tenantEmail: [null, [Validators.email, Validators.required]], tenantTier: [null, [Validators.required]], tenantPhone: [null], tenantAddress: [null], }); constructor( private fb: FormBuilder, private tenantSvc: TenantsService, private router: Router, private _snackBar: MatSnackBar ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; const user = { ...this.tenantForm.value, }; this.tenantSvc.post(this.tenantForm.value).subscribe({ next: () => { this.submitting = false; this.openErrorMessageSnackBar('Successfully created new tenant!'); this.router.navigate(['tenants']); }, error: (err) => { this.submitting = false; this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); }, }); } } ================================================ FILE: Lab6/client/Admin/src/app/views/tenants/list/list.component.html ================================================
Tenant List
Id {{ element.tenantId }} Tenant Name {{ element.tenantName }} E-Mail {{ element.tenantEmail }} Plan {{ element.tenantTier }} Status {{ element.isActive }}
================================================ FILE: Lab6/client/Admin/src/app/views/tenants/list/list.component.scss ================================================ .tenant-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab6/client/Admin/src/app/views/tenants/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Tenant } from '../models/tenant'; import { TenantsService } from '../tenants.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { tenants$ = new Observable(); tenantData: Tenant[] = []; isLoading: boolean = true; displayedColumns = [ 'tenantId', 'tenantName', 'tenantEmail', 'tenantTier', 'isActive', ]; constructor(private tenantSvc: TenantsService) {} ngOnInit(): void { this.tenantSvc.fetch().subscribe((data) => { this.tenantData = data; this.isLoading = false; }); } } ================================================ FILE: Lab6/client/Admin/src/app/views/tenants/models/tenant.ts ================================================ export interface Tenant { tenantId: string; tenantName: string; tenantEmail: string; tenantTier: string; isActive: boolean; } ================================================ FILE: Lab6/client/Admin/src/app/views/tenants/tenants-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', component: ListComponent, data: { title: 'Tenant List', }, }, { path: 'create', component: CreateComponent, data: { title: 'Provision new tenant', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class TenantsRoutingModule {} ================================================ FILE: Lab6/client/Admin/src/app/views/tenants/tenants.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ListComponent } from './list/list.component'; import { TenantsRoutingModule } from './tenants-routing.module'; import { CreateComponent } from './create/create.component'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, TenantsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ReactiveFormsModule, MatSnackBarModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class TenantsModule {} ================================================ FILE: Lab6/client/Admin/src/app/views/tenants/tenants.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Tenant } from './models/tenant'; @Injectable({ providedIn: 'root', }) export class TenantsService { constructor(private http: HttpClient) {} baseUrl = `${environment.apiUrl}`; tenantsApiUrl = `${this.baseUrl}/tenants`; registrationApiUrl = `${this.baseUrl}/registration`; // TODO strongly-type these anys as tenants once we dial in what the tenant call should return fetch(): Observable { return this.http.get(this.tenantsApiUrl); } post(tenant: Tenant): Observable { return this.http.post(this.registrationApiUrl, tenant); } } ================================================ FILE: Lab6/client/Admin/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
Tenant Id folder_special
================================================ FILE: Lab6/client/Admin/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab6/client/Admin/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], tenantId: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Lab6/client/Admin/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Lab6/client/Admin/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab6/client/Admin/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Lab6/client/Admin/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Lab6/client/Admin/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Lab6/client/Admin/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Lab6/client/Admin/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.apiUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } } ================================================ FILE: Lab6/client/Admin/src/assets/.gitkeep ================================================ ================================================ FILE: Lab6/client/Admin/src/aws-exports.ts ================================================ const awsmobile = { aws_project_region: 'us-west-2', aws_cognito_region: 'us-west-2', aws_user_pools_id: 'us-west-2_nWhwjijMc', aws_user_pools_web_client_id: '61ipvhmvdrfso0cftpil9irk8k', }; export default awsmobile; ================================================ FILE: Lab6/client/Admin/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab6/client/Admin/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab6/client/Admin/src/environments/environment.ts ================================================ export const environment = { production: false, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab6/client/Admin/src/index.html ================================================ Dashboard ================================================ FILE: Lab6/client/Admin/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { Amplify } from 'aws-amplify'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import aws_exports from './aws-exports'; Amplify.configure(aws_exports); if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab6/client/Admin/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab6/client/Admin/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab6/client/Admin/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab6/client/Admin/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; ================================================ FILE: Lab6/client/Admin/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab6/client/Admin/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab6/client/Admin/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab6/client/Admin/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab6/client/Application/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab6/client/Application/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab6/client/Application/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end #cypress cypress/videos/* cypress.env.json # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab6/client/Application/README.md ================================================ # Application This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab6/client/Application/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "application": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "2mb", "maximumError": "2mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "6kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "application:build:production" }, "development": { "browserTarget": "application:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "application:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab6/client/Application/cypress/README.md ================================================ # Application End-to-End Testing ## Instructions To run End-to-End (e2e) tests against the Sample Application, take the following steps: 1. Make a copy of the example env file (`cypress.env.json.example`): ```bash cp cypress.env.json.example cypress.env.json ``` 2. Edit the new file and replace the sample values with real values. The following should help when deciding what to use in place of the sample values provided: - `host`: The URL where the Sample Application is running. If testing locally, this is usually `"http://localhost:4200"`. - `tenantName`: The name of the tenant used to identify the appropriate Cognito User Pool to use for Authentication. - `tenantUsername`: The username to use when logging in. - `tenantUserPassword`: The password to use when logging in. - `email`: The email address to use for testing. (This should be a valid email address.) 3. Navigate to the root of the Application project (`aws-saas-factory-ref-solution-serverless-saas/clients/Application/`) and run the following: ```bash npx cypress run ``` This will run the tests located in the `cypress/e2e` folder. The documentation [here](https://docs.cypress.io/guides/guides/command-line#cypress-run) has more information on what can be passed in as arguments when running the Cypress tests. For example, running the following will show the Cypress UI and what is happening as each of the tests are run: ```bash npx cypress run --headed ``` ================================================ FILE: Lab6/client/Application/cypress/e2e/1-getting-started/basic-access.cy.js ================================================ /// describe('check that the app redirects to /unauthorized when tenant is not set', () => { it('redirects to unauthorized when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); }) it('redirects to unauthorized when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); }) it('redirects to unauthorized when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); }) }) describe('check that the app redirects to a page with a sign-in form when tenant is set and user is not logged in', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').should('exist') cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.location('href').should('contain', '/dashboard') }) it('redirects when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) }) ================================================ FILE: Lab6/client/Application/cypress/e2e/1-getting-started/product-testing.cy.js ================================================ /// describe('check that product, order and user functionality works as expected', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.get('form input[name="username"]').type(Cypress.env('tenantUsername')) cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() cy.wait(1500) cy.get('body').then(body => { if (body.find('form input[name="confirm_password"]').length > 0) { cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form input[name="confirm_password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() } }) }) it('can create new users and display them', () => { const email_username = Cypress.env('email').split('@')[0]; const email_domain = Cypress.env('email').split('@')[1]; const random_suffix = '+test'+Date.now().toString().slice(-3); const myUser = { name: "myUser-"+Date.now(), email: email_username + random_suffix + '@' + email_domain, role: 'userRole'+Date.now().toString().slice(-5) } cy.get("a").contains("Users").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.get("button[color='primary'").contains("Add User").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="userName"]').type(myUser.name) cy.get('input[formcontrolname="userEmail"]').type(myUser.email) cy.get('input[formcontrolname="userRole"]').type(myUser.role) }) cy.intercept({ method: 'POST', url: '**/user', }).as('postUser') cy.intercept({ method: 'GET', url: '**/users', }).as('getUsers') cy.get("button").contains("Create").click() cy.wait('@postUser') cy.go('back') cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.wait('@getUsers') cy.get('table td').contains(myUser.email) }) it('can create a new order with a new product and see them listed', () => { const myProduct = { name: "myProduct-"+Date.now(), price: Date.now().toString().slice(-3), sku: Date.now().toString().slice(-5), category: "category3", } const myOrder = { name: "myOrder-"+Date.now(), } // NOW TESTING PRODUCT CREATION // cy.get("a").contains("Products").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.get("button[color='primary'").contains("Create Product").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="name"]').type(myProduct.name) cy.get('input[formcontrolname="price"]').type(myProduct.price) cy.get('input[formcontrolname="sku"]').type(myProduct.sku) cy.get('mat-select[formcontrolname="category"]').click() }) cy.get('.mat-option-text').contains(myProduct.category).click() cy.get("button").contains("Submit").should('not.be.disabled') cy.intercept({ method: 'POST', url: '**/product', }).as('postProduct') cy.intercept({ method: 'GET', url: '**/products', }).as('getProducts') cy.get("button").contains("Submit").click() cy.wait('@postProduct') cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.wait('@getProducts') cy.get('table td').contains(myProduct.name) cy.get('table td').contains(myProduct.price) cy.get("a").contains("Orders").click() // DONE TESTING PRODUCT CREATION // // NOW TESTING ORDER CREATION // cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.get("button[color='primary']").contains("Create Order").click() cy.location().should((loc) => { expect(loc.href).to.contain('/orders/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="orderName"]').type(myOrder.name) }) cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.intercept({ method: 'POST', url: '**/order', }).as('postOrder') cy.intercept({ method: 'GET', url: '**/orders', }).as('getOrders') cy.get("button").contains("Submit").click() cy.wait('@postOrder') cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.wait('@getOrders') cy.get('table td').contains(myOrder.name) cy.get('table td').contains(new Intl.NumberFormat().format(myProduct.price * 2)) // DONE TESTING ORDER CREATION // }) }) ================================================ FILE: Lab6/client/Application/cypress.config.ts ================================================ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { supportFile: false, setupNodeEvents(on, config) { // implement node event listeners here }, }, }); ================================================ FILE: Lab6/client/Application/cypress.env.json.example ================================================ { "host": "http://localhost:4200", "tenantName": "UPDATE_ME!", "tenantUsername": "UPDATE_ME!", "tenantUserPassword": "UPDATE_ME!", "email": "test@example.com" } ================================================ FILE: Lab6/client/Application/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/application'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab6/client/Application/package.json ================================================ { "name": "application", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "e2e": "npx cypress run" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.2.0", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "cypress": "~10.3.1", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab6/client/Application/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Products', url: '/products', icon: 'sell', }, { name: 'Orders', url: '/orders', icon: 'shopping_cart', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Lab6/client/Application/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { CognitoGuard } from './cognito.guard'; export const routes: Routes = [ { path: '', redirectTo: 'unauthorized', pathMatch: 'full', }, { path: '', component: NavComponent, canActivate: [CognitoGuard], data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'orders', loadChildren: () => import('./views/orders/orders.module').then((m) => m.OrdersModule), }, { path: 'products', loadChildren: () => import('./views/products/products.module').then( (m) => m.ProductsModule ), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, { path: 'unauthorized', component: UnauthorizedComponent, }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab6/client/Application/src/app/app.component.scss ================================================ ================================================ FILE: Lab6/client/Application/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Lab6/client/Application/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { httpInterceptorProviders } from './interceptors'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; @NgModule({ declarations: [ AppComponent, NavComponent, AuthComponent, UnauthorizedComponent, ], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, MatSnackBarModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatIconModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, httpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab6/client/Application/src/app/cognito.guard.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, } from '@angular/router'; import { Auth } from 'aws-amplify'; import { AuthConfigurationService } from './views/auth/auth-configuration.service'; @Injectable({ providedIn: 'root' }) export class CognitoGuard implements CanActivate { constructor( private router: Router, private authConfigService: AuthConfigurationService ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Promise { if (!this.authConfigService.configureAmplifyAuth()) { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return new Promise((res, rej) => { res(false); }); } return Auth.currentSession() .then((u) => { if (u.isValid()) { return true; } else { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return false; } }) .catch((e) => { if (state.url === '/dashboard') { // if we're going to the dashboard and we're not logged in, // don't stop the flow as the amplify-authenticator will // route requests going to the dashboard to the sign-in page. return true; } console.log('Error getting current session', e); this.router.navigate(['/unauthorized']); return false; }); } } ================================================ FILE: Lab6/client/Application/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Lab6/client/Application/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const httpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Lab6/client/Application/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Lab6/client/Application/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Lab6/client/Application/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ (companyName$ | async) || "" }} {{ (username$ | async) || user.username}}
================================================ FILE: Lab6/client/Application/src/app/nav/nav.component.scss ================================================ .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .sidebar-icon-container { height: 64px; background-color: whitesmoke; display: flex; align-items: center; justify-content: center; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .footer { position: fixed; bottom: 0; width: 100%; height: 40px; background-color: whitesmoke; color: #2d3337; // text-align: center; margin: 0px; } .footer-text { display: flex; } .content { position: absolute; width: 100%; height: 90%; max-height: 90%; overflow: auto; background-color: lightgray; } ================================================ FILE: Lab6/client/Application/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; import { AuthConfigurationService } from './../views/auth/auth-configuration.service'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.scss'], }) export class NavComponent implements OnInit { loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router, private authConfigService: AuthConfigurationService ) { // this.configSvc.loadConfigurations().subscribe((val) => console.log(val)); this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => { console.log('Failed to get current session. Err: ', err); return err; }); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map( (sesh) => sesh && typeof sesh.isValid === 'function' && sesh.isValid() ) ); const token$ = session$.pipe( map( (sesh) => sesh && typeof sesh.getIdToken === 'function' && sesh.getIdToken() ) ); this.username$ = token$.pipe( map((t) => t && t.payload && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload && t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } async logout() { await Auth.signOut({ global: true }) .then((e) => { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); }) .catch((err) => { console.error('Error logging out: ', err); }); } } ================================================ FILE: Lab6/client/Application/src/app/views/auth/auth-configuration.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpParams, HttpParamsOptions, } from '@angular/common/http'; import { Injectable, OnInit } from '@angular/core'; import { map, switchMap, catchError } from 'rxjs/operators'; import { throwError } from 'rxjs'; import { environment } from '../../../environments/environment'; import { ConfigParams } from './models/config-params'; import { ActivatedRoute } from '@angular/router'; import Amplify from 'aws-amplify'; import { Auth } from 'aws-amplify'; import { Router } from '@angular/router'; import { from, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AuthConfigurationService { params$: Observable; params: ConfigParams; tenantName: string; constructor( private http: HttpClient, private route: ActivatedRoute, private router: Router ) {} public setTenantConfig(tenantName: string): Promise { const url = `${environment.regApiGatewayUrl}/tenant/init/` + tenantName; this.params$ = this.http.get(url); const setup$ = this.params$.pipe( map((val) => { // remove trailing slash (/) if present val.apiGatewayUrl = val.apiGatewayUrl.replace(/\/$/, ''); localStorage.setItem('userPoolId', val.userPoolId); localStorage.setItem('tenantName', tenantName); localStorage.setItem('appClientId', val.appClientId); localStorage.setItem('apiGatewayUrl', val.apiGatewayUrl); return 'success'; }), catchError((error) => { console.log('Error setting tenant config: ', error); return throwError(error); }) ); return setup$.toPromise(); } configureAmplifyAuth(): boolean { try { const userPoolId = localStorage.getItem('userPoolId'); const appClientId = localStorage.getItem('appClientId'); if (!userPoolId || !appClientId) { return false; } const region = userPoolId?.split('_')[0]; const awsmobile = { aws_project_region: region, aws_cognito_region: region, aws_user_pools_id: userPoolId, aws_user_pools_web_client_id: appClientId, }; Amplify.configure(awsmobile); return true; } catch (err) { console.error('Unable to initialize amplify auth.', err); return false; } } cleanLocalStorage() { localStorage.removeItem('tenantName'); localStorage.removeItem('userPoolId'); localStorage.removeItem('appClientId'); localStorage.removeItem('apiGatewayUrl'); } } ================================================ FILE: Lab6/client/Application/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Lab6/client/Application/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Lab6/client/Application/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Lab6/client/Application/src/app/views/auth/models/config-params.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ export interface ConfigParams { appClientId: string; userPoolId: string; apiGatewayUrl: string; } ================================================ FILE: Lab6/client/Application/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Lab6/client/Application/src/app/views/dashboard/dashboard.component.html ================================================

Dashboard

{{ card.title }}
================================================ FILE: Lab6/client/Application/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } ================================================ FILE: Lab6/client/Application/src/app/views/dashboard/dashboard.component.ts ================================================ import { Component, ViewChild } from '@angular/core'; import { map } from 'rxjs/operators'; import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js'; import { BaseChartDirective } from 'ng2-charts'; import DataLabelsPlugin from 'chartjs-plugin-datalabels'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'], }) export class DashboardComponent { @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; public barChartOptions: ChartConfiguration['options'] = { responsive: true, maintainAspectRatio: false, // We use these empty structures as placeholders for dynamic theming. scales: { x: {}, y: { min: 10, }, }, plugins: { legend: { display: true, }, datalabels: { anchor: 'end', align: 'end', }, }, }; public barChartType: ChartType = 'bar'; public barChartPlugins = [DataLabelsPlugin]; public barChartData: ChartData<'bar'> = { labels: ['2006', '2007', '2008', '2009', '2010', '2011', '2012'], datasets: [ { data: [65, 59, 80, 81, 56, 55, 40], label: 'Series A' }, { data: [28, 48, 40, 19, 86, 27, 90], label: 'Series B' }, ], }; // events public chartClicked({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public chartHovered({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public randomize(): void { // Only Change 3 values this.barChartData.datasets[0].data = [ Math.round(Math.random() * 100), 59, 80, Math.round(Math.random() * 100), 56, Math.round(Math.random() * 100), 40, ]; this.chart?.update(); } /** Based on the screen size, switch from standard to one column per row */ cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe( map(({ matches }) => { if (matches) { return [ { title: 'Card 1', cols: 1, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 1 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; } return [ { title: 'Card 1', cols: 2, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 2 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; }) ); constructor(private breakpointObserver: BreakpointObserver) {} } ================================================ FILE: Lab6/client/Application/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Lab6/client/Application/src/app/views/error/404.component.html ================================================

404

Oops! You're lost.

The page you are looking for was not found.

================================================ FILE: Lab6/client/Application/src/app/views/error/404.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '404.component.html', }) export class P404Component { constructor() {} } ================================================ FILE: Lab6/client/Application/src/app/views/error/500.component.html ================================================

500

Houston, we have a problem!

The page you are looking for is temporarily unavailable.

================================================ FILE: Lab6/client/Application/src/app/views/error/500.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '500.component.html', }) export class P500Component { constructor() {} } ================================================ FILE: Lab6/client/Application/src/app/views/error/unauthorized.component.html ================================================
Unauthorized Enter your tenant name and click submit below Tenant Name home
================================================ FILE: Lab6/client/Application/src/app/views/error/unauthorized.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .center-screen { display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; min-height: 100vh; } ================================================ FILE: Lab6/client/Application/src/app/views/error/unauthorized.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AuthConfigurationService } from './../auth/auth-configuration.service'; import { Observable } from 'rxjs'; import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-unauthorized', templateUrl: './unauthorized.component.html', styleUrls: ['./unauthorized.component.scss'], }) export class UnauthorizedComponent implements OnInit { tenantForm: FormGroup; params$: Observable; error = false; errorMessage: string; constructor( private fb: FormBuilder, private authConfigService: AuthConfigurationService, private _snackBar: MatSnackBar, private router: Router ) {} ngOnInit(): void { this.tenantForm = this.fb.group({ tenantName: [null, [Validators.required]], }); if (localStorage.getItem('tenantName')) { this.router.navigate(['/dashboard']); } } isFieldInvalid(field: string) { const formField = this.tenantForm.get(field); return ( formField && formField.invalid && (formField.dirty || formField.touched) ); } displayFieldCss(field: string) { return { 'is-invalid': this.isFieldInvalid(field), }; } hasRequiredError(field: string) { return !!this.tenantForm.get(field)?.hasError('required'); } openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } login() { let tenantName = this.tenantForm.value.tenantName; if (!tenantName) { this.errorMessage = 'No tenant name provided.'; this.error = true; this.openErrorMessageSnackBar(this.errorMessage); return false; } this.authConfigService .setTenantConfig(tenantName) .then((val) => { this.router.navigate(['/dashboard']); }) .catch((errorResponse) => { this.error = true; this.errorMessage = errorResponse.error.message || 'An unexpected error occurred!'; this.openErrorMessageSnackBar(this.errorMessage); }); return false; } } ================================================ FILE: Lab6/client/Application/src/app/views/orders/create/create.component.html ================================================
Create new order Enter order name Name is required
{{ op.product.name }}
{{ op.product.price | currency }}
{{ op.quantity || 0 }}
================================================ FILE: Lab6/client/Application/src/app/views/orders/create/create.component.scss ================================================ // .card { // margin: 20px; // display: flex; // flex-direction: column; // align-items: flex-start; // } .order-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab6/client/Application/src/app/views/orders/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { Product } from '../../products/models/product.interface'; import { ProductService } from '../../products/product.service'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; interface LineItem { product: Product; quantity?: number; } @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { displayedColumns = []; orderForm: FormGroup; orderProducts: LineItem[] = []; isLoadingProducts: boolean = true; error = ''; constructor( private fb: FormBuilder, private productSvc: ProductService, private orderSvc: OrdersService, private router: Router ) { this.orderForm = this.fb.group({ name: ['', Validators.required], }); } ngOnInit(): void { this.productSvc.fetch().subscribe((products) => { this.orderProducts = products.map((p) => ({ product: p })); this.isLoadingProducts = false; }); this.orderForm = this.fb.group({ orderName: ['', Validators.required], }); } get name() { return this.orderForm.get('name'); } get productQuantity() { return this.orderProducts .filter((p) => !!p.quantity).length > 0; } add(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity ? orderProduct.quantity + 1 : 1, }; } return p; }); } remove(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity && orderProduct.quantity > 1 ? orderProduct.quantity - 1 : undefined, }; } return p; }); } submit() { const val: Order = { ...this.orderForm?.value, orderProducts: this.orderProducts .filter((p) => !!p.quantity) .map((p) => ({ productId: p.product.productId, price: p.product.price, quantity: p.quantity, })), }; this.orderSvc.create(val).subscribe(() => this.router.navigate(['orders'])); } } ================================================ FILE: Lab6/client/Application/src/app/views/orders/detail/detail.component.html ================================================

Order Details mostly made up

NO: {{ orderId$ | async }} | Date: {{ today() | date }}
  • {{ tenantName() }}

Services / Products

Item / Details Unit Cost Sum Cost Discount Tax Total
{{ op.productId }}
{{ op.price | currency }}
Before Tax
{{ sum(op) | currency }}
{{ op.quantity }} Units
$0.00
None
{{ tax(op) | currency }}
Sales Tax 8.9%
{{ total(op) | currency }}
Sub Total Discount Total Tax Final
{{ subTotal(order) | currency }} -$0.00 {{ subTotal(order) | currency }} {{ calcTax(order) | currency }} {{ final(order) | currency }}
Comments / Notes
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Odit repudiandae numquam sit facere blanditiis, quasi distinctio ipsam? Libero odit ex expedita, facere sunt, possimus consectetur dolore, nobis iure amet vero.
================================================ FILE: Lab6/client/Application/src/app/views/orders/detail/detail.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .no-bottom-margin { margin-bottom: 0; } .nowrap { white-space: nowrap; } .invoice { font-family: Arial, Helvetica, sans-serif; width: 970px !important; margin: 50px auto; .invoice-header { padding: 25px 25px 15px; h1 { margin: 0; } .media { .media-body { font-size: 0.9em; margin: 0; } } } .invoice-body { border-radius: 10px; padding: 25px; background: #fff; } .invoice-footer { padding: 15px; font-size: 0.9em; text-align: center; color: #999; } } .logo { max-height: 70px; border-radius: 10px; } .dl-horizontal { margin: 0; dt { float: left; width: 80px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } dd { margin-left: 90px; } } .rowamount { padding-top: 15px !important; } .rowtotal { font-size: 1.3em; } .colfix { width: 12%; } .mono { font-family: monospace; } ================================================ FILE: Lab6/client/Application/src/app/views/orders/detail/detail.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Order } from '../models/order.interface'; import { OrderProduct } from '../models/orderproduct.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-detail', templateUrl: './detail.component.html', styleUrls: ['./detail.component.scss'], }) export class DetailComponent implements OnInit { orderId$: Observable | undefined; order$: Observable | undefined; orderProducts$: Observable | undefined; taxRate = 0.0899; constructor(private route: ActivatedRoute, private orderSvc: OrdersService) {} ngOnInit(): void { this.orderId$ = this.route.params.pipe(map((o) => o['orderId'])); this.order$ = this.orderId$.pipe(switchMap((o) => this.orderSvc.get(o))); this.orderProducts$ = this.order$.pipe(map((o) => o.orderProducts)); } today() { return new Date(); } tenantName() { return ''; } sum(op: OrderProduct) { return op.price * op.quantity; } tax(op: OrderProduct) { return this.sum(op) * this.taxRate; } total(op: OrderProduct) { return this.sum(op) + this.tax(op); } subTotal(order: Order) { return order.orderProducts .map((op) => op.price * op.quantity) .reduce((acc, curr) => acc + curr); } calcTax(order: Order) { return this.subTotal(order) * this.taxRate; } final(order: Order) { return this.subTotal(order) + this.calcTax(order); } } ================================================ FILE: Lab6/client/Application/src/app/views/orders/list/list.component.html ================================================

Order List

Name {{ element.orderName }} Line Items {{ element.orderProducts?.length }} Total {{ sum(element) | currency }}
================================================ FILE: Lab6/client/Application/src/app/views/orders/list/list.component.scss ================================================ .order-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Lab6/client/Application/src/app/views/orders/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { displayedColumns: string[] = ['name', 'lineItems', 'total']; orderData: Order[] = []; isLoading: boolean = true; constructor(private orderSvc: OrdersService, private router: Router) {} ngOnInit(): void { this.orderSvc.fetch().subscribe((data) => { this.isLoading = false; this.orderData = data; }); } sum(order: Order): number { return order.orderProducts .map((p) => p.price * p.quantity) .reduce((acc, curr) => acc + curr); } } ================================================ FILE: Lab6/client/Application/src/app/views/orders/models/order.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { OrderProduct } from './orderproduct.interface'; export interface Order { key: string; shardId: string; orderId: string; orderName: string; orderProducts: OrderProduct[]; } ================================================ FILE: Lab6/client/Application/src/app/views/orders/models/orderproduct.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface OrderProduct { productId: string; price: number; quantity: number; } ================================================ FILE: Lab6/client/Application/src/app/views/orders/orders-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', data: { title: 'All Orders', }, component: ListComponent, }, { path: 'create', data: { title: 'Create Order', }, component: CreateComponent, }, { path: 'detail/:orderId', data: { title: 'View Order Detail', }, component: DetailComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class OrdersRoutingModule {} ================================================ FILE: Lab6/client/Application/src/app/views/orders/orders.module.ts ================================================ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; import { OrdersRoutingModule } from './orders-routing.module'; @NgModule({ declarations: [CreateComponent, ListComponent, DetailComponent], imports: [ CommonModule, OrdersRoutingModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class OrdersModule {} ================================================ FILE: Lab6/client/Application/src/app/views/orders/orders.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Order } from './models/order.interface'; @Injectable({ providedIn: 'root', }) export class OrdersService { orders: Order[] = []; baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; constructor(private http: HttpClient) {} fetch(): Observable { return this.http.get(`${this.baseUrl}/orders`); } get(orderId: string): Observable { const url = `${this.baseUrl}/order/${orderId}`; return this.http.get(url); } create(order: Order): Observable { return this.http.post(`${this.baseUrl}/order`, order); } } ================================================ FILE: Lab6/client/Application/src/app/views/products/create/create.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required SKU Category {{category}}
================================================ FILE: Lab6/client/Application/src/app/views/products/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab6/client/Application/src/app/views/products/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ProductService } from '../product.service'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); } get name() { return this.productForm.get('name'); } get price() { return this.productForm.get('price'); } submit() { this.productSvc.post(this.productForm.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err.message); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Lab6/client/Application/src/app/views/products/edit/edit.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required Description
================================================ FILE: Lab6/client/Application/src/app/views/products/edit/edit.component.scss ================================================ ================================================ FILE: Lab6/client/Application/src/app/views/products/edit/edit.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.scss'], }) export class EditComponent implements OnInit { productForm: FormGroup | undefined; product$: Observable | undefined; productId$: Observable | undefined; productName$: Observable | undefined; constructor( private route: ActivatedRoute, private router: Router, private productSvc: ProductService, private fb: FormBuilder ) {} ngOnInit(): void { this.productId$ = this.route.params.pipe(map((p) => p['productId'])); this.product$ = this.productId$.pipe( switchMap((p) => this.productSvc.get(p)) ); this.productName$ = this.product$.pipe(map((p) => p?.name)); this.productForm = this.fb.group({ productId: [''], name: [''], price: [''], description: [''], }); this.product$.subscribe((val) => { this.productForm?.patchValue({ ...val, }); }); } get name() { return this.productForm?.get('name'); } get price() { return this.productForm?.get('price'); } submit() { this.productSvc.put(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => console.error(err), }); } delete() { this.productSvc.delete(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Lab6/client/Application/src/app/views/products/list/list.component.html ================================================

Product List

Name {{ element.name }} Price {{ element.price | currency }} SKU {{ element.sku }}
================================================ FILE: Lab6/client/Application/src/app/views/products/list/list.component.scss ================================================ .product-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Lab6/client/Application/src/app/views/products/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { productData: Product[] = []; isLoading: boolean = true; displayedColumns: string[] = ['name', 'price', 'sku']; constructor(private productSvc: ProductService, private router: Router) {} ngOnInit(): void { this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onEdit(product: Product) { this.router.navigate(['products', 'edit', product.productId]); return false; } onRemove(product: Product) { this.productSvc.delete(product); this.isLoading = true; this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onCreate() { this.router.navigate(['products', 'create']); } } ================================================ FILE: Lab6/client/Application/src/app/views/products/models/product.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface Product { key: string; shardId: string; productId: string; name: string; price: number; sku: string; category: string; pictureUrl?: string; } ================================================ FILE: Lab6/client/Application/src/app/views/products/product.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Product } from './models/product.interface'; @Injectable({ providedIn: 'root', }) export class ProductService { constructor(private http: HttpClient) {} baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; fetch(): Observable { return this.http.get(`${this.baseUrl}/products`); } get(productId: string): Observable { const url = `${this.baseUrl}/product/${productId}`; return this.http.get(url); } delete(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.delete(url); } put(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.put(url, product); } post(product: Product) { return this.http.post(`${this.baseUrl}/product`, product); } } ================================================ FILE: Lab6/client/Application/src/app/views/products/products-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'Product List', }, component: ListComponent, }, { path: 'create', data: { title: 'Create new Product', }, component: CreateComponent, }, { path: 'edit/:productId', data: { title: 'Edit Product', }, component: EditComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class ProductsRoutingModule {} ================================================ FILE: Lab6/client/Application/src/app/views/products/products.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ProductsRoutingModule } from './products-routing.module'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [CreateComponent, EditComponent, ListComponent], imports: [ CommonModule, ReactiveFormsModule, ProductsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class ProductsModule {} ================================================ FILE: Lab6/client/Application/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
================================================ FILE: Lab6/client/Application/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Lab6/client/Application/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Lab6/client/Application/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Lab6/client/Application/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Lab6/client/Application/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Lab6/client/Application/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Lab6/client/Application/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Lab6/client/Application/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Lab6/client/Application/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { find, mergeMap, defaultIfEmpty } from 'rxjs/operators'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.regApiGatewayUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } update(email: string, user: User) {} } ================================================ FILE: Lab6/client/Application/src/assets/.gitkeep ================================================ ================================================ FILE: Lab6/client/Application/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab6/client/Application/src/environments/environment.prod.ts ================================================ export const environment = { production: true, regApiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab6/client/Application/src/environments/environment.ts ================================================ export const environment = { production: false, regApiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab6/client/Application/src/index.html ================================================ Application ================================================ FILE: Lab6/client/Application/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab6/client/Application/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab6/client/Application/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab6/client/Application/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab6/client/Application/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "~bootstrap/scss/functions"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/maps"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; .chart-container canvas { max-height: 250px; width: auto; } .chart-container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; } ================================================ FILE: Lab6/client/Application/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab6/client/Application/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab6/client/Application/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "strictPropertyInitialization": false, "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab6/client/Application/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab6/client/Landing/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Lab6/client/Landing/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Lab6/client/Landing/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Lab6/client/Landing/README.md ================================================ # Landing This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Lab6/client/Landing/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "Landing": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/custom-theme.scss", "src/styles.scss", "node_modules/font-awesome/css/font-awesome.min.css" ] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "10kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "Landing:build:production" }, "development": { "browserTarget": "Landing:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "Landing:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Lab6/client/Landing/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Lab6/client/Landing/package.json ================================================ { "name": "landing", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "bootstrap": "~5.1.3", "font-awesome": "~4.7.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Lab6/client/Landing/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; export const routes: Routes = [ { path: '', redirectTo: 'landing', pathMatch: 'full', }, { path: 'landing', component: LandingComponent, }, { path: 'register', component: RegisterComponent, }, { path: '**', redirectTo: 'landing', pathMatch: 'full', }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Lab6/client/Landing/src/app/app.component.scss ================================================ ================================================ FILE: Lab6/client/Landing/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'landing'; } ================================================ FILE: Lab6/client/Landing/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; @NgModule({ declarations: [AppComponent, LandingComponent, RegisterComponent], imports: [ AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, MatButtonModule, MatCardModule, MatIconModule, MatProgressSpinnerModule, MatSidenavModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, FormsModule, HttpClientModule, ReactiveFormsModule, MatSnackBarModule, MatInputModule, MatFormFieldModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Lab6/client/Landing/src/app/views/landing/landing.component.html ================================================

Serverless SaaS Reference Architecture

It's so nice it blows your mind.

Sign up now!

Serverless SaaS Reference Architecture is so awesome.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere temporibus omnis illum, officia. Architecto voluptatibus commodi voluptatem perspiciatis eos possimus, eius at molestias quaerat magnam? Odio qui quos ipsam natus.

Serverless SaaS Reference Architecture so awesome. Makes you awesome - go sign up!

Serverless SaaS Reference Architecture so great. Makes you even greater - go sign up now. Super cheap deal!

Feel lonely? Go sign up and have a friend!

Take Serverless SaaS Reference Architecture with you everywhere you go.

Serverless SaaS Reference Architecture is all you need. Anywhere - ever. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita sapiente hic voluptatum quo sunt totam accusamus distinctio minus aliquid quis!

Love Serverless SaaS Reference Architecture. So nice! So good! Could not live without!

Satisfied Customer

Reasons to sign up this product:

  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!
  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!

Why you still reading?

Sign up now!
================================================ FILE: Lab6/client/Landing/src/app/views/landing/landing.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // $color-red: #cc615f; $color-grey: #4b4b4b; $color-grey--light: #d3d3d3; $btn-amzn: #ec7211; $color-primary: #232f3e; $bp-s: 65.75em; //700px; $bp-xs: 34.375em; //550px; @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,800,300); *{ box-sizing: border-box; } html{ width: 100%; height:100%; margin: 0; padding: 0; } body{ width: 100%; height:100%; font-family: 'Open Sans','Helvetica Neue',Helvetica, sans-serif; font-size: 100%; line-height: 1.45; color: #141414; } a{ text-decoration: none; &:hover{ text-decoration: none; } } img{ max-width: 100%; } .btn{ display: inline-block; margin: 1rem 0; color: white; font-weight: 500; font-size: 1.3rem; background: $btn-amzn; letter-spacing: .02em; border: none; border-radius: 5px; padding: .8rem 1rem .9rem; text-shadow: 0 1px rgba(black,.3); box-shadow: 0 0 2px rgba(black,.2); @media (max-width: $bp-s){ padding: .5rem .7rem .6rem; font-size: 1rem; } &:hover{ background: lighten($btn-amzn,5%); color: #fff; } &:focus, &:active, &:focus:active{ background: darken($btn-amzn,5%); border-color: darken($btn-amzn,5%); box-shadow: 0 2px 5px 0 rgba(black,.5) inset; } } .container{ margin: 0 auto; width: 90%; max-width: 900px; } header{ color: white; background: #232f3e; padding: 10rem 0; text-align: center; position: relative; z-index: 1; overflow: hidden; @media (max-width: $bp-s){ padding: 2rem 0; } h1{ font-size: 3rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 2rem; } } h2{ font-weight: 300; font-size: 1.5rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } } section{ background: #fff; color: $color-grey; padding: 3.5rem 0; @media (max-width: $bp-s){ padding: 2rem 0; } &.section--primary{ background: $color-primary; color: #fff; } &.section--primary--alt{ background: desaturate(lighten($color-primary,15%),10%); color: #fff; } &.section--primary--light{ background: rgba($color-primary,.1); } &.section--grey{ background: $color-grey; color: #fff; } &.section--grey--light{ background: $color-grey--light; color: #fff; } h3{ text-align: center; font-size: 2rem; font-weight: 300; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } li{ font-size: 1.2rem; font-weight: 300; } p{ font-size: 1.2rem; font-weight: 300; } } .col{ margin: 0 1.5%; display: inline-block; vertical-align: top; } .col-7{ @extend .col; width: 64%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-3{ @extend .col; width: 29%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-5{ @extend .col; width: 30%; @media (max-width: $bp-xs){ width: 60%; margin: 0; } } .details{ text-align: left; h3{ font-size: 2rem; text-align: left; } } .features{ text-align: center; padding: 1rem; &:hover{ background: rgba(white,.1); } @media (max-width: $bp-s){ width: 100%; margin: 0; text-align: left; border-bottom: 1px solid rgba(white,.2); &:last-child{ border: none; } } i{ font-size: 4rem; margin: 0 0 2rem 0; @media (max-width: $bp-s){ font-size: 1.5rem; width: 2rem; text-align: center; margin: 0 0 1rem 0; float: left; } } p{ margin: 0 0 1rem 0; font-size: 1rem; @media (max-width: $bp-s){ margin-left: 3rem; } } } blockquote{ position: relative; margin: 0; padding: 0; text-align: center; &:before{ display: inline-block; color: $color-primary; font-size: 2rem; content: '\201C'; } p{ margin: 0; display: inline; font-size: 1.5rem; @media (max-width: $bp-s){ font-size: 1.2rem; } } cite{ font-size: 1rem; display: block; margin: .5rem 0 0 1.2rem; @media (max-width: $bp-s){ font-size: .8rem; } &:before{ content: '–'; } } } footer{ background: $color-primary; color: #fff; padding: 2rem 0; text-align: center; font-size: .8rem; color: rgba(white,.4); ul{ margin: 0; padding: 0; list-style: none; li{ display: inline-block; a{ display: block; padding: .4rem .7rem; font-size: .9rem; text-decoration: none; color: rgba(white,.7); &:hover{ color: white; } } } } } .text--center{ text-align: center; } .text--left{ text-align: left; } .bg-image{ background: $color-primary; text-align: center; position: relative; z-index: 1; overflow: hidden; //text-shadow: 2px 0 5px black; &:before{ content: ''; display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; z-index: -1; background-size: 100%; background-attachment: fixed; filter: blur(3px); opacity: .8; transform: scale(1.1); } &.bg-image-2:before{ opacity: .6; background-position: center center; } } ================================================ FILE: Lab6/client/Landing/src/app/views/landing/landing.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-landing', templateUrl: './landing.component.html', styleUrls: ['./landing.component.scss'], }) export class LandingComponent implements OnInit { constructor(private router: Router) {} ngOnInit() {} register() { this.router.navigate(['register']); return false; } } ================================================ FILE: Lab6/client/Landing/src/app/views/register/register.component.html ================================================
Provision a new Tenant Login details will be sent to the email provided. Name Email Phone Address Plan
================================================ FILE: Lab6/client/Landing/src/app/views/register/register.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .tenant-form { display: flex; align-items: center; justify-content: center; } .wide-form { min-width: 150px; max-width: 500px; width: 100%; } .full-width { width: 100%; } .mat-form-field { width: 100%; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Lab6/client/Landing/src/app/views/register/register.component.ts ================================================ import { MatSnackBar } from '@angular/material/snack-bar'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.scss'], }) export class RegisterComponent implements OnInit { submitting: boolean = false; tenantForm: FormGroup = this.fb.group({ tenantName: ['', [Validators.required]], tenantEmail: ['', [Validators.email, Validators.required]], tenantTier: ['', [Validators.required]], tenantPhone: [''], tenantAddress: [''], }); constructor( private fb: FormBuilder, private _snackBar: MatSnackBar, private http: HttpClient ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; this.tenantForm.disable(); const tenant = { ...this.tenantForm.value, }; this.http .post(`${environment.apiGatewayUrl}/registration`, tenant) .subscribe({ next: () => { this.openErrorMessageSnackBar('Successfully created new tenant!'); this.tenantForm.reset(); this.tenantForm.enable(); this.submitting = false; }, error: (err) => { this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); this.tenantForm.enable(); this.submitting = false; }, }); } } ================================================ FILE: Lab6/client/Landing/src/assets/.gitkeep ================================================ ================================================ FILE: Lab6/client/Landing/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Lab6/client/Landing/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab6/client/Landing/src/environments/environment.ts ================================================ export const environment = { production: false, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Lab6/client/Landing/src/index.html ================================================ Landing ================================================ FILE: Lab6/client/Landing/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Lab6/client/Landing/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Lab6/client/Landing/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Lab6/client/Landing/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Lab6/client/Landing/src/styles.scss ================================================ ================================================ FILE: Lab6/client/Landing/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Lab6/client/Landing/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Lab6/client/Landing/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Lab6/client/Landing/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Lab6/scripts/deployment.sh ================================================ # During AWS hosted events using event engine tool # we pre-provision cloudfront and s3 buckets which hosts UI code. # So that it improves this labs total execution time. # Below code checks if cloudfront and s3 buckets are # pre-provisioned or not and then concludes if the workshop # is running in AWS hosted event through event engine tool or not. IS_RUNNING_IN_EVENT_ENGINE=false PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" IS_RUNNING_IN_EVENT_ENGINE=true ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) fi echo "server code is getting deployed" cd ../server REGION=$(aws configure get region) echo "Validating server code using pylint" python3 -m pylint -E -d E0401 $(find . -iname "*.py" -not -path "./.aws-sam/*" -not -path "./TenantPipeline/node_modules/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi sam build -t shared-template.yaml --use-container if [ "$IS_RUNNING_IN_EVENT_ENGINE" = true ]; then sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE AdminUserPoolCallbackURLParameter=$ADMIN_SITE_URL TenantUserPoolCallbackURLParameter=$APP_SITE_URL else sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE fi echo "Pooled tenant server code is getting deployed" REGION=$(aws configure get region) sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml --region=$REGION cd ../scripts if [ "$IS_RUNNING_IN_EVENT_ENGINE" = false ]; then ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Lab6/scripts/geturl.sh ================================================ PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) else ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Lab6/scripts/test-basic-tier-throttling.sh ================================================ #!/bin/bash APP_APIGATEWAYURL=$(aws cloudformation describe-stacks --stack-name stack-pooled --query "Stacks[0].Outputs[?OutputKey=='TenantAPI'].OutputValue" --output text) get_product() { STATUS_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X GET -H "Authorization: Bearer $1" -H "Content-Type: application/json" $APP_APIGATEWAYURL/products) echo "STATUS_CODE : $STATUS_CODE"; } for i in $(seq 1 1000) do get_product $1 $i & done wait echo "All done" ================================================ FILE: Lab6/server/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Lab6/server/OrderService/order_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Order: key='' def __init__(self, shardId, orderId, orderName, orderProducts): self.shardId = shardId self.orderId = orderId self.key = shardId + ':' + orderId self.orderName = orderName self.orderProducts = orderProducts class OrderProduct: def __init__(self, productId, price, quantity): self.productId = productId self.price = price self.quantity = quantity ================================================ FILE: Lab6/server/OrderService/order_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import order_service_dal from decimal import Decimal from types import SimpleNamespace from aws_lambda_powertools import Tracer tracer = Tracer() @tracer.capture_lambda_handler def get_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a order") params = event['pathParameters'] key = params['id'] logger.log_with_tenant_context(event, params) order = order_service_dal.get_order(event, key) logger.log_with_tenant_context(event, "Request completed to get a order") metrics_manager.record_metric(event, "SingleOrderRequested", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def create_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) order = order_service_dal.create_order(event, payload) logger.log_with_tenant_context(event, "Request completed to create a order") metrics_manager.record_metric(event, "OrderCreated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def update_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] order = order_service_dal.update_order(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a order") metrics_manager.record_metric(event, "OrderUpdated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def delete_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a order") params = event['pathParameters'] key = params['id'] response = order_service_dal.delete_order(event, key) logger.log_with_tenant_context(event, "Request completed to delete a order") metrics_manager.record_metric(event, "OrderDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the order") @tracer.capture_lambda_handler def get_orders(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all orders") response = order_service_dal.get_orders(event, tenantId) metrics_manager.record_metric(event, "OrdersRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all orders") return utils.generate_response(response) ================================================ FILE: Lab6/server/OrderService/order_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid from order_models import Order import json import utils import metrics_manager from types import SimpleNamespace import logger import random import threading from boto3.dynamodb.conditions import Key is_pooled_deploy = os.environ['IS_POOLED_DEPLOY'] table_name = os.environ['ORDER_TABLE_NAME'] dynamodb = None suffix_start = 1 suffix_end = 10 def get_order(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) response = table.get_item(Key={'shardId': shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a order', e) else: return order def delete_order(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a order', e) else: logger.info("DeleteItem succeeded:") return response def create_order(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] table = __get_dynamodb_table(event, dynamodb) suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) order = Order(shardId, str(uuid.uuid4()), payload.orderName, payload.orderProducts) try: response = table.put_item(Item={ 'shardId':shardId, 'orderId': order.orderId, 'orderName': order.orderName, 'orderProducts': get_order_products_dict(order.orderProducts) }, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a order', e) else: logger.info("PutItem succeeded:") return order def update_order(event, payload, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) order = Order(shardId, orderId,payload.orderName, payload.orderProducts) response = table.update_item(Key={'shardId':order.shardId, 'orderId': order.orderId}, UpdateExpression="set orderName=:orderName, " +"orderProducts=:orderProducts", ExpressionAttributeValues={ ':orderName': order.orderName, ':orderProducts': get_order_products_dict(order.orderProducts) }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a order', e) else: logger.info("UpdateItem succeeded:") return order def get_orders(event, tenantId): table = __get_dynamodb_table(event, dynamodb) get_all_products_response = [] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error("Error getting all orders") raise Exception('Error getting all orders', e) else: logger.info("Get orders succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) get_all_products_response.append(order) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def __get_dynamodb_table(event, dynamodb): """ Determine the table name based upo pooled vs silo model Args: event ([type]): [description] Returns: [type]: [description] """ if (is_pooled_deploy=='true'): accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken ) else: if not dynamodb: dynamodb = boto3.resource('dynamodb') return dynamodb.Table(table_name) def get_order_products_dict(orderProducts): orderProductList = [] for i in range(len(orderProducts)): product = orderProducts[i] orderProductList.append(vars(product)) return orderProductList ================================================ FILE: Lab6/server/OrderService/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Lab6/server/ProductService/product_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Product: key ='' def __init__(self, shardId, productId, sku, name, price, category): self.shardId = shardId self.productId = productId self.key = shardId + ':' + productId self.sku = sku self.name = name self.price = price self.category = category class Category: def __init__(self, id, name): self.id = id self.name = name ================================================ FILE: Lab6/server/ProductService/product_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import product_service_dal from aws_lambda_powertools import Tracer from decimal import Decimal from types import SimpleNamespace tracer = Tracer() @tracer.capture_lambda_handler def get_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a product") params = event['pathParameters'] logger.log_with_tenant_context(event, params) key = params['id'] logger.log_with_tenant_context(event, key) product = product_service_dal.get_product(event, key) logger.log_with_tenant_context(event, "Request completed to get a product") metrics_manager.record_metric(event, "SingleProductRequested", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def create_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) product = product_service_dal.create_product(event, payload) logger.log_with_tenant_context(event, "Request completed to create a product") metrics_manager.record_metric(event, "ProductCreated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def update_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] product = product_service_dal.update_product(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a product") metrics_manager.record_metric(event, "ProductUpdated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def delete_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a product") params = event['pathParameters'] key = params['id'] response = product_service_dal.delete_product(event, key) logger.log_with_tenant_context(event, "Request completed to delete a product") metrics_manager.record_metric(event, "ProductDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the product") @tracer.capture_lambda_handler def get_products(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all products") response = product_service_dal.get_products(event, tenantId) metrics_manager.record_metric(event, "ProductsRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all products") return utils.generate_response(response) ================================================ FILE: Lab6/server/ProductService/product_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid import json import logger import metrics_manager import random import threading from product_models import Product from types import SimpleNamespace from boto3.dynamodb.conditions import Key is_pooled_deploy = os.environ['IS_POOLED_DEPLOY'] table_name = os.environ['PRODUCT_TABLE_NAME'] dynamodb = None suffix_start = 1 suffix_end = 10 def get_product(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) response = table.get_item(Key={'shardId': shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a product', e) else: logger.info("GetItem succeeded:"+ str(product)) return product def delete_product(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a product', e) else: logger.info("DeleteItem succeeded:") return response def create_product(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] table = __get_dynamodb_table(event, dynamodb) suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) product = Product(shardId, str(uuid.uuid4()), payload.sku,payload.name, payload.price, payload.category) try: response = table.put_item( Item= { 'shardId': shardId, 'productId': product.productId, 'sku': product.sku, 'name': product.name, 'price': product.price, 'category': product.category }, ReturnConsumedCapacity='TOTAL' ) metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: logger.info("PutItem succeeded:") return product def update_product(event, payload, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) product = Product(shardId,productId,payload.sku, payload.name, payload.price, payload.category) response = table.update_item(Key={'shardId':product.shardId, 'productId': product.productId}, UpdateExpression="set sku=:sku, #n=:productName, price=:price, category=:category", ExpressionAttributeNames= {'#n':'name'}, ExpressionAttributeValues={ ':sku': product.sku, ':productName': product.name, ':price': product.price, ':category': product.category }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a product', e) else: logger.info("UpdateItem succeeded:") return product def get_products(event, tenantId): table = __get_dynamodb_table(event, dynamodb) get_all_products_response =[] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting all products', e) else: logger.info("Get products succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) get_all_products_response.append(product) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def __get_dynamodb_table(event, dynamodb): if (is_pooled_deploy=='true'): accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken ) else: if not dynamodb: dynamodb = boto3.resource('dynamodb') return dynamodb.Table(table_name) ================================================ FILE: Lab6/server/ProductService/requirements.txt ================================================ requests pytest-mock aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Lab6/server/README.md ================================================ sam build -t shared-template.yaml --use-container sam deploy --config-file shared-samconfig.toml sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml ================================================ FILE: Lab6/server/Resources/requirements.txt ================================================ python-jose[cryptography] ================================================ FILE: Lab6/server/Resources/shared_service_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import auth_manager import utils region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] api_key_operation_user = os.environ['OPERATION_USERS_API_KEY'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) if(auth_manager.isSaaSProvider(unauthorized_claims['custom:userRole'])): userpool_id = user_pool_operation_user appclient_id = app_client_operation_user api_key = api_key_operation_user else: #get tenant user pool and app client to validate jwt token against tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': unauthorized_claims['custom:tenantId'] } ) logger.info(tenant_details) userpool_id = tenant_details['Item']['userPoolId'] appclient_id = tenant_details['Item']['appClientId'] api_key = tenant_details['Item']['apiKey'] #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] #only tenant admin and system admin can do certain actions like create and disable users if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): policy.allowAllMethods() if (auth_manager.isTenantAdmin(user_role)): policy.denyMethod(HttpVerb.POST, "tenant-activation") policy.denyMethod(HttpVerb.GET, "tenants") else: #if not tenant admin or system admin then only allow to get info and update info policy.allowMethod(HttpVerb.GET, "user/*") policy.allowMethod(HttpVerb.PUT, "user/*") authResponse = policy.build() # Generate STS credentials to be used for FGAC # Important Note: # We are generating STS token inside Authorizer to take advantage of the caching behavior of authorizer # Another option is to generate the STS token inside the lambda function itself, as mentioned in this blog post: https://aws.amazon.com/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/ # Finally, you can also consider creating one Authorizer per microservice in cases where you want the IAM policy specific to that service iam_policy = auth_manager.getPolicyForUser(user_role, utils.Service_Identifier.SHARED_SERVICES.value, tenant_id, region, aws_account_id) logger.info(iam_policy) role_arn = "arn:aws:iam::{}:role/authorizer-access-role".format(aws_account_id) assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName="tenant-aware-session", Policy=iam_policy, ) credentials = assumed_role["Credentials"] #pass sts credentials to lambda context = { 'accesskey': credentials['AccessKeyId'], # $context.authorizer.key -> value 'secretkey' : credentials['SecretAccessKey'], 'sessiontoken' : credentials["SessionToken"], 'userName': user_name, 'tenantId': tenant_id, 'userPoolId': userpool_id, 'apiKey': api_key, 'userRole': user_role } authResponse['context'] = context authResponse['usageIdentifierKey'] = api_key return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Lab6/server/Resources/tenant_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import auth_manager import utils region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] api_key_operation_user = os.environ['OPERATION_USERS_API_KEY'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) if(auth_manager.isSaaSProvider(unauthorized_claims['custom:userRole'])): userpool_id = user_pool_operation_user appclient_id = app_client_operation_user api_key = api_key_operation_user else: #get tenant user pool and app client to validate jwt token against tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': unauthorized_claims['custom:tenantId'] } ) logger.info(tenant_details) userpool_id = tenant_details['Item']['userPoolId'] appclient_id = tenant_details['Item']['appClientId'] apigateway_url = tenant_details['Item']['apiGatewayUrl'] #TODO: Get API Key from tenant management table #api_key = tenant_details['Item']['apiKey'] #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] if (auth_manager.isSaaSProvider(user_role) == False): if (isTenantAuthorizedForThisAPI(apigateway_url, api_gateway_arn_tmp[0]) == False): logger.error('Unauthorized') raise Exception('Unauthorized') #roles are not fine-grained enough to allow selectively policy.allowAllMethods() authResponse = policy.build() # Generate STS credentials to be used for FGAC # Important Note: # We are generating STS token inside Authorizer to take advantage of the caching behavior of authorizer # Another option is to generate the STS token inside the lambda function itself, as mentioned in this blog post: https://aws.amazon.com/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/ # Finally, you can also consider creating one Authorizer per microservice in cases where you want the IAM policy specific to that service iam_policy = auth_manager.getPolicyForUser(user_role, utils.Service_Identifier.BUSINESS_SERVICES.value, tenant_id, region, aws_account_id) logger.info(iam_policy) role_arn = "arn:aws:iam::{}:role/authorizer-access-role".format(aws_account_id) assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName="tenant-aware-session", Policy=iam_policy, ) credentials = assumed_role["Credentials"] #pass sts credentials to lambda context = { 'accesskey': credentials['AccessKeyId'], # $context.authorizer.key -> value 'secretkey' : credentials['SecretAccessKey'], 'sessiontoken' : credentials["SessionToken"], 'userName': user_name, 'tenantId': tenant_id, 'userPoolId': userpool_id, #TODO: Assign API Key to authorizer response #'apiKey': api_key, 'userRole': user_role } authResponse['context'] = context authResponse['usageIdentifierKey'] = api_key return authResponse def isTenantAuthorizedForThisAPI(apigateway_url, current_api_id): if(apigateway_url.split('.')[0] != 'https://' + current_api_id): return False else: return True def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Lab6/server/TenantManagementService/requirements.txt ================================================ requests aws_requests_auth ================================================ FILE: Lab6/server/TenantManagementService/tenant-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import json import boto3 from boto3.dynamodb.conditions import Key import urllib.parse import utils from botocore.exceptions import ClientError import logger import metrics_manager import auth_manager import requests from aws_requests_auth.aws_auth import AWSRequestsAuth from aws_lambda_powertools import Tracer tracer = Tracer() region = os.environ['AWS_REGION'] #This method has been locked down to be only def create_tenant(event, context): api_gateway_url = '' tenant_details = json.loads(event['body']) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails')#TODO: read table names from env vars table_system_settings = dynamodb.Table('ServerlessSaaS-Settings') try: # for pooled tenants the apigateway url is saving in settings during stack creation # update from there during tenant creation if(tenant_details['dedicatedTenancy'].lower()!= 'true'): settings_response = table_system_settings.get_item( Key={ 'settingName': 'apiGatewayUrl-Pooled' } ) api_gateway_url = settings_response['Item']['settingValue'] #TODO: Save API Key inside the table** response = table_tenant_details.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'tenantName' : tenant_details['tenantName'], 'tenantAddress': tenant_details['tenantAddress'], 'tenantEmail': tenant_details['tenantEmail'], 'tenantPhone': tenant_details['tenantPhone'], 'tenantTier': tenant_details['tenantTier'], #'apiKey': tenant_details['apiKey'], 'userPoolId': tenant_details['userPoolId'], 'appClientId': tenant_details['appClientId'], 'dedicatedTenancy': tenant_details['dedicatedTenancy'], 'isActive': True, 'apiGatewayUrl': api_gateway_url } ) except Exception as e: raise Exception('Error creating a new tenant', e) else: return utils.create_success_response("Tenant Created") def get_tenants(event, context): table_tenant_details = __getTenantManagementTable(event) try: response = table_tenant_details.scan() except Exception as e: raise Exception('Error getting all tenants', e) else: return utils.generate_response(response['Items']) @tracer.capture_lambda_handler def update_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_details = json.loads(event['body']) tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): exiting_tenant_details = table_tenant_details.get_item( Key={ 'tenantId': tenant_id, } ) if (exiting_tenant_details['Item']['tenantTier'].upper() != tenant_details['tenantTier'].upper()): api_key = __getApiKey(tenant_details['tenantTier']) else: api_key = exiting_tenant_details['Item']['apiKey'] response_update = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set tenantName = :tenantName, tenantAddress = :tenantAddress, tenantEmail = :tenantEmail, tenantPhone = :tenantPhone, tenantTier=:tenantTier, apiKey=:apiKey", ExpressionAttributeValues={ ':tenantName' : tenant_details['tenantName'], ':tenantAddress': tenant_details['tenantAddress'], ':tenantEmail': tenant_details['tenantEmail'], ':tenantPhone': tenant_details['tenantPhone'], ':tenantTier': tenant_details['tenantTier'], ':apiKey': api_key }, ReturnValues="UPDATED_NEW" ) logger.log_with_tenant_context(event, response_update) logger.log_with_tenant_context(event, "Request completed to update tenant") return utils.create_success_response("Tenant Updated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get tenant details") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): tenant_details = table_tenant_details.get_item( Key={ 'tenantId': tenant_id, }, AttributesToGet=[ 'tenantName', 'tenantAddress', 'tenantEmail', 'tenantPhone' ] ) item = tenant_details['Item'] tenant_info = TenantInfo(item['tenantName'], item['tenantAddress'],item['tenantEmail'], item['tenantPhone']) logger.log_with_tenant_context(event, tenant_info) logger.log_with_tenant_context(event, "Request completed to get tenant details") return utils.create_success_response(tenant_info.__dict__) else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def deactivate_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) url_disable_users = os.environ['DISABLE_USERS_BY_TENANT'] url_deprovision_tenant = os.environ['DEPROVISION_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to deactivate tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': False }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) if (response["Attributes"]["dedicatedTenancy"].upper() == "TRUE"): update_details = {} update_details['tenantId'] = tenant_id update_user_response = __invoke_deprovision_tenant(update_details, headers, auth, host, stage_name, url_deprovision_tenant) update_details = {} update_details['userPoolId'] = response["Attributes"]['userPoolId'] update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_disable_users(update_details, headers, auth, host, stage_name, url_disable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to deactivate tenant") return utils.create_success_response("Tenant Deactivated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def activate_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) url_enable_users = os.environ['ENABLE_USERS_BY_TENANT'] url_provision_tenant = os.environ['PROVISION_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to activate tenant") if (auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': True }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) if (response["Attributes"]["dedicatedTenancy"].upper() == "TRUE"): update_details = {} update_details['tenantId'] = tenant_id provision_response = __invoke_provision_tenant(update_details, headers, auth, host, stage_name, url_provision_tenant) logger.log_with_tenant_context(event, provision_response) update_details = {} update_details['userPoolId'] = response["Attributes"]['userPoolId'] update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_enable_users(update_details, headers, auth, host, stage_name, url_enable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to activate tenant") return utils.create_success_response("Tenant Activated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only system admin can activate tenant!") return utils.create_unauthorized_response() def load_tenant_config(event, context): params = event['pathParameters'] tenantName = urllib.parse.unquote(params['tenantname']) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails')#TODO: read table names from env vars try: response = table_tenant_details.query( IndexName="ServerlessSaas-TenantConfig", KeyConditionExpression=Key('tenantName').eq(tenantName), ProjectionExpression="userPoolId, appClientId, apiGatewayUrl" ) except Exception as e: raise Exception('Error getting tenant config', e) else: if (response['Count'] == 0): return utils.create_notfound_response("Tenant not found."+ "Please enter exact tenant name used during tenant registration.") else: return utils.generate_response(response['Items'][0]) def __invoke_disable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url]) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while disabling users for the tenant') except Exception as e: logger.error('Error occured while disabling users for the tenant') raise Exception('Error occured while disabling users for the tenant', e) else: return "Success invoking disable users" def __invoke_deprovision_tenant(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url + update_details['tenantId']]) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while deprovisioning tenant') except Exception as e: logger.error('Error occured while deprovisioning tenant') raise Exception('Error occured while deprovisioning tenant', e) else: return "Success invoking deprovision tenant" def __invoke_enable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url]) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while enabling users for the tenant') except Exception as e: logger.error('Error occured while enabling users for the tenant') raise Exception('Error occured while enabling users for the tenant', e) else: return "Success invoking enable users" def __invoke_provision_tenant(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url]) response = requests.post(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while provisioning tenant') except Exception as e: logger.error('Error occured while provisioning tenant') raise Exception('Error occured while provisioning tenant', e) else: return "Success invoking provision tenant" def __getApiKey(tenant_tier): if (tenant_tier.upper() == utils.TenantTier.PLATINUM.value.upper()): return os.environ['PLATINUM_TIER_API_KEY'] elif (tenant_tier.upper() == utils.TenantTier.PREMIUM.value.upper()): return os.environ['PREMIUM_TIER_API_KEY'] elif (tenant_tier.upper() == utils.TenantTier.STANDARD.value.upper()): return os.environ['STANDARD_TIER_API_KEY'] elif (tenant_tier.upper() == utils.TenantTier.BASIC.value.upper()): return os.environ['BASIC_TIER_API_KEY'] def __getTenantManagementTable(event): accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken) table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails')#TODO: read table names from env vars return table_tenant_details class TenantInfo: def __init__(self, tenant_name, tenant_address, tenant_email, tenant_phone): self.tenant_name = tenant_name self.tenant_address = tenant_address self.tenant_email = tenant_email self.tenant_phone = tenant_phone ================================================ FILE: Lab6/server/TenantManagementService/tenant-provisioning.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import utils from botocore.exceptions import ClientError import logger import os from aws_lambda_powertools import Tracer tracer = Tracer() tenant_stack_mapping_table_name = os.environ['TENANT_STACK_MAPPING_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') codepipeline = boto3.client('codepipeline') cloudformation = boto3.client('cloudformation') table_tenant_stack_mapping = dynamodb.Table(tenant_stack_mapping_table_name) stack_name = 'stack-{0}' @tracer.capture_lambda_handler def provision_tenant(event, context): tenant_details = json.loads(event['body']) try: response_ddb = table_tenant_stack_mapping.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'stackName': stack_name.format(tenant_details['tenantId']), 'applyLatestRelease': True, 'codeCommitId': '' } ) logger.info(response_ddb) response_codepipeline = codepipeline.start_pipeline_execution( name='serverless-saas-pipeline' ) logger.info(response_ddb) except Exception as e: raise else: return utils.create_success_response("Tenant Provisioning Started") @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def deprovision_tenant(event, context): logger.info("Request received to deprovision a tenant") tenantid_to_deprovision = event['tenantId'] try: response_ddb = table_tenant_stack_mapping.delete_item( Key={ 'tenantId': tenantid_to_deprovision } ) logger.info(response_ddb) response_cloudformation = cloudformation.delete_stack( StackName=stack_name.format(tenantid_to_deprovision) ) logger.info(response_cloudformation) except Exception as e: raise else: return utils.create_success_response("Tenant Deprovisioning Started") ================================================ FILE: Lab6/server/TenantManagementService/tenant-registration.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import utils import uuid import logger import requests import re region = os.environ['AWS_REGION'] create_tenant_admin_user_resource_path = os.environ['CREATE_TENANT_ADMIN_USER_RESOURCE_PATH'] create_tenant_resource_path = os.environ['CREATE_TENANT_RESOURCE_PATH'] provision_tenant_resource_path = os.environ['PROVISION_TENANT_RESOURCE_PATH'] platinum_tier_api_key = os.environ['PLATINUM_TIER_API_KEY'] premium_tier_api_key = os.environ['PREMIUM_TIER_API_KEY'] standard_tier_api_key = os.environ['STANDARD_TIER_API_KEY'] basic_tier_api_key = os.environ['BASIC_TIER_API_KEY'] lambda_client = boto3.client('lambda') def register_tenant(event, context): try: api_key='' tenant_id = uuid.uuid1().hex tenant_details = json.loads(event['body']) tenant_details['dedicatedTenancy'] = 'false' #TODO: Pass relevant apikey to tenant_details object based upon tenant tier if (tenant_details['tenantTier'].upper() == utils.TenantTier.PLATINUM.value.upper()): tenant_details['dedicatedTenancy'] = 'true' tenant_details['tenantId'] = tenant_id logger.info(tenant_details) stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) create_user_response = __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name) logger.info (create_user_response) tenant_details['userPoolId'] = create_user_response['message']['userPoolId'] tenant_details['appClientId'] = create_user_response['message']['appClientId'] tenant_details['tenantAdminUserName'] = create_user_response['message']['tenantAdminUserName'] create_tenant_response = __create_tenant(tenant_details, headers, auth, host, stage_name) logger.info (create_tenant_response) if (tenant_details['dedicatedTenancy'].upper() == 'TRUE'): provision_tenant_response = __provision_tenant(tenant_details, headers, auth, host, stage_name) logger.info(provision_tenant_response) except Exception as e: logger.error('Error registering a new tenant') raise Exception('Error registering a new tenant', e) else: return utils.create_success_response("You have been registered in our system") def __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_admin_user_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while calling the create tenant admin user service') raise Exception('Error occured while calling the create tenant admin user service', e) else: return response_json def __create_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_resource_path]) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while creating the tenant record in table') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json def __provision_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, provision_tenant_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json()['message'] except Exception as e: logger.error('Error occured while provisioning the tenant') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json ================================================ FILE: Lab6/server/TenantManagementService/user-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import sys import logger import utils import metrics_manager import auth_manager from boto3.dynamodb.conditions import Key from aws_lambda_powertools import Tracer tracer = Tracer() client = boto3.client('cognito-idp') dynamodb = boto3.resource('dynamodb') table_tenant_user_map = dynamodb.Table('ServerlessSaaS-TenantUserMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') def create_tenant_admin_user(event, context): tenant_user_pool_id = os.environ['TENANT_USER_POOL_ID'] tenant_app_client_id = os.environ['TENANT_APP_CLIENT_ID'] tenant_details = json.loads(event['body']) tenant_id = tenant_details['tenantId'] logger.info(tenant_details) user_mgmt = UserManagement() if (tenant_details['dedicatedTenancy'] == 'true'): user_pool_response = user_mgmt.create_user_pool(tenant_id) user_pool_id = user_pool_response['UserPool']['Id'] logger.info (user_pool_id) app_client_response = user_mgmt.create_user_pool_client(user_pool_id) logger.info(app_client_response) app_client_id = app_client_response['UserPoolClient']['ClientId'] user_pool_domain_response = user_mgmt.create_user_pool_domain(user_pool_id, tenant_id) logger.info ("New Tenant Created") else: user_pool_id = tenant_user_pool_id app_client_id = tenant_app_client_id #Add tenant admin now based upon user pool tenant_user_group_response = user_mgmt.create_user_group(user_pool_id,tenant_id,"User group for tenant {0}".format(tenant_id)) tenant_admin_user_name = 'tenant-admin-{0}'.format(tenant_details['tenantId']) create_tenant_admin_response = user_mgmt.create_tenant_admin(user_pool_id, tenant_admin_user_name, tenant_details) add_tenant_admin_to_group_response = user_mgmt.add_user_to_group(user_pool_id, tenant_admin_user_name, tenant_user_group_response['Group']['GroupName']) tenant_user_mapping_response = user_mgmt.create_user_tenant_mapping(tenant_admin_user_name,tenant_id) response = {"userPoolId": user_pool_id, "appClientId": app_client_id, "tenantAdminUserName": tenant_admin_user_name} return utils.create_success_response(response) @tracer.capture_lambda_handler #only tenant admin can create users def create_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to create new user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = user_details['tenantId'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] else: user_tenant_id = tenant_id if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): metrics_manager.record_metric(event, "UserCreated", "Count", 1) response = client.admin_create_user( Username=user_details['userName'], UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] }, { 'Name': 'custom:tenantId', 'Value': user_tenant_id } ] ) logger.log_with_tenant_context(event, response) user_mgmt = UserManagement() user_mgmt.add_user_to_group(user_pool_id, user_details['userName'], user_tenant_id) response_mapping = user_mgmt.create_user_tenant_mapping(user_details['userName'], user_tenant_id) logger.log_with_tenant_context(event, "Request completed to create new user") return utils.create_success_response("New user created") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can create user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_users(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] users = [] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update user") if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): response = client.list_users( UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) num_of_users = len(response['Users']) metrics_manager.record_metric(event, "Number of users", "Count", num_of_users) if (num_of_users > 0): for user in response['Users']: is_same_tenant_user = False user_info = UserInfo() for attr in user["Attributes"]: if(attr["Name"] == "custom:tenantId" and attr["Value"] == tenant_id): is_same_tenant_user = True user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] if(is_same_tenant_user): user_info.enabled = user["Enabled"] user_info.created = user["UserCreateDate"] user_info.modified = user["UserLastModifiedDate"] user_info.status = user["UserStatus"] user_info.user_name = user["Username"] users.append(user_info) return utils.generate_response(users) else: logger.log_with_tenant_context(event, "Request completed as unauthorized.") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = event['queryStringParameters']['tenantid'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] if (auth_manager.isTenantUser(user_role) and user_name != requesting_user_name): logger.log_with_tenant_context(event, "Request completed as unauthorized. User can only get its information.") return utils.create_unauthorized_response() else: user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: logger.log_with_tenant_context(event, "Request completed to get new user") return utils.create_success_response(user_info.__dict__) @tracer.capture_lambda_handler def update_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = user_details['tenantId'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] if (auth_manager.isTenantUser(user_role)): logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update user!") return utils.create_unauthorized_response() else: user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserUpdated", "Count", 1) response = client.admin_update_user_attributes( Username=user_name, UserPoolId=user_pool_id, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] } ] ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to update user") return utils.create_success_response("user updated") @tracer.capture_lambda_handler def disable_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to disable new user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = event['queryStringParameters']['tenantid'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserDisabled", "Count", 1) response = client.admin_disable_user( Username=user_name, UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to disable new user") return utils.create_success_response("User disabled") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can disable user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def disable_users_by_tenant(event, context): logger.info("Request received to disable users by tenant") tenantid_to_update = event['tenantId'] tenant_user_pool_id = event['userPoolId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if ((auth_manager.isTenantAdmin(user_role) and tenantid_to_update == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_disable_user( Username=user['userName'], UserPoolId=tenant_user_pool_id ) logger.info(response) logger.info("Request completed to disable users") return utils.create_success_response("Users disabled") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def enable_users_by_tenant(event, context): logger.info("Request received to enable users by tenant") tenantid_to_update = event['tenantId'] tenant_user_pool_id = event['userPoolId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if (auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_enable_user( Username=user['userName'], UserPoolId=tenant_user_pool_id ) logger.info(response) logger.info("Request completed to enable users") return utils.create_success_response("Users enables") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() def get_user_info(event, user_pool_id, user_name): metrics_manager.record_metric(event, "UserInfoRequested", "Count", 1) response = client.admin_get_user( UserPoolId=user_pool_id, Username=user_name ) logger.log_with_tenant_context(event, response) user_info = UserInfo() user_info.user_name = response["Username"] for attr in response["UserAttributes"]: if(attr["Name"] == "custom:tenantId"): user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] logger.log_with_tenant_context(event, user_info) return user_info class UserManagement: def create_user_pool(self, tenant_id): application_site_url = os.environ['TENANT_USER_POOL_CALLBACK_URL'] email_message = ''.join(["Login into tenant UI application at ", application_site_url, " with username {username} and temporary password {####}"]) email_subject = "Your temporary password for tenant UI application" response = client.create_user_pool( PoolName= tenant_id + '-ServerlessSaaSUserPool', AutoVerifiedAttributes=['email'], AccountRecoverySetting={ 'RecoveryMechanisms': [ { 'Priority': 1, 'Name': 'verified_email' }, ] }, Schema=[ { 'Name': 'email', 'AttributeDataType': 'String', 'Required': True, }, { 'Name': 'tenantId', 'AttributeDataType': 'String', 'Required': False, }, { 'Name': 'userRole', 'AttributeDataType': 'String', 'Required': False, } ], AdminCreateUserConfig={ 'InviteMessageTemplate': { 'EmailMessage': email_message, 'EmailSubject': email_subject } } ) return response def create_user_pool_client(self, user_pool_id): user_pool_callback_url = os.environ['TENANT_USER_POOL_CALLBACK_URL'] response = client.create_user_pool_client( UserPoolId= user_pool_id, ClientName= 'ServerlessSaaSClient', GenerateSecret= False, AllowedOAuthFlowsUserPoolClient= True, AllowedOAuthFlows=[ 'code', 'implicit' ], SupportedIdentityProviders=[ 'COGNITO', ], CallbackURLs=[ user_pool_callback_url, ], LogoutURLs= [ user_pool_callback_url, ], AllowedOAuthScopes=[ 'email', 'openid', 'profile' ], WriteAttributes=[ 'email', 'custom:tenantId' ] ) return response def create_user_pool_domain(self, user_pool_id, tenant_id): response = client.create_user_pool_domain( Domain= tenant_id + '-serverlesssaas', UserPoolId=user_pool_id ) return response def create_user_group(self, user_pool_id, group_name, group_description): response = client.create_group( GroupName=group_name, UserPoolId=user_pool_id, Description= group_description, Precedence=0 ) return response def create_tenant_admin(self, user_pool_id, tenant_admin_user_name, user_details): response = client.admin_create_user( Username=tenant_admin_user_name, UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['tenantEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': 'TenantAdmin' }, { 'Name': 'custom:tenantId', 'Value': user_details['tenantId'] } ] ) return response def add_user_to_group(self, user_pool_id, user_name, group_name): response = client.admin_add_user_to_group( UserPoolId=user_pool_id, Username=user_name, GroupName=group_name ) return response def create_user_tenant_mapping(self, user_name, tenant_id): response = table_tenant_user_map.put_item( Item={ 'tenantId': tenant_id, 'userName': user_name } ) return response class UserInfo: def __init__(self, user_name=None, tenant_id=None, user_role=None, email=None, status=None, enabled=None, created=None, modified=None): self.user_name = user_name self.tenant_id = tenant_id self.user_role = user_role self.email = email self.status = status self.enabled = enabled self.created = created self.modified = modified ================================================ FILE: Lab6/server/TenantPipeline/.gitignore ================================================ *.js !jest.config.js *.d.ts node_modules # CDK asset staging directory .cdk.staging cdk.out # Parcel default cache directory .parcel-cache ================================================ FILE: Lab6/server/TenantPipeline/.npmignore ================================================ *.ts !*.d.ts # CDK asset staging directory .cdk.staging cdk.out ================================================ FILE: Lab6/server/TenantPipeline/README.md ================================================ # Welcome to your CDK TypeScript project! This is a blank project for TypeScript development with CDK. The `cdk.json` file tells the CDK Toolkit how to execute your app. ## Useful commands * `npm run build` compile typescript to js * `npm run watch` watch for changes and compile * `npm run test` perform the jest unit tests * `cdk deploy` deploy this stack to your default AWS account/region * `cdk diff` compare deployed stack with current state * `cdk synth` emits the synthesized CloudFormation template ================================================ FILE: Lab6/server/TenantPipeline/bin/pipeline.ts ================================================ #!/usr/bin/env node import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; import { ServerlessSaaSStack } from '../lib/serverless-saas-stack'; const app = new cdk.App(); new ServerlessSaaSStack(app, 'serverless-saas-pipeline'); ================================================ FILE: Lab6/server/TenantPipeline/cdk.json ================================================ { "app": "npx ts-node bin/pipeline.ts", "context": {} } ================================================ FILE: Lab6/server/TenantPipeline/jest.config.js ================================================ module.exports = { roots: ['/test'], testMatch: ['**/*.test.ts'], transform: { '^.+\\.tsx?$': 'ts-jest' } }; ================================================ FILE: Lab6/server/TenantPipeline/lib/serverless-saas-stack.ts ================================================ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 import { Construct } from 'constructs'; import * as cdk from 'aws-cdk-lib'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as codecommit from 'aws-cdk-lib/aws-codecommit'; import * as codepipeline from 'aws-cdk-lib/aws-codepipeline'; import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions'; import * as codebuild from 'aws-cdk-lib/aws-codebuild'; import { Function, Runtime, AssetCode } from 'aws-cdk-lib/aws-lambda'; import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Duration } from 'aws-cdk-lib'; export class ServerlessSaaSStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const artifactsBucket = new s3.Bucket(this, "ArtifactsBucket", { encryption: s3.BucketEncryption.S3_MANAGED, }); //Since this lambda is invoking cloudformation which is inturn deploying AWS resources, we are giving overly permissive permissions to this lambda. //You can limit this based upon your use case and AWS Resources you need to deploy. const lambdaPolicy = new PolicyStatement() lambdaPolicy.addActions("*") lambdaPolicy.addResources("*") const lambdaFunction = new Function(this, "deploy-tenant-stack", { handler: "lambda-deploy-tenant-stack.lambda_handler", runtime: Runtime.PYTHON_3_9, code: new AssetCode(`./resources`), memorySize: 512, timeout: Duration.seconds(10), environment: { BUCKET: artifactsBucket.bucketName, }, initialPolicy: [lambdaPolicy], }) // Pipeline creation starts const pipeline = new codepipeline.Pipeline(this, 'Pipeline', { pipelineName: 'serverless-saas-pipeline', artifactBucket: artifactsBucket }); // Import existing CodeCommit sam-app repository const codeRepo = codecommit.Repository.fromRepositoryName( this, 'AppRepository', 'aws-serverless-saas-workshop' ); // Declare source code as an artifact const sourceOutput = new codepipeline.Artifact(); // Add source stage to pipeline pipeline.addStage({ stageName: 'Source', actions: [ new codepipeline_actions.CodeCommitSourceAction({ actionName: 'CodeCommit_Source', repository: codeRepo, branch: 'main', output: sourceOutput, variablesNamespace: 'SourceVariables' }), ], }); // Declare build output as artifacts const buildOutput = new codepipeline.Artifact(); //Declare a new CodeBuild project const buildProject = new codebuild.PipelineProject(this, 'Build', { buildSpec : codebuild.BuildSpec.fromSourceFilename("Lab6/server/tenant-buildspec.yml"), environment: { buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_4 }, environmentVariables: { 'PACKAGE_BUCKET': { value: artifactsBucket.bucketName } } }); // Add the build stage to our pipeline pipeline.addStage({ stageName: 'Build', actions: [ new codepipeline_actions.CodeBuildAction({ actionName: 'Build-Serverless-SaaS', project: buildProject, input: sourceOutput, outputs: [buildOutput], }), ], }); const deployOutput = new codepipeline.Artifact(); //Add the Lambda function that will deploy the tenant stack in a multitenant way pipeline.addStage({ stageName: 'Deploy', actions: [ new codepipeline_actions.LambdaInvokeAction({ actionName: 'DeployTenantStack', lambda: lambdaFunction, inputs: [buildOutput], outputs: [deployOutput], userParameters: { 'artifact': 'Artifact_Build_Build-Serverless-SaaS', 'template_file': 'packaged.yaml', 'commit_id': '#{SourceVariables.CommitId}' } }), ], }); } } ================================================ FILE: Lab6/server/TenantPipeline/package.json ================================================ { "name": "pipeline", "version": "0.1.0", "bin": { "pipeline": "bin/pipeline.js" }, "scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "cdk": "cdk" }, "devDependencies": { "@aws-cdk/assert": "1.64.1", "@types/jest": "^26.0.10", "@types/node": "10.17.27", "aws-cdk-lib": "^2.0.0", "constructs": "^10.0.0", "jest": "^26.4.2", "node-notifier": "^8.0.1", "ts-jest": "^26.2.0", "ts-node": "^8.1.0", "typescript": "4.9.5", "@types/prettier": "2.6.0" }, "dependencies": { "aws-cdk-lib": "^2.0.0", "constructs": "^10.0.0", "source-map-support": "^0.5.19" } } ================================================ FILE: Lab6/server/TenantPipeline/resources/lambda-deploy-tenant-stack.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from boto3.session import Session import json import boto3 import zipfile import tempfile import botocore import traceback import time print('Loading function') cf = boto3.client('cloudformation') code_pipeline = boto3.client('codepipeline') dynamodb = boto3.resource('dynamodb') table_tenant_stack_mapping = dynamodb.Table('ServerlessSaaS-TenantStackMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') table_tenant_settings = dynamodb.Table('ServerlessSaaS-Settings') def find_artifact(artifacts, name): """Finds the artifact 'name' among the 'artifacts' Args: artifacts: The list of artifacts available to the function name: The artifact we wish to use Returns: The artifact dictionary found Raises: Exception: If no matching artifact is found """ for artifact in artifacts: if artifact['name'] == name: return artifact raise Exception('Input artifact named "{0}" not found in event'.format(name)) def get_template_url(s3, artifact, file_in_zip): """Gets the template artifact Downloads the artifact from the S3 artifact store to a temporary file then extracts the zip and returns the file containing the CloudFormation template. Args: artifact: The artifact to download file_in_zip: The path to the file within the zip containing the template Returns: The CloudFormation template as a string Raises: Exception: Any exception thrown while downloading the artifact or unzipping it """ tmp_file = tempfile.NamedTemporaryFile() bucket = artifact['location']['s3Location']['bucketName'] print(bucket) key = artifact['location']['s3Location']['objectKey'] print(key) with tempfile.NamedTemporaryFile() as tmp_file: s3.download_file(bucket, key, tmp_file.name) with zipfile.ZipFile(tmp_file.name, 'r') as zip: extracted_file = zip.extract(file_in_zip, '/tmp/') s3.upload_file(extracted_file, bucket, file_in_zip) template_url =''.join(['https://', bucket,'.s3.amazonaws.com/',file_in_zip]) return template_url def update_stack(stack, template_url, params): """Start a CloudFormation stack update Args: stack: The stack to update template_url: The template to apply Returns: True if an update was started, false if there were no changes to the template since the last update. Raises: Exception: Any exception besides "No updates are to be performed." """ try: cf.update_stack(StackName=stack, TemplateURL=template_url, Capabilities=['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], Parameters=params) return True except botocore.exceptions.ClientError as e: if e.response['Error']['Message'] == 'No updates are to be performed.': return False else: raise Exception('Error updating CloudFormation stack "{0}"'.format(stack), e) def stack_exists(stack): """Check if a stack exists or not Args: stack: The stack to check Returns: True or False depending on whether the stack exists Raises: Any exceptions raised .describe_stacks() besides that the stack doesn't exist. """ try: cf.describe_stacks(StackName=stack) return True except botocore.exceptions.ClientError as e: if "does not exist" in e.response['Error']['Message']: return False else: raise e def create_stack(stack, template_url, params): """Starts a new CloudFormation stack creation Args: stack: The stack to be created template_url: The template for the stack to be created with Throws: Exception: Any exception thrown by .create_stack() """ cf.create_stack(StackName=stack, TemplateURL=template_url, Capabilities=['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], Parameters=params) def get_stack_status(stack): """Get the status of an existing CloudFormation stack Args: stack: The name of the stack to check Returns: The CloudFormation status string of the stack such as CREATE_COMPLETE Raises: Exception: Any exception thrown by .describe_stacks() """ stack_description = cf.describe_stacks(StackName=stack) return stack_description['Stacks'][0]['StackStatus'] def put_job_success(job, message): """Notify CodePipeline of a successful job Args: job: The CodePipeline job ID message: A message to be logged relating to the job status Raises: Exception: Any exception thrown by .put_job_success_result() """ print('Putting job success') print(message) code_pipeline.put_job_success_result(jobId=job) def put_job_failure(job, message): """Notify CodePipeline of a failed job Args: job: The CodePipeline job ID message: A message to be logged relating to the job status Raises: Exception: Any exception thrown by .put_job_failure_result() """ print('Putting job failure') print(message) code_pipeline.put_job_failure_result(jobId=job, failureDetails={'message': message, 'type': 'JobFailed'}) def continue_job_later(job, message): """Notify CodePipeline of a continuing job This will cause CodePipeline to invoke the function again with the supplied continuation token. Args: job: The JobID message: A message to be logged relating to the job status continuation_token: The continuation token Raises: Exception: Any exception thrown by .put_job_success_result() """ # Use the continuation token to keep track of any job execution state # This data will be available when a new job is scheduled to continue the current execution continuation_token = json.dumps({'previous_job_id': job}) print('Putting job continuation') print(message) code_pipeline.put_job_success_result(jobId=job, continuationToken=continuation_token) def start_update_or_create(job_id, stack, template_url, params): """Starts the stack update or create process If the stack exists then update, otherwise create. Args: job_id: The ID of the CodePipeline job stack: The stack to create or update template_url: The template to create/update the stack with """ if stack_exists(stack): status = get_stack_status(stack) if status not in ['CREATE_COMPLETE', 'ROLLBACK_COMPLETE', 'UPDATE_COMPLETE']: # If the CloudFormation stack is not in a state where # it can be updated again then fail the job right away. put_job_failure(job_id, 'Stack cannot be updated when status is: ' + status) return were_updates = update_stack(stack, template_url, params) if were_updates: # If there were updates then continue the job so it can monitor # the progress of the update. continue_job_later(job_id, 'Stack update started') else: # If there were no updates then succeed the job immediately put_job_success(job_id, 'There were no stack updates') else: # If the stack doesn't already exist then create it instead # of updating it. create_stack(stack, template_url, params) # Continue the job so the pipeline will wait for the CloudFormation # stack to be created. continue_job_later(job_id, 'Stack create started') def check_stack_update_status(job_id, stack): """Monitor an already-running CloudFormation update/create Succeeds, fails or continues the job depending on the stack status. Args: job_id: The CodePipeline job ID stack: The stack to monitor """ status = get_stack_status(stack) if status in ['UPDATE_COMPLETE', 'CREATE_COMPLETE']: # If the update/create finished successfully then # succeed the job and don't continue. put_job_success(job_id, 'Stack update complete') elif status in ['UPDATE_IN_PROGRESS', 'UPDATE_ROLLBACK_IN_PROGRESS', 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', 'CREATE_IN_PROGRESS', 'ROLLBACK_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS']: # If the job isn't finished yet then continue it continue_job_later(job_id, 'Stack update still in progress') else: # If the Stack is a state which isn't "in progress" or "complete" # then the stack update/create has failed so end the job with # a failed result. put_job_failure(job_id, 'Update failed: ' + status) def get_user_params(job_data): """Decodes the JSON user parameters and validates the required properties. Args: job_data: The job data structure containing the UserParameters string which should be a valid JSON structure Returns: The JSON parameters decoded as a dictionary. Raises: Exception: The JSON can't be decoded or a property is missing. """ try: # Get the user parameters which contain the stack, artifact and file settings user_parameters = job_data['actionConfiguration']['configuration']['UserParameters'] decoded_parameters = json.loads(user_parameters) except Exception: # We're expecting the user parameters to be encoded as JSON # so we can pass multiple values. If the JSON can't be decoded # then fail the job with a helpful message. raise Exception('UserParameters could not be decoded as JSON') if 'artifact' not in decoded_parameters: # Validate that the artifact name is provided, otherwise fail the job # with a helpful message. raise Exception('Your UserParameters JSON must include the artifact name') if 'template_file' not in decoded_parameters: # Validate that the template file is provided, otherwise fail the job # with a helpful message. raise Exception('Your UserParameters JSON must include the template file name') return decoded_parameters def setup_s3_client(job_data): """Creates an S3 client Uses the credentials passed in the event by CodePipeline. These credentials can be used to access the artifact bucket. Args: job_data: The job data structure Returns: An S3 client with the appropriate credentials """ # Could not use the artifact credentials to put object to artifacts s3 bucket. # We are running into issue as described in https://github.com/aws/aws-cdk/issues/3274 # key_id = job_data['artifactCredentials']['accessKeyId'] # key_secret = job_data['artifactCredentials']['secretAccessKey'] # session_token = job_data['artifactCredentials']['sessionToken'] # session = Session(aws_access_key_id=key_id, # aws_secret_access_key=key_secret, # aws_session_token=session_token) # return session.client('s3') return boto3.client('s3') def get_tenant_params(tenantId): """Get tenant details to be supplied to Cloud formation Args: tenantId (str): tenantId for which details are needed Returns: params from tenant management table """ params = [] param_tenantid = {} param_tenantid['ParameterKey'] = 'TenantIdParameter' param_tenantid['ParameterValue'] = tenantId params.append(param_tenantid) return params def add_parameter(params, parameter_key, parameter_value): parameter = {} parameter['ParameterKey'] = parameter_key parameter['ParameterValue'] = parameter_value params.append(parameter) def update_tenantstackmapping(tenantId, commit_id): """Update the tenant stack mapping table with the code pipeline job id Args: tenantId ([string]): tenant id for which data needs to be updated job_id ([type]): current code pipeline job id Returns: [type]: [description] """ response = table_tenant_stack_mapping.update_item( Key={'tenantId': tenantId}, UpdateExpression="set codeCommitId=:codeCommitId", ExpressionAttributeValues={ ':codeCommitId': commit_id }, ReturnValues="NONE") return response def lambda_handler(event, context): """The Lambda function handler If a continuing job then checks the CloudFormation stack status and updates the job accordingly. If a new job then kick of an update or creation of the target CloudFormation stack. Args: event: The event passed by Lambda context: The context passed by Lambda """ try: # Extract the Job ID job_id = event['CodePipeline.job']['id'] # Extract the Job Data job_data = event['CodePipeline.job']['data'] # Extract the params params = get_user_params(job_data) # Get the list of artifacts passed to the function artifacts = job_data['inputArtifacts'] artifact = params['artifact'] template_file = params['template_file'] commit_id = params['commit_id'] # Get all the stacks for each tenant to be updated/created from tenant stack mapping table mappings = table_tenant_stack_mapping.scan() print (mappings) #Update/Create stacks for all tenants for mapping in mappings['Items']: stack = mapping['stackName'] tenantId = mapping['tenantId'] applyLatestRelease = mapping['applyLatestRelease'] if (applyLatestRelease): # Get the parameters to be passed to the Cloudformation from tenant table params = get_tenant_params(tenantId) if 'continuationToken' in job_data: # If we're continuing then the create/update has already been triggered # we just need to check if it has finished. check_stack_update_status(job_id, stack) else: # Get the artifact details artifact_data = find_artifact(artifacts, artifact) # Get S3 client to access artifact with s3 = setup_s3_client(job_data) # Get the JSON template file out of the artifact template_url = get_template_url(s3, artifact_data, template_file) # Kick off a stack update or create start_update_or_create(job_id, stack, template_url, params) # If we are applying the release, update tenant stack mapping with the pipe line id update_tenantstackmapping(tenantId, commit_id) except Exception as e: # If any other exceptions which we didn't expect are raised # then fail the job and log the exception message. print('Function failed due to exception.') print(e) traceback.print_exc() put_job_failure(job_id, 'Function exception: ' + str(e)) #put_job_success(job_id, "Changeset executed successfully") print('Function complete.') return "Complete." ================================================ FILE: Lab6/server/TenantPipeline/test/pipeline.test.ts ================================================ // import { SynthUtils } from '@aws-cdk/assert'; // import { Stack, App } from 'aws-cdk-lib'; // import { Template } from 'aws-cdk-lib/assertions'; // import * as Pipeline from '../lib/serverless-saas-stack'; // test('synthesized cloudformation template should match original template', () => { // const app = new App(); // const stack = new Pipeline.ServerlessSaaSStack(app, 'MyTestStack'); // const template = Template.fromStack(stack); // expect(template).toMatchSnapshot(); // }); ================================================ FILE: Lab6/server/TenantPipeline/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2018", "module": "commonjs", "lib": ["es2018"], "declaration": true, "strict": true, "noImplicitAny": true, "strictNullChecks": true, "noImplicitThis": true, "alwaysStrict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": false, "inlineSourceMap": true, "inlineSources": true, "experimentalDecorators": true, "strictPropertyInitialization": false, "typeRoots": ["./node_modules/@types"] }, "exclude": ["cdk.out"] } ================================================ FILE: Lab6/server/custom_resources/requirements.txt ================================================ requests crhelper ================================================ FILE: Lab6/server/custom_resources/update_settings_table.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import logger from crhelper import CfnResource helper = CfnResource() try: dynamodb = boto3.resource('dynamodb') except Exception as e: helper.init_failure(e) @helper.create @helper.update def do_action(event, _): """ Called as part of bootstrap template. Inserts/Updates Settings table based upon the resources deployed inside bootstrap template We use these settings inside tenant template Args: event ([type]): [description] _ ([type]): [description] """ logger.info("Updating settings") settings_table_name = event['ResourceProperties']['SettingsTableName'] cognitoUserPoolId = event['ResourceProperties']['cognitoUserPoolId'] cognitoUserPoolClientId = event['ResourceProperties']['cognitoUserPoolClientId'] table_system_settings = dynamodb.Table(settings_table_name) response = table_system_settings.put_item( Item={ 'settingName': 'userPoolId-pooled', 'settingValue' : cognitoUserPoolId } ) response = table_system_settings.put_item( Item={ 'settingName': 'appClientId-pooled', 'settingValue' : cognitoUserPoolClientId } ) @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Lab6/server/custom_resources/update_tenant_apigatewayurl.py ================================================ import json import boto3 import logger from boto3.dynamodb.conditions import Key from crhelper import CfnResource helper = CfnResource() try: client = boto3.client('dynamodb') dynamodb = boto3.resource('dynamodb') except Exception as e: helper.init_failure(e) @helper.create @helper.update def do_action(event, _): """ The URL for Tenant APIs(Product/Order) can differ by tenant. For Pooled tenants it is shared and for Silo (Platinum tier tenants) it is unique to them. This method keeps the URL for Pooled tenants inside Settings Table, since it is shared across multiple tenants, And for Silo tenants inside the tenant management table along with other tenant settings, for that tenant Args: event ([type]): [description] _ ([type]): [description] """ logger.info("Updating Tenant Details table") tenant_details_table_name = event['ResourceProperties']['TenantDetailsTableName'] settings_table_name = event['ResourceProperties']['SettingsTableName'] tenant_id = event['ResourceProperties']['TenantId'] tenant_api_gateway_url = event['ResourceProperties']['TenantApiGatewayUrl'] if(tenant_id.lower() =='pooled'): # Note: Tenant management service will use below setting to update apiGatewayUrl for pooled tenants in TenantDetails table settings_table = dynamodb.Table(settings_table_name) settings_table.put_item(Item={ 'settingName': 'apiGatewayUrl-Pooled', 'settingValue' : tenant_api_gateway_url }) else: tenant_details = dynamodb.Table(tenant_details_table_name) response = tenant_details.update_item( Key={'tenantId': tenant_id}, UpdateExpression="set apiGatewayUrl=:apiGatewayUrl", ExpressionAttributeValues={ ':apiGatewayUrl': tenant_api_gateway_url }, ReturnValues="NONE") @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Lab6/server/custom_resources/update_tenantstackmap_table.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import logger from crhelper import CfnResource helper = CfnResource() try: dynamodb = boto3.resource('dynamodb') except Exception as e: helper.init_failure(e) @helper.create @helper.update def do_action(event, _): """ One time entry for pooled tenants inside tenant stack mapping table. This ensures that when code pipeline for tenant template is kicked off, it always create a default stack for pooled tenants. Args: event ([type]): [description] _ ([type]): [description] """ logger.info("Updating Tenant Stack Map") tenantstackmap_table_name = event['ResourceProperties']['TenantStackMappingTableName'] table_stack_mapping = dynamodb.Table(tenantstackmap_table_name) response = table_stack_mapping.put_item( Item={ 'tenantId': 'pooled', 'stackName' : 'stack-pooled', 'applyLatestRelease': True, 'codeCommitId': '' } ) @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Lab6/server/custom_resources/update_usage_plan.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import logger from crhelper import CfnResource helper = CfnResource() try: dynamodb = boto3.resource('dynamodb') apigateway = boto3.client('apigateway') except Exception as e: helper.init_failure(e) @helper.create def do_action(event, _): """ Usage plans are created as part of bootstrap template. This method associates the usage plans for various tiers with tenant Apis Args: event ([type]): [description] _ ([type]): [description] """ logger.info("adding api gateway stage to usage plan") api_id = event['ResourceProperties']['ApiGatewayId'] settings_table_name = event['ResourceProperties']['SettingsTableName'] is_pooled_deploy = event['ResourceProperties']['IsPooledDeploy'] stage = event['ResourceProperties']['Stage'] usage_plan_id_basic = event['ResourceProperties']['UsagePlanBasicTier'] usage_plan_id_standard = event['ResourceProperties']['UsagePlanStandardTier'] usage_plan_id_premium = event['ResourceProperties']['UsagePlanPremiumTier'] usage_plan_id_platinum = event['ResourceProperties']['UsagePlanPlatinumTier'] table_system_settings = dynamodb.Table(settings_table_name) if(is_pooled_deploy == "true"): response_apigateway = apigateway.update_usage_plan ( usagePlanId=usage_plan_id_basic, patchOperations=[ { 'op':'add', 'path':'/apiStages', 'value': api_id + ":" + stage } ] ) response_apigateway = apigateway.update_usage_plan ( usagePlanId=usage_plan_id_standard, patchOperations=[ { 'op':'add', 'path':'/apiStages', 'value': api_id + ":" + stage } ] ) response_apigateway = apigateway.update_usage_plan ( usagePlanId=usage_plan_id_premium, patchOperations=[ { 'op':'add', 'path':'/apiStages', 'value': api_id + ":" + stage } ] ) else: response_apigateway = apigateway.update_usage_plan ( usagePlanId=usage_plan_id_platinum, patchOperations=[ { 'op':'add', 'path':'/apiStages', 'value': api_id + ":" + stage } ] ) @helper.update @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Lab6/server/layers/auth_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils # These are the roles being supported in this reference architecture class UserRoles: SYSTEM_ADMIN = "SystemAdmin" CUSTOMER_SUPPORT = "CustomerSupport" TENANT_ADMIN = "TenantAdmin" TENANT_USER = "TenantUser" def isTenantAdmin(user_role): if (user_role == UserRoles.TENANT_ADMIN): return True else: return False def isSystemAdmin(user_role): if (user_role == UserRoles.SYSTEM_ADMIN): return True else: return False def isSaaSProvider(user_role): if (user_role == UserRoles.SYSTEM_ADMIN or user_role == UserRoles.CUSTOMER_SUPPORT): return True else: return False def isTenantUser(user_role): if (user_role == UserRoles.TENANT_USER): return True else: return False def getPolicyForUser(user_role, service_identifier, tenant_id, region, aws_account_id): """ This method is being used by Authorizer to get appropriate policy by user role Args: user_role (string): UserRoles enum tenant_id (string): region (string): aws_account_id (string): Returns: string: policy that tenant needs to assume """ iam_policy = "" if (isSystemAdmin(user_role)): iam_policy = __getPolicyForSystemAdmin(region, aws_account_id) elif (isTenantAdmin(user_role)): iam_policy = __getPolicyForTenantAdmin(tenant_id, service_identifier, region, aws_account_id) elif (isTenantUser(user_role)): iam_policy = __getPolicyForTenantUser(tenant_id, region, aws_account_id) return iam_policy def __getPolicyForSystemAdmin(region, aws_account_id): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/*".format(region, aws_account_id), ] } ] } return json.dumps(policy) def __getPolicyForTenantAdmin(tenant_id, sevice_identifier, region, aws_account_id): if (sevice_identifier == utils.Service_Identifier.SHARED_SERVICES.value): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantUserMapping".format(region, aws_account_id), "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantDetails".format(region, aws_account_id) ], "Condition": { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [ "{0}".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantStackMapping".format(region, aws_account_id), "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-Settings".format(region, aws_account_id) ] } ] } else: policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Product-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Order-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } } ] } return json.dumps(policy) def __getPolicyForTenantUser(tenant_id, region, aws_account_id): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Product-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Order-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } } ] } return json.dumps(policy) ================================================ FILE: Lab6/server/layers/logger.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from aws_lambda_powertools import Logger logger = Logger() """Log info messages """ def info(log_message): #logger.structure_logs(append=True, tenant_id=tenant_id) logger.info (log_message) """Log error messages """ def error(log_message): #logger.structure_logs(append=True, tenant_id=tenant_id) logger.error (log_message) """Log with tenant context. Extracts tenant context from the lambda events """ def log_with_tenant_context(event, log_message): logger.structure_logs(append=True, tenant_id= event['requestContext']['authorizer']['tenantId']) logger.info (log_message) ================================================ FILE: Lab6/server/layers/metrics_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json from aws_lambda_powertools import Metrics metrics = Metrics() def record_metric(event, metric_name, metric_unit, metric_value): """ Record the metric in Cloudwatch using EMF format Args: event ([type]): [description] metric_name ([type]): [description] metric_unit ([type]): [description] metric_value ([type]): [description] """ metrics.add_dimension(name="tenant_id", value=event['requestContext']['authorizer']['tenantId']) metrics.add_metric(name=metric_name, unit=metric_unit, value=metric_value) metrics_object = metrics.serialize_metric_set() metrics.clear_metrics() print(json.dumps(metrics_object)) ================================================ FILE: Lab6/server/layers/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] simplejson jsonpickle aws_requests_auth python-jose[cryptography] aws_requests_auth ================================================ FILE: Lab6/server/layers/utils.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import jsonpickle import simplejson import boto3 from aws_requests_auth.aws_auth import AWSRequestsAuth from enum import Enum class TenantTier(Enum): PLATINUM = "Platinum" PREMIUM = "Premium" STANDARD = "Standard" BASIC = "Basic" class StatusCodes(Enum): SUCCESS = 200 UN_AUTHORIZED = 401 NOT_FOUND = 404 class Service_Identifier(Enum): SHARED_SERVICES = "SharedServices" BUSINESS_SERVICES = "BusinessServices" def create_success_response(message): return { "statusCode": StatusCodes.SUCCESS.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def create_unauthorized_response(): return { "statusCode": StatusCodes.UN_AUTHORIZED.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": "User not authorized to perform this action" }), } def create_notfound_response(message): return { "statusCode": StatusCodes.NOT_FOUND.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def get_auth(host, region): session = boto3.Session() credentials = session.get_credentials() auth = AWSRequestsAuth(aws_access_key=credentials.access_key, aws_secret_access_key=credentials.secret_key, aws_token=credentials.token, aws_host=host, aws_region=region, aws_service='execute-api') return auth def get_headers(event): return event['headers'] def generate_response(inputObject): return { "statusCode": 200, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": encode_to_json_object(inputObject), } def encode_to_json_object(inputObject): jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True) jsonpickle.set_preferred_backend('simplejson') return jsonpickle.encode(inputObject, unpicklable=False, use_decimal=True) ================================================ FILE: Lab6/server/nested_templates/apigateway.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway, apis, api keys and usage plan as part of bootstrap Parameters: StageName: Type: String RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ProvisionTenantFunctionArn: Type: String DeProvisionTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetTenantConfigFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String ApiKeyOperationUsersParameter: Type: String ApiKeyPlatinumTierParameter: Type: String ApiKeyPremiumTierParameter: Type: String ApiKeyStandardTierParameter: Type: String ApiKeyBasicTierParameter: Type: String Resources: ApiGatewayCloudWatchLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub apigateway-cloudwatch-publish-role-${AWS::Region} Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole ApiGatewayAttachCloudwatchLogArn: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchLogRole.Arn AdminApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/api-gateway/access-logs-serverless-saas-admin-api RetentionInDays: 30 AdminApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: "/*" HttpMethod: "*" Auth: ResourcePolicy: CustomStatements: - Effect: Allow Principal: "*" Action: "execute-api:Invoke" Resource: ["execute-api:/*/*/*"] - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/tenant" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/user/tenant-admin" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/provisioning" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref RegisterTenantLambdaExecutionRoleArn - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/disable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/enable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/provisioning/{tenantid}" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn AccessLogSetting: DestinationArn: !GetAtt AdminApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ["", ["serverless-saas-admin-api-", !Ref "AWS::Region"]] basePath: !Join ["", ["/", !Ref StageName]] x-amazon-apigateway-api-key-source : "AUTHORIZER" schemes: - https paths: /registration: post: summary: Register a new tenant description: Register a new tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref RegisterTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /provisioning: post: summary: provisions resource for new tenant description: provisions resource for new tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ProvisionTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /provisioning/{tenantid}: put: summary: deprovision by tenant description: deprovision by tenant produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeProvisionTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/activation/{tenantid}: put: security: - api_key: [] - Authorizer: [] summary: Activate an existing tenant description: Activate an existing tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ActivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenants: get: summary: Returns all tenants description: Returns all tenants produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantsFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant: post: summary: Creates a tenant description: Creates a tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/init/{tenantname}: get: summary: Returns a tenant config description: Return a tenant config by a tenant name produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantConfigFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/{tenantid}: get: summary: Returns a tenant description: Return a tenant by a tenant id produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Disables a tenant description: Disables a tenant by a tenant id produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeactivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy put: summary: Updates a tenant description: Updates a tenant produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user/{username}: get: summary: Returns a user description: Return a user by a user id produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUserFunctionArn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Diables a user description: Disable a user by a user id produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUserFunctionArn - /invocations httpMethod: POST type: aws_proxy /user/tenant-admin: post: summary: Creates a tenant admin user description: Creates a tenant admin user produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantAdminUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user: post: summary: Create a user description: Create a user by a user id produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users: get: summary: Get all users by tenantId description: Get all users by tenantId produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUsersFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/disable: put: summary: disable users by tenant id description: disable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/enable: put: summary: enable users by tenant id description: enable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref EnableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: api_key: type: "apiKey" name: "x-api-key" in: "header" sigv4Reference: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "awsSigv4" Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref AuthorizerFunctionArn - /invocations authorizerResultTtlInSeconds: 60 type: "token" StageName: prod #Create API Keys and Usage Plans APIGatewayApiKeySystemAdmin: Type: AWS::ApiGateway::ApiKey Properties: Description: "This is the api key to be used by system admin" Enabled: True Name: Serverless-SaaS-SysAdmin-ApiKey Value: !Ref ApiKeyOperationUsersParameter APIGatewayApiKeyPlatinumTier: Type: AWS::ApiGateway::ApiKey Properties: Description: 'This is the api key to be used by platinum tier tenants' Enabled: True Name: Serverless-SaaS-PlatinumTier-ApiKey Value: !Ref ApiKeyPlatinumTierParameter APIGatewayApiKeyPremiumTier: Type: AWS::ApiGateway::ApiKey Properties: Description: 'This is the api key to be used by premium tier tenants' Enabled: True Name: Serverless-SaaS-PremiumTier-ApiKey Value: !Ref ApiKeyPremiumTierParameter APIGatewayApiKeyStandardTier: Type: AWS::ApiGateway::ApiKey Properties: Description: 'This is the api key to be used by standard tier tenants' Enabled: True Name: Serverless-SaaS-StandardTier-ApiKey Value: !Ref ApiKeyStandardTierParameter APIGatewayApiKeyBasicTier: Type: AWS::ApiGateway::ApiKey Properties: Description: 'This is the api key to be used by basic tier tenants' Enabled: True Name: Serverless-SaaS-BasicTier-ApiKey Value: !Ref ApiKeyBasicTierParameter UsagePlanPlatinumTier: Type: 'AWS::ApiGateway::UsagePlan' Properties: ApiStages: - ApiId: !Ref AdminApiGatewayApi Stage: !Ref StageName Description: Usage plan for platinum tier tenants Quota: Limit: 10000 Period: DAY Throttle: BurstLimit: 300 RateLimit: 300 UsagePlanName: Plan_Platinum_Tier DependsOn: - AdminApiGatewayApiprodStage UsagePlanPremiumTier: Type: 'AWS::ApiGateway::UsagePlan' Properties: ApiStages: - ApiId: !Ref AdminApiGatewayApi Stage: !Ref StageName Description: Usage plan for premium tier tenants Quota: Limit: 5000 Period: DAY Throttle: BurstLimit: 200 RateLimit: 100 UsagePlanName: Plan_Premium_Tier DependsOn: - AdminApiGatewayApiprodStage UsagePlanStandardTier: Type: 'AWS::ApiGateway::UsagePlan' Properties: ApiStages: - ApiId: !Ref AdminApiGatewayApi Stage: !Ref StageName Description: Usage plan for standard tier tenants Quota: Limit: 3000 Period: DAY Throttle: BurstLimit: 100 RateLimit: 75 UsagePlanName: Plan_Standard_Tier DependsOn: - AdminApiGatewayApiprodStage UsagePlanBasicTier: Type: 'AWS::ApiGateway::UsagePlan' Properties: ApiStages: - ApiId: !Ref AdminApiGatewayApi Stage: !Ref StageName Description: Usage plan for basic tier tenants Quota: Limit: 500 Period: DAY Throttle: BurstLimit: 50 RateLimit: 50 UsagePlanName: Plan_Basic_Tier DependsOn: - AdminApiGatewayApiprodStage UsagePlanSystemAdmin: Type: "AWS::ApiGateway::UsagePlan" Properties: ApiStages: - ApiId: !Ref AdminApiGatewayApi Stage: !Ref StageName Description: Usage plan for system admin Quota: Limit: 10000 Period: DAY Throttle: BurstLimit: 5000 RateLimit: 500 UsagePlanName: System_Admin_Usage_Plan DependsOn: - AdminApiGatewayApiprodStage AssociateAPIKeyToUsagePlan: Type: AWS::ApiGateway::UsagePlanKey Properties: KeyId: !Ref APIGatewayApiKeySystemAdmin KeyType: API_KEY UsagePlanId: !Ref UsagePlanSystemAdmin DependsOn: UsagePlanSystemAdmin AssociatePlatinumAPIKeyToUsagePlan: Type: AWS::ApiGateway::UsagePlanKey Properties: KeyId: !Ref APIGatewayApiKeyPlatinumTier KeyType: API_KEY UsagePlanId: !Ref UsagePlanPlatinumTier DependsOn: UsagePlanPlatinumTier AssociatePremiumAPIKeyToUsagePlan: Type: AWS::ApiGateway::UsagePlanKey Properties: KeyId: !Ref APIGatewayApiKeyPremiumTier KeyType: API_KEY UsagePlanId: !Ref UsagePlanPremiumTier DependsOn: UsagePlanPremiumTier AssociateStandardAPIKeyToUsagePlan: Type: AWS::ApiGateway::UsagePlanKey Properties: KeyId: !Ref APIGatewayApiKeyStandardTier KeyType: API_KEY UsagePlanId: !Ref UsagePlanStandardTier DependsOn: UsagePlanStandardTier AssociateBasicAPIKeyToUsagePlan: Type: AWS::ApiGateway::UsagePlanKey Properties: KeyId: !Ref APIGatewayApiKeyBasicTier KeyType: API_KEY UsagePlanId: !Ref UsagePlanBasicTier DependsOn: UsagePlanBasicTier Outputs: UsagePlanBasicTier: Value: !Ref UsagePlanBasicTier UsagePlanStandardTier: Value: !Ref UsagePlanStandardTier UsagePlanPremiumTier: Value: !Ref UsagePlanPremiumTier UsagePlanPlatinumTier: Value: !Ref UsagePlanPlatinumTier AdminApiGatewayApi: Value: !Ref AdminApiGatewayApi ================================================ FILE: Lab6/server/nested_templates/apigateway_lambdapermissions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway and apis as part of bootstrap Parameters: RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ProvisionTenantFunctionArn: Type: String DeProvisionTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetTenantConfigFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String AdminApiGatewayApi: Type: String Resources: #provide api gateway permissions to call lambda functions RegisterTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref RegisterTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateTenantAdminUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantAdminUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ProvisionTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ProvisionTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeProvisionTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeProvisionTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] EnableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref EnableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUsersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUsersFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref AuthorizerFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*" ]] CreateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantsFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeactivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeactivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ActivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ActivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantConfigLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantConfigFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ================================================ FILE: Lab6/server/nested_templates/cognito.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup cognito as part of bootstrap Parameters: AdminEmailParameter: Type: String Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Description: "Enter the role name for system admin" AdminUserPoolCallbackURLParameter: Type: String Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Description: "Enter Tenant Management userpool call back url" Resources: CognitoUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: PooledTenant-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into tenant UI application at " - "https://" - !Ref TenantUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for tenant UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSClient GenerateSecret: false UserPoolId: !Ref CognitoUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [pooledtenant-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoUserPool CognitoOperationUsersUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: OperationUsers-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into admin UI application at " - "https://" - !Ref AdminUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for admin UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoOperationUsersUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSOperationUsersPoolClient GenerateSecret: false UserPoolId: !Ref CognitoOperationUsersUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoOperationUsersUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [operationsusers-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUserGroup: Type: AWS::Cognito::UserPoolGroup Properties: GroupName: SystemAdmins Description: Admin user group Precedence: 0 UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUser: Type: AWS::Cognito::UserPoolUser Properties: Username: admin DesiredDeliveryMediums: - EMAIL ForceAliasCreation: true UserAttributes: - Name: email Value: !Ref AdminEmailParameter - Name: custom:tenantId Value: system_admins - Name: custom:userRole Value: !Ref SystemAdminRoleNameParameter UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAddUserToGroup: Type: AWS::Cognito::UserPoolUserToGroupAttachment Properties: GroupName: !Ref CognitoAdminUserGroup Username: !Ref CognitoAdminUser UserPoolId: !Ref CognitoOperationUsersUserPool Outputs: CognitoUserPoolId: Value: !Ref CognitoUserPool CognitoUserPoolClientId: Value: !Ref CognitoUserPoolClient CognitoOperationUsersUserPoolId: Value: !Ref CognitoOperationUsersUserPool CognitoOperationUsersUserPoolClientId: Value: !Ref CognitoOperationUsersUserPoolClient CognitoOperationUsersUserPoolProviderURL: Value: !GetAtt CognitoOperationUsersUserPool.ProviderURL ================================================ FILE: Lab6/server/nested_templates/custom_resources.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: ServerlessSaaSSettingsTableArn: Type: String ServerlessSaaSSettingsTableName: Type: String TenantStackMappingTableArn: Type: String TenantStackMappingTableName: Type: String UpdateSettingsTableFunctionArn: Type: String UpdateTenantStackMapTableFunctionArn: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String Resources: #Custom resources UpdateSettingsTable: Type: Custom::UpdateSettingsTable Properties: ServiceToken: !Ref UpdateSettingsTableFunctionArn SettingsTableName: !Ref ServerlessSaaSSettingsTableName cognitoUserPoolId: !Ref CognitoUserPoolId cognitoUserPoolClientId: !Ref CognitoUserPoolClientId UpdateTenantStackMap: Type: Custom::UpdateTenantStackMap Properties: ServiceToken: !Ref UpdateTenantStackMapTableFunctionArn TenantStackMappingTableName: !Ref TenantStackMappingTableName ================================================ FILE: Lab6/server/nested_templates/lambdafunctions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy lambda functions as part of bootstrap Parameters: CognitoOperationUsersUserPoolId: Type: String CognitoOperationUsersUserPoolClientId: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String TenantDetailsTableArn: Type: String ServerlessSaaSSettingsTableArn: Type: String ApiKeyOperationUsersParameter: Type: String ApiKeyPlatinumTierParameter: Type: String ApiKeyPremiumTierParameter: Type: String ApiKeyStandardTierParameter: Type: String ApiKeyBasicTierParameter: Type: String TenantStackMappingTableArn: Type: String TenantUserMappingTableArn: Type: String TenantStackMappingTableName: Type: String TenantUserPoolCallbackURLParameter: Type: String Description: "Enter Tenant Management userpool call back url" Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies Description: Utilities for project ContentUri: ../layers/ CompatibleRuntimes: - python3.9 LicenseInfo: "MIT" RetentionPolicy: Retain Metadata: BuildMethod: python3.9 #Tenant Authorizer AuthorizerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: authorizer-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: authorizer-execution-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:List* Resource: - !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn AuthorizerAccessRole: Type: AWS::IAM::Role DependsOn: AuthorizerExecutionRole Properties: RoleName: authorizer-access-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: - !GetAtt 'AuthorizerExecutionRole.Arn' Action: - sts:AssumeRole Policies: - PolicyName: authorizer-access-role-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:BatchGetItem - dynamodb:GetItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:UpdateItem - dynamodb:Query - dynamodb:Scan Resource: - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/* SharedServicesAuthorizerFunction: Type: AWS::Serverless::Function DependsOn: AuthorizerAccessRole Properties: CodeUri: ../Resources/ Handler: shared_service_authorizer.lambda_handler Runtime: python3.9 Role: !GetAtt AuthorizerExecutionRole.Arn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !Ref CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !Ref CognitoOperationUsersUserPoolClientId OPERATION_USERS_API_KEY : !Ref ApiKeyOperationUsersParameter #Create user pool for the tenant TenantUserPoolLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-userpool-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-userpool-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn - Effect: Allow Action: - dynamodb:GetItem - dynamodb:Query Resource: - !Ref TenantUserMappingTableArn CreateUserLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub create-user-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-user-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:PutItem Resource: - !Ref TenantUserMappingTableArn - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn CreateTenantAdminUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_tenant_admin_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId TENANT_APP_CLIENT_ID: !Ref CognitoUserPoolClientId TENANT_USER_POOL_CALLBACK_URL: !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] POWERTOOLS_SERVICE_NAME: "UserManagement.CreateTenantAdmin" #User management CreateUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.CreateUser" UpdateUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.update_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.UpdateUser" DisableUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUser" DisableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUsersByTenant" EnableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.enable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.EnableUsersByTenant" GetUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.GetUser" GetUsersFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_users Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.GetUsers" #Tenant Management TenantManagementLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-management-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-tenant-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:Scan - dynamodb:Query Resource: - !Ref TenantDetailsTableArn - !Join ["", [!Ref TenantDetailsTableArn, '/index/*']] - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref ServerlessSaaSSettingsTableArn CreateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.create_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.CreateTenant" ActivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.activate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.ActivateTenant" ENABLE_USERS_BY_TENANT: "/users/enable" PROVISION_TENANT: "/provisioning/" GetTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.GetTenant" DeactivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.deactivate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.DeactivateTenant" DEPROVISION_TENANT: "/provisioning/" DISABLE_USERS_BY_TENANT: "/users/disable" UpdateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.update_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.UpdateTenant" PLATINUM_TIER_API_KEY: !Ref ApiKeyPlatinumTierParameter PREMIUM_TIER_API_KEY: !Ref ApiKeyPremiumTierParameter STANDARD_TIER_API_KEY: !Ref ApiKeyStandardTierParameter BASIC_TIER_API_KEY: !Ref ApiKeyBasicTierParameter GetTenantsFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenants Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers GetTenantConfigFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.load_tenant_config Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers #Tenant Registration RegisterTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-registration-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess RegisterTenantFunction: Type: AWS::Serverless::Function DependsOn: RegisterTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-registration.register_tenant Runtime: python3.9 Role: !GetAtt RegisterTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: # Need to find a better way than hard coding resource paths CREATE_TENANT_ADMIN_USER_RESOURCE_PATH: "/user/tenant-admin" CREATE_TENANT_RESOURCE_PATH: "/tenant" PROVISION_TENANT_RESOURCE_PATH: "/provisioning" PLATINUM_TIER_API_KEY: !Ref ApiKeyPlatinumTierParameter PREMIUM_TIER_API_KEY: !Ref ApiKeyPremiumTierParameter STANDARD_TIER_API_KEY: !Ref ApiKeyStandardTierParameter BASIC_TIER_API_KEY: !Ref ApiKeyBasicTierParameter POWERTOOLS_SERVICE_NAME: "TenantRegistration.RegisterTenant" #Tenant Provisioning ProvisionTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-provisioning-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-provisioning-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:DeleteItem Resource: - !Ref TenantStackMappingTableArn - Effect: Allow Action: - codepipeline:StartPipelineExecution Resource: - !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:serverless-saas-pipeline - Effect: Allow Action: - cloudformation:DeleteStack Resource: "*" ProvisionTenantFunction: Type: AWS::Serverless::Function DependsOn: ProvisionTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-provisioning.provision_tenant Runtime: python3.9 Role: !GetAtt ProvisionTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_STACK_MAPPING_TABLE_NAME: !Ref TenantStackMappingTableName DeProvisionTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-deprovisioning-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-deprovisioning-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 #Since this lambda is invoking cloudformation which is inturn removing AWS resources, we are giving overly permissive permissions to this lambda. #You can limit this based upon your use case and AWS Resources you need to remove. Statement: - Effect: Allow Action: "*" Resource: "*" DeProvisionTenantFunction: Type: AWS::Serverless::Function DependsOn: DeProvisionTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-provisioning.deprovision_tenant Runtime: python3.9 Role: !GetAtt DeProvisionTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_STACK_MAPPING_TABLE_NAME: !Ref TenantStackMappingTableName UpdateSettingsTableLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub update-settingstable-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub update-settingstable-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: !Ref ServerlessSaaSSettingsTableArn UpdateSettingsTableFunction: Type: AWS::Serverless::Function DependsOn: UpdateSettingsTableLambdaExecutionRole Properties: CodeUri: ../custom_resources/ Handler: update_settings_table.handler Runtime: python3.9 Role: !GetAtt UpdateSettingsTableLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers UpdateTenantStackMapTableLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub update-tenantstackmap-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub update-tenantstackmap-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: !Ref TenantStackMappingTableArn UpdateTenantStackMapTableFunction: Type: AWS::Serverless::Function DependsOn: UpdateTenantStackMapTableLambdaExecutionRole Properties: CodeUri: ../custom_resources/ Handler: update_tenantstackmap_table.handler Runtime: python3.9 Role: !GetAtt UpdateTenantStackMapTableLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Outputs: RegisterTenantLambdaExecutionRoleArn: Value: !GetAtt RegisterTenantLambdaExecutionRole.Arn TenantManagementLambdaExecutionRoleArn: Value: !GetAtt TenantManagementLambdaExecutionRole.Arn RegisterTenantFunctionArn: Value: !GetAtt RegisterTenantFunction.Arn ProvisionTenantFunctionArn: Value: !GetAtt ProvisionTenantFunction.Arn DeProvisionTenantFunctionArn: Value: !GetAtt DeProvisionTenantFunction.Arn ActivateTenantFunctionArn: Value: !GetAtt ActivateTenantFunction.Arn GetTenantConfigFunctionArn: Value: !GetAtt GetTenantConfigFunction.Arn GetTenantsFunctionArn: Value: !GetAtt GetTenantsFunction.Arn CreateTenantFunctionArn: Value: !GetAtt CreateTenantFunction.Arn GetTenantFunctionArn: Value: !GetAtt GetTenantFunction.Arn DeactivateTenantFunctionArn: Value: !GetAtt DeactivateTenantFunction.Arn UpdateTenantFunctionArn: Value: !GetAtt UpdateTenantFunction.Arn GetUsersFunctionArn: Value: !GetAtt GetUsersFunction.Arn GetUserFunctionArn: Value: !GetAtt GetUserFunction.Arn UpdateUserFunctionArn: Value: !GetAtt UpdateUserFunction.Arn DisableUserFunctionArn: Value: !GetAtt DisableUserFunction.Arn CreateTenantAdminUserFunctionArn: Value: !GetAtt CreateTenantAdminUserFunction.Arn CreateUserFunctionArn: Value: !GetAtt CreateUserFunction.Arn DisableUsersByTenantFunctionArn: Value: !GetAtt DisableUsersByTenantFunction.Arn EnableUsersByTenantFunctionArn: Value: !GetAtt EnableUsersByTenantFunction.Arn SharedServicesAuthorizerFunctionArn: Value: !GetAtt SharedServicesAuthorizerFunction.Arn AuthorizerExecutionRoleArn: Value: !GetAtt AuthorizerExecutionRole.Arn UpdateSettingsTableFunctionArn: Value: !GetAtt UpdateSettingsTableFunction.Arn UpdateTenantStackMapTableFunctionArn: Value: !GetAtt UpdateTenantStackMapTableFunction.Arn ================================================ FILE: Lab6/server/nested_templates/tables.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to create dynamodb tables as part of bootstrap Resources: ServerlessSaaSSettingsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: settingName AttributeType: S KeySchema: - AttributeName: settingName KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-Settings TenantStackMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantStackMapping TenantDetailsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: tenantName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: ServerlessSaas-TenantConfig KeySchema: - AttributeName: tenantName KeyType: HASH Projection: NonKeyAttributes: - userPoolId - appClientId - apiGatewayUrl ProjectionType: INCLUDE TableName: ServerlessSaaS-TenantDetails TenantUserMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: userName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH - AttributeName: userName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantUserMapping GlobalSecondaryIndexes: - IndexName: UserName KeySchema: - AttributeName: userName KeyType: HASH - AttributeName: tenantId KeyType: RANGE Projection: ProjectionType: ALL Outputs: ServerlessSaaSSettingsTableArn: Value: !GetAtt ServerlessSaaSSettingsTable.Arn ServerlessSaaSSettingsTableName: Value: !Ref ServerlessSaaSSettingsTable TenantStackMappingTableArn: Value: !GetAtt TenantStackMappingTable.Arn TenantStackMappingTableName: Value: !Ref TenantStackMappingTable TenantDetailsTableArn: Value: !GetAtt TenantDetailsTable.Arn TenantDetailsTableName: Value: !Ref TenantDetailsTable TenantUserMappingTableArn: Value: !GetAtt TenantUserMappingTable.Arn TenantUserMappingTableName: Value: !Ref TenantUserMappingTable ================================================ FILE: Lab6/server/nested_templates/userinterface.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: IsCloudFrontAndS3PreProvisioned: Type: String Default: false Description: "Tells if cloudfront and s3 buckets are pre-provisioned or not. They get pre-provisioned when the workshop is running as a part of AWS Event through AWS event engine tool." Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref IsCloudFrontAndS3PreProvisioned, true] ] Resources: CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: IsNotRunningInEventEngine Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distributions" AdminAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AdminAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AdminAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AdminAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId AdminAppSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: #Aliases: # - !Sub 'admin.${CustomDomainName}' CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS CachedMethods: - GET - HEAD - OPTIONS Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: adminapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt AdminAppBucket.RegionalDomainName Id: adminapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' AppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId ApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: tenantapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'AppBucket.RegionalDomainName' Id: tenantapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' LandingAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True LandingAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref LandingAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${LandingAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId LandingApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: landingapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'LandingAppBucket.RegionalDomainName' Id: landingapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: AdminBucket: Description: The name of the bucket for uploading the Admin Management site to Value: !Ref AdminAppBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt AdminAppSite.DomainName Condition: IsNotRunningInEventEngine AppBucket: Description: The name of the bucket for uploading the Tenant Management site to Value: !Ref AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt ApplicationSite.DomainName Condition: IsNotRunningInEventEngine LandingAppBucket: Description: The name of the bucket for uploading the Landing site to Value: !Ref LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt LandingApplicationSite.DomainName Condition: IsNotRunningInEventEngine ================================================ FILE: Lab6/server/shared-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas" s3_bucket = "aws-saas-sam-cli-ujwbuket" s3_prefix = "serverless-saas" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" cached="true" parallel="true" ================================================ FILE: Lab6/server/shared-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to Bootstrap the Common Resources Parameters: AdminEmailParameter: Type: String Default: "test@test.com" Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Default: "SystemAdmin" Description: "Enter the role name for system admin" ApiKeyOperationUsersParameter: Type: String Default: "9a7743fa-3ae7-11eb-adc1-0242ac120002" Description: "Enter default api key value to be used by api gateway for system admins" ApiKeyPlatinumTierParameter: Type: String Default: "88b43c36-802e-11eb-af35-38f9d35b2c15" Description: "Enter default api key value to be used by api gateway for platinum tier tenants" ApiKeyPremiumTierParameter: Type: String Default: "6db2bdc2-6d96-11eb-a56f-38f9d33cfd0f" Description: "Enter default api key value to be used by api gateway for premium tier tenants" ApiKeyStandardTierParameter: Type: String Default: "b1c735d8-6d96-11eb-a28b-38f9d33cfd0f" Description: "Enter default api key value to be used by api gateway for standard tier tenants" ApiKeyBasicTierParameter: Type: String Default: "daae9784-6d96-11eb-a28b-38f9d33cfd0f" Description: "Enter default api key value to be used by api gateway for basic tier tenants" StageName: Type: String Default: "prod" Description: "Stage Name for the api" EventEngineParameter: Type: String Default: false Description: "Tells if this workshop is running as a part of AWS Event through AWS EventEngine or not" AdminUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Tenant Management userpool call back url" Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref EventEngineParameter, true] ] Resources: DynamoDBTables: Type: AWS::Serverless::Application Properties: Location: nested_templates/tables.yaml #Create cloudfront and s3 for UI Cde UserInterface: Type: AWS::Serverless::Application Properties: Location: nested_templates/userinterface.yaml Parameters: IsCloudFrontAndS3PreProvisioned: !Ref EventEngineParameter Cognito: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/cognito.yaml Parameters: AdminEmailParameter: !Ref AdminEmailParameter SystemAdminRoleNameParameter: !Ref SystemAdminRoleNameParameter AdminUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.AdminAppSite, !Ref AdminUserPoolCallbackURLParameter] TenantUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.ApplicationSite, !Ref TenantUserPoolCallbackURLParameter] LambdaFunctions: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/lambdafunctions.yaml Parameters: CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId CognitoOperationUsersUserPoolId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId CognitoOperationUsersUserPoolClientId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId TenantDetailsTableArn: !GetAtt DynamoDBTables.Outputs.TenantDetailsTableArn ServerlessSaaSSettingsTableArn: !GetAtt DynamoDBTables.Outputs.ServerlessSaaSSettingsTableArn ApiKeyOperationUsersParameter: !Ref ApiKeyOperationUsersParameter ApiKeyPlatinumTierParameter: !Ref ApiKeyPlatinumTierParameter ApiKeyPremiumTierParameter: !Ref ApiKeyPremiumTierParameter ApiKeyStandardTierParameter: !Ref ApiKeyStandardTierParameter ApiKeyBasicTierParameter: !Ref ApiKeyBasicTierParameter TenantStackMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableArn TenantUserMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantUserMappingTableArn TenantStackMappingTableName: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableName TenantUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.ApplicationSite, !Ref TenantUserPoolCallbackURLParameter] APIs: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway.yaml Parameters: StageName: !Ref StageName RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ProvisionTenantFunctionArn DeProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeProvisionTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn GetTenantConfigFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantConfigFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn ApiKeyOperationUsersParameter: !Ref ApiKeyOperationUsersParameter ApiKeyPlatinumTierParameter: !Ref ApiKeyPlatinumTierParameter ApiKeyPremiumTierParameter: !Ref ApiKeyPremiumTierParameter ApiKeyStandardTierParameter: !Ref ApiKeyStandardTierParameter ApiKeyBasicTierParameter: !Ref ApiKeyBasicTierParameter APIGatewayLambdaPermissions: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway_lambdapermissions.yaml Parameters: RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ProvisionTenantFunctionArn DeProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeProvisionTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantConfigFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantConfigFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn AdminApiGatewayApi: !GetAtt APIs.Outputs.AdminApiGatewayApi #setup custom resources CustomResources: Type: AWS::Serverless::Application DependsOn: APIs Properties: Location: nested_templates/custom_resources.yaml Parameters: ServerlessSaaSSettingsTableArn: !GetAtt DynamoDBTables.Outputs.ServerlessSaaSSettingsTableArn ServerlessSaaSSettingsTableName: !GetAtt DynamoDBTables.Outputs.ServerlessSaaSSettingsTableName TenantStackMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableArn TenantStackMappingTableName: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableName UpdateSettingsTableFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateSettingsTableFunctionArn UpdateTenantStackMapTableFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantStackMapTableFunctionArn CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId Outputs: AdminApi: Description: "API Gateway endpoint URL for Admin API" Value: !Join ["", ["https://", !GetAtt APIs.Outputs.AdminApiGatewayApi, ".execute-api.", !Ref "AWS::Region", ".amazonaws.com/", !Ref StageName]] AdminSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant administration application Value: !GetAtt UserInterface.Outputs.AdminBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt UserInterface.Outputs.AdminAppSite Condition: IsNotRunningInEventEngine LandingApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the landing application Value: !GetAtt UserInterface.Outputs.LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt UserInterface.Outputs.LandingApplicationSite Condition: IsNotRunningInEventEngine ApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant application Value: !GetAtt UserInterface.Outputs.AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt UserInterface.Outputs.ApplicationSite Condition: IsNotRunningInEventEngine CognitoOperationUsersUserPoolId: Description: The user pool id of Admin Management userpool Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId Export: Name: "Serverless-SaaS-CognitoOperationUsersUserPoolId" CognitoOperationUsersUserPoolProviderURL: Description: The Admin Management userpool provider url Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolProviderURL CognitoOperationUsersUserPoolClientId: Description: The Admin Management userpool client id Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId Export: Name: "Serverless-SaaS-CognitoOperationUsersUserPoolClientId" CognitoTenantUserPoolId: Description: The user pool id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolId Export: Name: "Serverless-SaaS-CognitoTenantUserPoolId" CognitoTenantAppClientId: Description: The app client id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolClientId Export: Name: "Serverless-SaaS-CognitoTenantAppClientId" AuthorizerExecutionRoleArn: Description: The Lambda authorizer execution role Value: !GetAtt LambdaFunctions.Outputs.AuthorizerExecutionRoleArn Export: Name: "Serverless-SaaS-AuthorizerExecutionRoleArn" UsagePlanBasicTier: Description: The basic tier usage plan Value: !GetAtt APIs.Outputs.UsagePlanBasicTier Export: Name: "Serverless-SaaS-UsagePlanBasicTier" UsagePlanStandardTier: Description: The standard tier usage plan Value: !GetAtt APIs.Outputs.UsagePlanStandardTier Export: Name: "Serverless-SaaS-UsagePlanStandardTier" UsagePlanPremiumTier: Description: The premium tier usage plan Value: !GetAtt APIs.Outputs.UsagePlanPremiumTier Export: Name: "Serverless-SaaS-UsagePlanPremiumTier" UsagePlanPlatinumTier: Description: The premium tier usage plan Value: !GetAtt APIs.Outputs.UsagePlanPlatinumTier Export: Name: "Serverless-SaaS-UsagePlanPlatinumTier" ApiKeyOperationUsers: Description: The api key of system admins Value: !Ref ApiKeyOperationUsersParameter Export: Name: "Serverless-SaaS-ApiKeyOperationUsers" ================================================ FILE: Lab6/server/tenant-buildspec.yml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 version: 0.2 phases: install: runtime-versions: python: 3.9 commands: # Install packages or any pre-reqs in this phase. # Upgrading SAM CLI to 1.33.0 version - python -m pip install aws-sam-cli==1.33.0 - sam --version # Installing project dependencies - cd Lab6/server/ProductService - python -m pip install -r requirements.txt - cd ../OrderService - python -m pip install -r requirements.txt pre_build: commands: # Run tests, lint scripts or any other pre-build checks. - cd .. - export PYTHONPATH=./ProductService/ # unit tests needs to be fixed. Commenting for now #- python -m pytest tests/unit/ProductService-test_handler.py build: commands: # Use Build phase to build your artifacts (compile, etc.) - sam build -t tenant-template.yaml post_build: commands: # Use Post-Build for notifications, git tags, upload artifacts to S3 - sam package --s3-bucket $PACKAGE_BUCKET --output-template-file packaged.yaml artifacts: discard-paths: yes files: # List of local artifacts that will be passed down the pipeline - Lab6/server/packaged.yaml ================================================ FILE: Lab6/server/tenant-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "stack-pooled" s3_bucket = "aws-saas-sam-cli-ujwbuket" s3_prefix = "serverless-saas-tenant" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM" cached="true" parallel="true" ================================================ FILE: Lab6/server/tenant-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Serverless SaaS Reference Architecture Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Parameters: TenantIdParameter: Type: String Default: pooled Description: Tenant ID for the stack StageName: Type: String Default: "prod" Description: "Stage Name for the api" LambdaReserveConcurrency: Type: Number Default: 20 Description: "Reserve concurrency for lambda function. You can customize this on per tenant basis, if needed, by storing in the tenant table" Conditions: IsPooledDeploy: !Equals [ !Ref TenantIdParameter, pooled] IsSiloDeploy: !Not [!Equals [ !Ref TenantIdParameter, pooled]] Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: !Join ['-', [serverless-saas-dependencies, !Ref TenantIdParameter]] Description: Utilities for project ContentUri: layers/ CompatibleRuntimes: - python3.9 LicenseInfo: 'MIT' RetentionPolicy: Retain Metadata: BuildMethod: python3.9 ProductTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: productId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: productId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: !Join ['-', [Product, !Ref TenantIdParameter]] Tags: - Key: "TenantId" Value: !Ref TenantIdParameter OrderTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: orderId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: orderId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: !Join ['-', [Order, !Ref TenantIdParameter]] Tags: - Key: "TenantId" Value: !Ref TenantIdParameter ProductFunctionExecutionRolePolicy: Condition: IsSiloDeploy Type: AWS::IAM::Policy Properties: PolicyName: !Join ['-', [!Ref TenantIdParameter, product-function-policy]] Roles: - !Ref ProductFunctionExecutionRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt ProductTable.Arn ProductFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, product-function-execution-role]] Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess GetProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn ReservedConcurrentExecutions: !If [IsPooledDeploy, !Ref "AWS::NoValue" , !Ref LambdaReserveConcurrency] Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter GetProductsFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_products Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn ReservedConcurrentExecutions: !If [IsPooledDeploy, !Ref "AWS::NoValue" , !Ref LambdaReserveConcurrency] Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter CreateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.create_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter UpdateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.update_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter DeleteProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.delete_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter OrderFunctionExecutionRolePolicy: Condition: IsSiloDeploy Type: AWS::IAM::Policy Properties: PolicyName: !Join ['-', [!Ref TenantIdParameter, order-function-policy]] Roles: - !Ref OrderFunctionExecutionRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt OrderTable.Arn OrderFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, order-function-execution-role]] Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess GetOrdersFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_orders Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn ReservedConcurrentExecutions: !If [IsPooledDeploy, !Ref "AWS::NoValue" , !Ref LambdaReserveConcurrency] Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter GetOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn ReservedConcurrentExecutions: !If [IsPooledDeploy, !Ref "AWS::NoValue" , !Ref LambdaReserveConcurrency] Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter CreateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.create_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter UpdateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.update_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter DeleteOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.delete_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter BusinessServicesAuthorizerFunction: Type: AWS::Serverless::Function Properties: CodeUri: Resources/ Handler: tenant_authorizer.lambda_handler Runtime: python3.9 Role: !ImportValue Serverless-SaaS-AuthorizerExecutionRoleArn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !ImportValue Serverless-SaaS-CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !ImportValue Serverless-SaaS-CognitoOperationUsersUserPoolClientId OPERATION_USERS_API_KEY : !ImportValue Serverless-SaaS-ApiKeyOperationUsers ApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Join ['-', [/aws/api-gateway/access-logs-serverless-saas-tenant-api-, !Ref TenantIdParameter]] RetentionInDays: 30 ThrottlingLimitMetricFilter: Type: AWS::Logs::MetricFilter Properties: LogGroupName: Ref: "ApiGatewayAccessLogs" FilterPattern: '{$.status = "429"}' MetricTransformations: - MetricValue: "1" MetricNamespace: "Serverless-SaaS-Reference-Architecture" MetricName: !Join ['-', ["ThrottlingLimitExceeded", !Ref TenantIdParameter]] ThrottlingLimitExceeded: Type: AWS::CloudWatch::Alarm Properties: AlarmDescription: Throttling limit exceeded errors ComparisonOperator: GreaterThanThreshold EvaluationPeriods: 1 MetricName: !Join ['-', ["ThrottlingLimitExceeded", !Ref TenantIdParameter]] Namespace: "Serverless-SaaS-Reference-Architecture" Period: 60 Statistic: SampleCount Threshold: 0 ApiGatewayTenantApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: '/*' HttpMethod: '*' AccessLogSetting: DestinationArn: !GetAtt ApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ['-', [!Ref TenantIdParameter, 'serverless-saas-tenant-api']] basePath: !Join ['', ['/', !Ref StageName]] x-amazon-apigateway-api-key-source : "AUTHORIZER" schemes: - https paths: /order/{id}: get: summary: Returns a order description: Return a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a order description: Deletes a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /orders: get: summary: Returns all orders description: Returns all orders. produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrdersFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /order: post: produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product/{id}: get: summary: Returns a product description: Return a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a product description: Deletes a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /products: get: summary: Returns all products description: Returns all products. produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductsFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product: post: produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt BusinessServicesAuthorizerFunction.Arn - /invocations authorizerResultTtlInSeconds: 30 type: "token" StageName: !Ref StageName GetProductsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt GetProductsFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrdersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrdersFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt BusinessServicesAuthorizerFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref ApiGatewayTenantApi, "/*/*" ]] UpdateUsagePlanLambdaExecutionRole: Type: AWS::IAM::Role DependsOn: ApiGatewayTenantApi Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, update-usage-plan-role]] Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy Policies: - PolicyName: !Join ['-', [!Ref TenantIdParameter, update-usage-plan-policy]] PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - kms:Decrypt Resource: !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/* - Effect: Allow Action: - logs:CreateLogGroup - logs:PutLogEvents - logs:CreateLogStream Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:* - Effect: Allow Action: - logs:DescribeLogStreams Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - xray:PutTraceSegments - xray:PutTelemetryRecords Resource: "*" - Effect: Allow Action: - apigateway:PATCH Resource: !Sub arn:aws:apigateway:${AWS::Region}::/usageplans/* - Effect: Allow Action: - dynamodb:GetItem Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/ServerlessSaaS-Settings UpdateUsagePlanFunction: Type: AWS::Serverless::Function DependsOn: UpdateUsagePlanLambdaExecutionRole Properties: CodeUri: custom_resources/ Handler: update_usage_plan.handler Runtime: python3.9 Role: !GetAtt UpdateUsagePlanLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers AssociateUsagePlanWithTenantAPI: Type: Custom::AssociateUsagePlanWithTenantAPI DependsOn: UpdateUsagePlanFunction Properties: ServiceToken: !GetAtt UpdateUsagePlanFunction.Arn ApiGatewayId: !Ref ApiGatewayTenantApi SettingsTableName: ServerlessSaaS-Settings IsPooledDeploy: !If [IsPooledDeploy, true, false] Stage: !Ref StageName UsagePlanBasicTier: !ImportValue Serverless-SaaS-UsagePlanBasicTier UsagePlanStandardTier: !ImportValue Serverless-SaaS-UsagePlanStandardTier UsagePlanPremiumTier: !ImportValue Serverless-SaaS-UsagePlanPremiumTier UsagePlanPlatinumTier: !ImportValue Serverless-SaaS-UsagePlanPlatinumTier UpdateTenantApiGatewayUrlLambdaExecutionRole: Type: AWS::IAM::Role DependsOn: ApiGatewayTenantApi Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, apigwurl-lambda-exec-role]] Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Join ['-', [!Ref TenantIdParameter, apigwurl-lambda-exe-policy ]] PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/ServerlessSaaS-Settings - Effect: Allow Action: - dynamodb:UpdateItem Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/ServerlessSaaS-TenantDetails UpdateTenantApiGatewayUrlFunction: Type: AWS::Serverless::Function DependsOn: UpdateTenantApiGatewayUrlLambdaExecutionRole Properties: CodeUri: custom_resources/ Handler: update_tenant_apigatewayurl.handler Runtime: python3.9 Role: !GetAtt UpdateTenantApiGatewayUrlLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers UpdateTenantApiGatewayUrl: Type: Custom::UpdateTenantApiGatewayUrl DependsOn: UpdateTenantApiGatewayUrlFunction Properties: ServiceToken: !GetAtt UpdateTenantApiGatewayUrlFunction.Arn TenantDetailsTableName: ServerlessSaaS-TenantDetails SettingsTableName: ServerlessSaaS-Settings TenantId: !Ref TenantIdParameter TenantApiGatewayUrl: !Sub "https://${ApiGatewayTenantApi}.execute-api.${AWS::Region}.amazonaws.com/prod/" Outputs: TenantApiGatewayId: Description: Id for Tenant API Gateway Value: !Ref ApiGatewayTenantApi TenantAPI: Description: "API Gateway endpoint URL for Tenant API" Value: !Join ['', [!Sub "https://${ApiGatewayTenantApi}.execute-api.${AWS::Region}.amazonaws.com/", !Ref StageName]] ================================================ FILE: Lab7/.aws-sam/build/GetDynamoDBUsageAndCostByTenant/requirements.txt ================================================ ================================================ FILE: Lab7/.aws-sam/build/GetDynamoDBUsageAndCostByTenant/tenant_usage_and_cost.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import boto3 import time import os from datetime import datetime, timedelta from botocore.exceptions import ClientError from decimal import * cloudformation = boto3.client('cloudformation') logs = boto3.client('logs') athena = boto3.client('athena') dynamodb = boto3.resource('dynamodb') attribution_table = dynamodb.Table("TenantCostAndUsageAttribution") ATHENA_S3_OUTPUT = os.getenv("ATHENA_S3_OUTPUT") RETRY_COUNT = 100 #This function needs to be scheduled on daily basis def calculate_daily_dynamodb_attribution_by_tenant(event, context): time_zone = datetime.now().astimezone().tzinfo start_date_time = int(datetime.now(tz=time_zone).date().strftime('%s')) #current day epoch end_date_time = int((datetime.now(tz=time_zone) + timedelta(days=1)).date().strftime('%s')) #next day epoch #Get total dynamodb cost for the given duration total_dynamodb_cost = __get_total_service_cost('AmazonDynamoDB', start_date_time, end_date_time) log_group_names = __get_list_of_log_group_names() usage_by_tenant_by_day_query = 'fields @timestamp, @message \ | filter @message like /ReadCapacityUnits|WriteCapacityUnits/ \ | fields tenant_id as TenantId, service as Service, \ ReadCapacityUnits.0 as RCapacityUnits, WriteCapacityUnits.0 as WCapacityUnits \ | stats sum(RCapacityUnits) as ReadCapacityUnits, sum(WCapacityUnits) as WriteCapacityUnits by TenantId, dateceil(@timestamp, 1d) as timestamp' print( log_group_names) usage_by_tenant_by_day = __query_cloudwatch_logs(logs, log_group_names, usage_by_tenant_by_day_query, start_date_time, end_date_time) print(usage_by_tenant_by_day) #optionally save this data in a table total_usage_by_day_query = 'fields @timestamp, @message \ | filter @message like /ReadCapacityUnits|WriteCapacityUnits/ \ | fields ReadCapacityUnits.0 as RCapacityUnits, WriteCapacityUnits.0 as WCapacityUnits \ | stats sum(RCapacityUnits) as ReadCapacityUnits, sum(WCapacityUnits) as WriteCapacityUnits by dateceil(@timestamp, 1d) as timestamp' total_usage_by_day = __query_cloudwatch_logs(logs, log_group_names, total_usage_by_day_query, start_date_time, end_date_time) print(total_usage_by_day) total_RCU = 0 total_WCU = 0 for result in total_usage_by_day['results'][0]: if 'ReadCapacityUnits' in result['field']: total_RCU = Decimal(result['value']) if 'WriteCapacityUnits' in result['field']: total_WCU = Decimal(result['value']) print (total_RCU) print (total_WCU) if (total_RCU + total_WCU > 0): total_RCU_By_Tenant = 0 total_WCU_By_Tenant = 0 for result in usage_by_tenant_by_day['results']: for field in result: if 'TenantId' in field['field']: tenant_id = field['value'] if 'ReadCapacityUnits' in field['field']: total_RCU_By_Tenant = Decimal(field['value']) if 'WriteCapacityUnits' in field['field']: total_WCU_By_Tenant = Decimal(field['value']) #RCU is about 5 times cheaper tenant_attribution_percentage= (((total_RCU_By_Tenant * 5) + total_WCU_By_Tenant) / ((total_RCU * 5) + total_WCU)) tenant_dynamodb_cost = tenant_attribution_percentage * total_dynamodb_cost try: response = attribution_table.put_item( Item= { "Date": start_date_time, "ServiceName": "DynamoDB", "TenantId": tenant_id, "TotalRCU": total_RCU, "TenantTotalRCU": total_RCU_By_Tenant, "TotalWCU": total_WCU, "TenantTotalWCU": total_WCU_By_Tenant, "TenantAttributionPercentage": tenant_attribution_percentage, "TenantServiceCost": tenant_dynamodb_cost, "TotalServiceCost": total_dynamodb_cost } ) except ClientError as e: print(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: print("PutItem succeeded:") tenant_id = 'unknown' total_RCU_By_Tenant = 0.0 total_WCU_By_Tenant = 0.0 #Below function considers number of invocation as the metrics to calculate usage and cost. #You can go granluar by recording duration of each metrics and use that to get more granular #Since our functions are basic CRUD this might work as a ball park cost estimate def calculate_daily_lambda_attribution_by_tenant(event, context): #Get total dynamodb cost for the given duration time_zone = datetime.now().astimezone().tzinfo start_date_time = int(datetime.now(tz=time_zone).date().strftime('%s')) #current day epoch end_date_time = int((datetime.now(tz=time_zone) + timedelta(days=1)).date().strftime('%s')) #next day epoch #Get total dynamodb cost for the given duration total_lambda_cost = __get_total_service_cost('AWSLambda', start_date_time, end_date_time) log_group_names = __get_list_of_log_group_names() usage_by_tenant_by_day_query='fields @timestamp, @message \ | filter @message like /Request completed/ \ | fields tenant_id as TenantId , CountLambdaInvocations.0 As LambdaInvocations, timestamp\ | stats count (tenant_id) as CountLambdaInvocations by TenantId, dateceil(@timestamp, 1d) as timestamp' usage_by_tenant_by_day = __query_cloudwatch_logs(logs, log_group_names, usage_by_tenant_by_day_query, start_date_time, end_date_time) print(usage_by_tenant_by_day) total_usage_by_day_query = 'filter @message like /Request completed/ \ | fields CountLambdaInvocations.0 As LambdaInvocations, timestamp\ | stats count (tenant_id) as CountLambdaInvocations by dateceil(@timestamp, 1d) as timestamp' total_usage_by_day = __query_cloudwatch_logs(logs, log_group_names, total_usage_by_day_query, start_date_time, end_date_time) print(total_usage_by_day) total_invocations = 1 #to avoid divide by zero for result in total_usage_by_day['results'][0]: if 'LambdaInvocations' in result['field']: total_invocations = Decimal(result['value']) print (total_invocations) if (total_invocations>0): total_invocations_by_tenant = 0 for result in usage_by_tenant_by_day['results']: for field in result: if 'TenantId' in field['field']: tenant_id = field['value'] if 'LambdaInvocations' in field['field']: total_invocations_by_tenant = Decimal(field['value']) #RCU is about 5 times cheaper tenant_attribution_percentage= (total_invocations_by_tenant / total_invocations) tenant_lambda_cost = tenant_attribution_percentage * total_lambda_cost try: response = attribution_table.put_item( Item= { "Date": start_date_time, "ServiceName": "AWSLambda", "TenantId": tenant_id, "TotalInvocations": total_invocations, "TenantTotalInvocations": total_invocations_by_tenant, "TenantAttributionPercentage": tenant_attribution_percentage, "TenantServiceCost": tenant_lambda_cost, "TotalServiceCost": total_lambda_cost } ) except ClientError as e: print(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: print("PutItem succeeded:") tenant_id = 'unknown' tenant_total_RCU = 0.0 tenant_total_WCU = 0.0 def __get_total_service_cost(servicename, start_date_time, end_date_time): # We need to add more filters for day, month, year, resource ids etc. Below query is because we are just using a sample cur file #Ignoting startTime and endTime filter for now since we have a static/sample cur file query = "SELECT sum(line_item_blended_cost) AS cost FROM costexplorerdb.curoutput WHERE line_item_product_code='{0}'".format(servicename) # Execution response = athena.start_query_execution( QueryString=query, QueryExecutionContext={ 'Database': 'costexplorerdb' }, ResultConfiguration={ 'OutputLocation': "s3://" + ATHENA_S3_OUTPUT, } ) # get query execution id query_execution_id = response['QueryExecutionId'] print(query_execution_id) # get execution status for i in range(1, 1 + RETRY_COUNT): # get query execution query_status = athena.get_query_execution(QueryExecutionId=query_execution_id) print (query_status) query_execution_status = query_status['QueryExecution']['Status']['State'] if query_execution_status == 'SUCCEEDED': print("STATUS:" + query_execution_status) break if query_execution_status == 'FAILED': raise Exception("STATUS:" + query_execution_status) else: print("STATUS:" + query_execution_status) time.sleep(i) else: athena.stop_query_execution(QueryExecutionId=query_execution_id) raise Exception('TIME OVER') # get query results result = athena.get_query_results(QueryExecutionId=query_execution_id) print (result) total_dynamo_db_cost = result['ResultSet']['Rows'][1]['Data'][0]['VarCharValue'] print(total_dynamo_db_cost) return Decimal(total_dynamo_db_cost) def __query_cloudwatch_logs(logs, log_group_names, query_string, start_time, end_time): query = logs.start_query(logGroupNames=log_group_names, startTime=start_time, endTime=end_time, queryString=query_string) query_results = logs.get_query_results(queryId=query["queryId"]) while query_results['status']=='Running' or query_results['status']=='Scheduled': time.sleep(5) query_results = logs.get_query_results(queryId=query["queryId"]) return query_results def __is_log_group_exists(logs_client, log_group_name): logs_paginator = logs_client.get_paginator('describe_log_groups') response_iterator = logs_paginator.paginate(logGroupNamePrefix=log_group_name) for log_groups_list in response_iterator: if not log_groups_list["logGroups"]: return False else: return True def __add_log_group_name(logs_client, log_group_name, log_group_names_list): if __is_log_group_exists(logs_client, log_group_name): log_group_names_list.append(log_group_name) def __get_list_of_log_group_names(): log_group_names = [] log_group_prefix = '/aws/lambda/' cloudformation_paginator = cloudformation.get_paginator('list_stack_resources') response_iterator = cloudformation_paginator.paginate(StackName='stack-pooled') for stack_resources in response_iterator: for resource in stack_resources['StackResourceSummaries']: if (resource["LogicalResourceId"] == "CreateProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "UpdateProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "GetProductsFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "DeleteProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "CreateOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "UpdateOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "GetOrdersFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "DeleteOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue return log_group_names ================================================ FILE: Lab7/.aws-sam/build/GetLambdaUsageAndCostByTenant/requirements.txt ================================================ ================================================ FILE: Lab7/.aws-sam/build/GetLambdaUsageAndCostByTenant/tenant_usage_and_cost.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import boto3 import time import os from datetime import datetime, timedelta from botocore.exceptions import ClientError from decimal import * cloudformation = boto3.client('cloudformation') logs = boto3.client('logs') athena = boto3.client('athena') dynamodb = boto3.resource('dynamodb') attribution_table = dynamodb.Table("TenantCostAndUsageAttribution") ATHENA_S3_OUTPUT = os.getenv("ATHENA_S3_OUTPUT") RETRY_COUNT = 100 #This function needs to be scheduled on daily basis def calculate_daily_dynamodb_attribution_by_tenant(event, context): time_zone = datetime.now().astimezone().tzinfo start_date_time = int(datetime.now(tz=time_zone).date().strftime('%s')) #current day epoch end_date_time = int((datetime.now(tz=time_zone) + timedelta(days=1)).date().strftime('%s')) #next day epoch #Get total dynamodb cost for the given duration total_dynamodb_cost = __get_total_service_cost('AmazonDynamoDB', start_date_time, end_date_time) log_group_names = __get_list_of_log_group_names() usage_by_tenant_by_day_query = 'fields @timestamp, @message \ | filter @message like /ReadCapacityUnits|WriteCapacityUnits/ \ | fields tenant_id as TenantId, service as Service, \ ReadCapacityUnits.0 as RCapacityUnits, WriteCapacityUnits.0 as WCapacityUnits \ | stats sum(RCapacityUnits) as ReadCapacityUnits, sum(WCapacityUnits) as WriteCapacityUnits by TenantId, dateceil(@timestamp, 1d) as timestamp' print( log_group_names) usage_by_tenant_by_day = __query_cloudwatch_logs(logs, log_group_names, usage_by_tenant_by_day_query, start_date_time, end_date_time) print(usage_by_tenant_by_day) #optionally save this data in a table total_usage_by_day_query = 'fields @timestamp, @message \ | filter @message like /ReadCapacityUnits|WriteCapacityUnits/ \ | fields ReadCapacityUnits.0 as RCapacityUnits, WriteCapacityUnits.0 as WCapacityUnits \ | stats sum(RCapacityUnits) as ReadCapacityUnits, sum(WCapacityUnits) as WriteCapacityUnits by dateceil(@timestamp, 1d) as timestamp' total_usage_by_day = __query_cloudwatch_logs(logs, log_group_names, total_usage_by_day_query, start_date_time, end_date_time) print(total_usage_by_day) total_RCU = 0 total_WCU = 0 for result in total_usage_by_day['results'][0]: if 'ReadCapacityUnits' in result['field']: total_RCU = Decimal(result['value']) if 'WriteCapacityUnits' in result['field']: total_WCU = Decimal(result['value']) print (total_RCU) print (total_WCU) if (total_RCU + total_WCU > 0): total_RCU_By_Tenant = 0 total_WCU_By_Tenant = 0 for result in usage_by_tenant_by_day['results']: for field in result: if 'TenantId' in field['field']: tenant_id = field['value'] if 'ReadCapacityUnits' in field['field']: total_RCU_By_Tenant = Decimal(field['value']) if 'WriteCapacityUnits' in field['field']: total_WCU_By_Tenant = Decimal(field['value']) #RCU is about 5 times cheaper tenant_attribution_percentage= (((total_RCU_By_Tenant * 5) + total_WCU_By_Tenant) / ((total_RCU * 5) + total_WCU)) tenant_dynamodb_cost = tenant_attribution_percentage * total_dynamodb_cost try: response = attribution_table.put_item( Item= { "Date": start_date_time, "ServiceName": "DynamoDB", "TenantId": tenant_id, "TotalRCU": total_RCU, "TenantTotalRCU": total_RCU_By_Tenant, "TotalWCU": total_WCU, "TenantTotalWCU": total_WCU_By_Tenant, "TenantAttributionPercentage": tenant_attribution_percentage, "TenantServiceCost": tenant_dynamodb_cost, "TotalServiceCost": total_dynamodb_cost } ) except ClientError as e: print(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: print("PutItem succeeded:") tenant_id = 'unknown' total_RCU_By_Tenant = 0.0 total_WCU_By_Tenant = 0.0 #Below function considers number of invocation as the metrics to calculate usage and cost. #You can go granluar by recording duration of each metrics and use that to get more granular #Since our functions are basic CRUD this might work as a ball park cost estimate def calculate_daily_lambda_attribution_by_tenant(event, context): #Get total dynamodb cost for the given duration time_zone = datetime.now().astimezone().tzinfo start_date_time = int(datetime.now(tz=time_zone).date().strftime('%s')) #current day epoch end_date_time = int((datetime.now(tz=time_zone) + timedelta(days=1)).date().strftime('%s')) #next day epoch #Get total dynamodb cost for the given duration total_lambda_cost = __get_total_service_cost('AWSLambda', start_date_time, end_date_time) log_group_names = __get_list_of_log_group_names() usage_by_tenant_by_day_query='fields @timestamp, @message \ | filter @message like /Request completed/ \ | fields tenant_id as TenantId , CountLambdaInvocations.0 As LambdaInvocations, timestamp\ | stats count (tenant_id) as CountLambdaInvocations by TenantId, dateceil(@timestamp, 1d) as timestamp' usage_by_tenant_by_day = __query_cloudwatch_logs(logs, log_group_names, usage_by_tenant_by_day_query, start_date_time, end_date_time) print(usage_by_tenant_by_day) total_usage_by_day_query = 'filter @message like /Request completed/ \ | fields CountLambdaInvocations.0 As LambdaInvocations, timestamp\ | stats count (tenant_id) as CountLambdaInvocations by dateceil(@timestamp, 1d) as timestamp' total_usage_by_day = __query_cloudwatch_logs(logs, log_group_names, total_usage_by_day_query, start_date_time, end_date_time) print(total_usage_by_day) total_invocations = 1 #to avoid divide by zero for result in total_usage_by_day['results'][0]: if 'LambdaInvocations' in result['field']: total_invocations = Decimal(result['value']) print (total_invocations) if (total_invocations>0): total_invocations_by_tenant = 0 for result in usage_by_tenant_by_day['results']: for field in result: if 'TenantId' in field['field']: tenant_id = field['value'] if 'LambdaInvocations' in field['field']: total_invocations_by_tenant = Decimal(field['value']) #RCU is about 5 times cheaper tenant_attribution_percentage= (total_invocations_by_tenant / total_invocations) tenant_lambda_cost = tenant_attribution_percentage * total_lambda_cost try: response = attribution_table.put_item( Item= { "Date": start_date_time, "ServiceName": "AWSLambda", "TenantId": tenant_id, "TotalInvocations": total_invocations, "TenantTotalInvocations": total_invocations_by_tenant, "TenantAttributionPercentage": tenant_attribution_percentage, "TenantServiceCost": tenant_lambda_cost, "TotalServiceCost": total_lambda_cost } ) except ClientError as e: print(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: print("PutItem succeeded:") tenant_id = 'unknown' tenant_total_RCU = 0.0 tenant_total_WCU = 0.0 def __get_total_service_cost(servicename, start_date_time, end_date_time): # We need to add more filters for day, month, year, resource ids etc. Below query is because we are just using a sample cur file #Ignoting startTime and endTime filter for now since we have a static/sample cur file query = "SELECT sum(line_item_blended_cost) AS cost FROM costexplorerdb.curoutput WHERE line_item_product_code='{0}'".format(servicename) # Execution response = athena.start_query_execution( QueryString=query, QueryExecutionContext={ 'Database': 'costexplorerdb' }, ResultConfiguration={ 'OutputLocation': "s3://" + ATHENA_S3_OUTPUT, } ) # get query execution id query_execution_id = response['QueryExecutionId'] print(query_execution_id) # get execution status for i in range(1, 1 + RETRY_COUNT): # get query execution query_status = athena.get_query_execution(QueryExecutionId=query_execution_id) print (query_status) query_execution_status = query_status['QueryExecution']['Status']['State'] if query_execution_status == 'SUCCEEDED': print("STATUS:" + query_execution_status) break if query_execution_status == 'FAILED': raise Exception("STATUS:" + query_execution_status) else: print("STATUS:" + query_execution_status) time.sleep(i) else: athena.stop_query_execution(QueryExecutionId=query_execution_id) raise Exception('TIME OVER') # get query results result = athena.get_query_results(QueryExecutionId=query_execution_id) print (result) total_dynamo_db_cost = result['ResultSet']['Rows'][1]['Data'][0]['VarCharValue'] print(total_dynamo_db_cost) return Decimal(total_dynamo_db_cost) def __query_cloudwatch_logs(logs, log_group_names, query_string, start_time, end_time): query = logs.start_query(logGroupNames=log_group_names, startTime=start_time, endTime=end_time, queryString=query_string) query_results = logs.get_query_results(queryId=query["queryId"]) while query_results['status']=='Running' or query_results['status']=='Scheduled': time.sleep(5) query_results = logs.get_query_results(queryId=query["queryId"]) return query_results def __is_log_group_exists(logs_client, log_group_name): logs_paginator = logs_client.get_paginator('describe_log_groups') response_iterator = logs_paginator.paginate(logGroupNamePrefix=log_group_name) for log_groups_list in response_iterator: if not log_groups_list["logGroups"]: return False else: return True def __add_log_group_name(logs_client, log_group_name, log_group_names_list): if __is_log_group_exists(logs_client, log_group_name): log_group_names_list.append(log_group_name) def __get_list_of_log_group_names(): log_group_names = [] log_group_prefix = '/aws/lambda/' cloudformation_paginator = cloudformation.get_paginator('list_stack_resources') response_iterator = cloudformation_paginator.paginate(StackName='stack-pooled') for stack_resources in response_iterator: for resource in stack_resources['StackResourceSummaries']: if (resource["LogicalResourceId"] == "CreateProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "UpdateProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "GetProductsFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "DeleteProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "CreateOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "UpdateOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "GetOrdersFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "DeleteOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue return log_group_names ================================================ FILE: Lab7/.aws-sam/build/template.yaml ================================================ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: 'Serverless SaaS - Cost by tenant ' Globals: Function: Timeout: 29 Resources: CURBucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true AWSCURDatabase: Type: AWS::Glue::Database Properties: DatabaseInput: Name: Fn::Sub: costexplorerdb CatalogId: Ref: AWS::AccountId AWSCURCrawlerComponentFunction: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - glue.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSGlueServiceRole Policies: - PolicyName: AWSCURCrawlerComponentFunction PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: Fn::Sub: arn:${AWS::Partition}:logs:*:*:* - Effect: Allow Action: - glue:UpdateDatabase - glue:UpdatePartition - glue:CreateTable - glue:UpdateTable - glue:ImportCatalogToGlue Resource: '*' - Effect: Allow Action: - s3:GetObject - s3:PutObject Resource: Fn::Sub: ${CURBucket.Arn}* - PolicyName: AWSCURKMSDecryption PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - kms:Decrypt Resource: '*' AWSCURCrawlerLambdaExecutor: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: AWSCURCrawlerLambdaExecutor PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: Fn::Sub: arn:${AWS::Partition}:logs:*:*:* - Effect: Allow Action: - glue:StartCrawler Resource: '*' AWSCURCrawler: Type: AWS::Glue::Crawler DependsOn: - AWSCURDatabase - AWSCURCrawlerComponentFunction Properties: Name: AWSCURCrawler-Multi-tenant Description: A recurring crawler that keeps your CUR table in Athena up-to-date. Role: Fn::GetAtt: - AWSCURCrawlerComponentFunction - Arn DatabaseName: Ref: AWSCURDatabase Targets: S3Targets: - Path: Fn::Sub: s3://${CURBucket}/curoutput Exclusions: - '**.json' - '**.yml' - '**.sql' - '**.csv' - '**.gz' - '**.zip' SchemaChangePolicy: UpdateBehavior: UPDATE_IN_DATABASE DeleteBehavior: DELETE_FROM_DATABASE AWSCURInitializer: Type: AWS::Lambda::Function DependsOn: AWSCURCrawler Properties: Code: ZipFile: "const AWS = require('aws-sdk'); const response = require('./cfn-response');\ \ exports.handler = function(event, context, callback) {\n if (event.RequestType\ \ === 'Delete') {\n response.send(event, context, response.SUCCESS);\n\ \ } else {\n const glue = new AWS.Glue();\n glue.startCrawler({ Name:\ \ 'AWSCURCrawler-Multi-tenant' }, function(err, data) {\n if (err)\ \ {\n const responseData = JSON.parse(this.httpResponse.body);\n\ \ if (responseData['__type'] == 'CrawlerRunningException') {\n \ \ callback(null, responseData.Message);\n } else {\n \ \ const responseString = JSON.stringify(responseData);\n if\ \ (event.ResponseURL) {\n response.send(event, context, response.FAILED,{\ \ msg: responseString });\n } else {\n callback(responseString);\n\ \ }\n }\n }\n else {\n if (event.ResponseURL)\ \ {\n response.send(event, context, response.SUCCESS);\n \ \ } else {\n callback(null, response.SUCCESS);\n }\n \ \ }\n });\n }\n};\n" Handler: index.handler Timeout: 30 Runtime: nodejs16.x ReservedConcurrentExecutions: 1 Role: Fn::GetAtt: - AWSCURCrawlerLambdaExecutor - Arn TenantCostandUsageAttributionTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: Date AttributeType: N - AttributeName: ServiceName AttributeType: S KeySchema: - AttributeName: Date KeyType: HASH - AttributeName: ServiceName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: TenantCostAndUsageAttribution QueryLogInsightsExecutionRole: Type: AWS::IAM::Role Properties: RoleName: product-function-execution-role-lab1 Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: query-log-insight-lab7 PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:GetQueryResults - logs:StartQuery - logs:StopQuery - logs:FilterLogEvents - logs:DescribeLogGroups - cloudformation:ListStackResources Resource: - '*' - Effect: Allow Action: - s3:* Resource: - Fn::Sub: arn:aws:s3:::${CURBucket}* - Effect: Allow Action: - dynamodb:* Resource: - Fn::GetAtt: - TenantCostandUsageAttributionTable - Arn - Effect: Allow Action: - Athena:* Resource: - '*' - Effect: Allow Action: - glue:* Resource: - '*' GetDynamoDBUsageAndCostByTenant: Type: AWS::Serverless::Function DependsOn: QueryLogInsightsExecutionRole Properties: CodeUri: GetDynamoDBUsageAndCostByTenant Handler: tenant_usage_and_cost.calculate_daily_dynamodb_attribution_by_tenant Runtime: python3.9 Role: Fn::GetAtt: - QueryLogInsightsExecutionRole - Arn Environment: Variables: ATHENA_S3_OUTPUT: Ref: CURBucket Events: ScheduledEvent: Type: Schedule Properties: Name: CalculateDynamoUsageAndCostByTenant Schedule: rate(5 minutes) GetLambdaUsageAndCostByTenant: Type: AWS::Serverless::Function DependsOn: QueryLogInsightsExecutionRole Properties: CodeUri: GetLambdaUsageAndCostByTenant Handler: tenant_usage_and_cost.calculate_daily_lambda_attribution_by_tenant Runtime: python3.9 Role: Fn::GetAtt: - QueryLogInsightsExecutionRole - Arn Environment: Variables: ATHENA_S3_OUTPUT: Ref: CURBucket Events: ScheduledEvent: Type: Schedule Properties: Name: CalculateLambdaUsageAndCostByTenant Schedule: rate(5 minutes) Outputs: CURBucketname: Description: The name of S3 bucket name Value: Ref: CURBucket Export: Name: CURBucketname AWSCURInitializerFunctionName: Description: Function name of CUR initializer Value: Ref: AWSCURInitializer Export: Name: AWSCURInitializerFunctionName ================================================ FILE: Lab7/.aws-sam/build.toml ================================================ # This file is auto generated by SAM CLI build command [function_build_definitions] [function_build_definitions.8d6cee39-9fc9-4073-9b65-22bf29298af4] codeuri = "/Users/shaanubh/Documents/code/serverless-saas-workshop/code/aws-serverless-saas-workshop/Lab7/TenantUsageAndCost" runtime = "python3.8" architecture = "x86_64" manifest_hash = "" packagetype = "Zip" functions = ["GetDynamoDBUsageAndCostByTenant", "GetLambdaUsageAndCostByTenant"] [layer_build_definitions] ================================================ FILE: Lab7/TenantUsageAndCost/requirements.txt ================================================ ================================================ FILE: Lab7/TenantUsageAndCost/tenant_usage_and_cost.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import boto3 import time import os from datetime import datetime, timedelta from botocore.exceptions import ClientError from decimal import * cloudformation = boto3.client('cloudformation') logs = boto3.client('logs') athena = boto3.client('athena') dynamodb = boto3.resource('dynamodb') attribution_table = dynamodb.Table("TenantCostAndUsageAttribution") ATHENA_S3_OUTPUT = os.getenv("ATHENA_S3_OUTPUT") RETRY_COUNT = 100 #This function needs to be scheduled on daily basis def calculate_daily_dynamodb_attribution_by_tenant(event, context): start_date_time = __get_start_date_time() #current day epoch end_date_time = __get_end_date_time() #next day epoch #Get total dynamodb cost for the given duration #TODO: Get total cost of DynamoDB for the current date total_dynamodb_cost = 0 log_group_names = __get_list_of_log_group_names() print( log_group_names) #TODO: Write the query to get the DynamoDB WCU and RCUs consumption grouped by TenantId usage_by_tenant_by_day_query = 'query placeholder' usage_by_tenant_by_day = __query_cloudwatch_logs(logs, log_group_names, usage_by_tenant_by_day_query, start_date_time, end_date_time) print(usage_by_tenant_by_day) #TODO: Write the query to get the Total DynamoDB WCU and RCUs consumption across all tenants total_usage_by_day_query = 'query placeholder' total_usage_by_day = __query_cloudwatch_logs(logs, log_group_names, total_usage_by_day_query, start_date_time, end_date_time) print(total_usage_by_day) total_RCU = Decimal('0.0') total_WCU = Decimal('0.0') for result in total_usage_by_day['results'][0]: if 'ReadCapacityUnits' in result['field']: total_RCU = Decimal(result['value']) if 'WriteCapacityUnits' in result['field']: total_WCU = Decimal(result['value']) print (total_RCU) print (total_WCU) if (total_RCU + total_WCU > 0): total_RCU_By_Tenant = Decimal('0.0') total_WCU_By_Tenant = Decimal('0.0') for result in usage_by_tenant_by_day['results']: for field in result: if 'TenantId' in field['field']: tenant_id = field['value'] if 'ReadCapacityUnits' in field['field']: total_RCU_By_Tenant = Decimal(field['value']) if 'WriteCapacityUnits' in field['field']: total_WCU_By_Tenant = Decimal(field['value']) #RCU is about 5 times cheaper tenant_attribution_percentage_numerator= Decimal(str(total_RCU_By_Tenant * Decimal('5.0'))) + Decimal(str(total_WCU_By_Tenant)) tenant_attribution_percentage_denominator= Decimal(str(total_RCU * Decimal('5.0'))) + Decimal(str(total_WCU)) tenant_attribution_percentage = tenant_attribution_percentage_numerator/tenant_attribution_percentage_denominator tenant_dynamodb_cost = tenant_attribution_percentage * total_dynamodb_cost try: #TODO: Save the tenant attribution data inside a dynamodb table pass except ClientError as e: print(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: print("PutItem succeeded:") tenant_id = 'unknown' total_RCU_By_Tenant = 0.0 total_WCU_By_Tenant = 0.0 #Below function considers number of invocation as the metrics to calculate usage and cost. #You can go granluar by recording duration of each metrics and use that to get more granular #Since our functions are basic CRUD this might work as a ball park cost estimate def calculate_daily_lambda_attribution_by_tenant(event, context): #Get total dynamodb cost for the given duration start_date_time = __get_start_date_time() #current day epoch end_date_time = __get_end_date_time() #next day epoch #Get total dynamodb cost for the given duration total_lambda_cost = __get_total_service_cost('AWSLambda', start_date_time, end_date_time) log_group_names = __get_list_of_log_group_names() #TODO: Write the below query to get the total lambda invocations grouped by tenants usage_by_tenant_by_day_query='query placeholder' usage_by_tenant_by_day = __query_cloudwatch_logs(logs, log_group_names, usage_by_tenant_by_day_query, start_date_time, end_date_time) print(usage_by_tenant_by_day) #TODO: Write the below query to get the total lambda invocations across all tenants total_usage_by_day_query = 'query placeholder' total_usage_by_day = __query_cloudwatch_logs(logs, log_group_names, total_usage_by_day_query, start_date_time, end_date_time) print(total_usage_by_day) total_invocations = 1 #to avoid divide by zero for result in total_usage_by_day['results'][0]: if 'LambdaInvocations' in result['field']: total_invocations = Decimal(result['value']) print (total_invocations) if (total_invocations>0): total_invocations_by_tenant = 0 for result in usage_by_tenant_by_day['results']: for field in result: if 'TenantId' in field['field']: tenant_id = field['value'] if 'LambdaInvocations' in field['field']: total_invocations_by_tenant = Decimal(field['value']) tenant_attribution_percentage= (total_invocations_by_tenant / total_invocations) tenant_lambda_cost = tenant_attribution_percentage * total_lambda_cost try: response = attribution_table.put_item( Item= { "Date": start_date_time, "TenantId#ServiceName": tenant_id+"#"+"AWSLambda", "TenantId": tenant_id, "TotalInvocations": total_invocations, "TenantTotalInvocations": total_invocations_by_tenant, "TenantAttributionPercentage": tenant_attribution_percentage, "TenantServiceCost": tenant_lambda_cost, "TotalServiceCost": total_lambda_cost } ) except ClientError as e: print(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: print("PutItem succeeded:") tenant_id = 'unknown' total_invocations_by_tenant = 0 def __get_total_service_cost(servicename, start_date_time, end_date_time): # We need to add more filters for day, month, year, resource ids etc. Below query is because we are just using a sample cur file #Ignoting startTime and endTime filter for now since we have a static/sample cur file query = "SELECT sum(line_item_blended_cost) AS cost FROM costexplorerdb.curoutput WHERE line_item_product_code='{0}'".format(servicename) # Execution response = athena.start_query_execution( QueryString=query, QueryExecutionContext={ 'Database': 'costexplorerdb' }, ResultConfiguration={ 'OutputLocation': "s3://" + ATHENA_S3_OUTPUT, } ) # get query execution id query_execution_id = response['QueryExecutionId'] print(query_execution_id) # get execution status for i in range(1, 1 + RETRY_COUNT): # get query execution query_status = athena.get_query_execution(QueryExecutionId=query_execution_id) print (query_status) query_execution_status = query_status['QueryExecution']['Status']['State'] if query_execution_status == 'SUCCEEDED': print("STATUS:" + query_execution_status) break if query_execution_status == 'FAILED': raise Exception("STATUS:" + query_execution_status) else: print("STATUS:" + query_execution_status) time.sleep(i) else: athena.stop_query_execution(QueryExecutionId=query_execution_id) raise Exception('TIME OVER') # get query results result = athena.get_query_results(QueryExecutionId=query_execution_id) print (result) total_dynamo_db_cost = result['ResultSet']['Rows'][1]['Data'][0]['VarCharValue'] print(total_dynamo_db_cost) return Decimal(total_dynamo_db_cost) def __query_cloudwatch_logs(logs, log_group_names, query_string, start_time, end_time): query = logs.start_query(logGroupNames=log_group_names, startTime=start_time, endTime=end_time, queryString=query_string) query_results = logs.get_query_results(queryId=query["queryId"]) while query_results['status']=='Running' or query_results['status']=='Scheduled': time.sleep(5) query_results = logs.get_query_results(queryId=query["queryId"]) return query_results def __is_log_group_exists(logs_client, log_group_name): logs_paginator = logs_client.get_paginator('describe_log_groups') response_iterator = logs_paginator.paginate(logGroupNamePrefix=log_group_name) for log_groups_list in response_iterator: if not log_groups_list["logGroups"]: return False else: return True def __add_log_group_name(logs_client, log_group_name, log_group_names_list): if __is_log_group_exists(logs_client, log_group_name): log_group_names_list.append(log_group_name) def __get_list_of_log_group_names(): log_group_names = [] log_group_prefix = '/aws/lambda/' cloudformation_paginator = cloudformation.get_paginator('list_stack_resources') response_iterator = cloudformation_paginator.paginate(StackName='stack-pooled') for stack_resources in response_iterator: for resource in stack_resources['StackResourceSummaries']: if (resource["LogicalResourceId"] == "CreateProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "UpdateProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "GetProductsFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "DeleteProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "CreateOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "UpdateOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "GetOrdersFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "DeleteOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue return log_group_names def __get_start_date_time(): time_zone = datetime.now().astimezone().tzinfo start_date_time = int(datetime.now(tz=time_zone).date().strftime('%s')) #current day epoch return start_date_time def __get_end_date_time(): time_zone = datetime.now().astimezone().tzinfo end_date_time = int((datetime.now(tz=time_zone) + timedelta(days=1)).date().strftime('%s')) #next day epoch return end_date_time ================================================ FILE: Lab7/deployment.sh ================================================ REGION=$(aws configure get region) sam build -t template.yaml --use-container sam deploy --config-file samconfig.toml --region=$REGION CUR_BUCKET=$(aws cloudformation list-exports --query "Exports[?Name=='CURBucketname'].Value" --output text) AWSCURInitializerFunctionName=$(aws cloudformation list-exports --query "Exports[?Name=='AWSCURInitializerFunctionName'].Value" --output text) aws s3 cp SampleCUR/ s3://$CUR_BUCKET/curoutput/year=2022/month=10/ --recursive aws lambda invoke --function-name $AWSCURInitializerFunctionName lambdaoutput.json ================================================ FILE: Lab7/lambdaoutput.json ================================================ "Crawler with name AWSCURCrawler-Multi-tenant has already started" ================================================ FILE: Lab7/samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas-cost-per-tenant-lab7" s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-1p6m7gm2vwaaz" s3_prefix = "serverless-saas-lab7" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" cached="true" parallel="true" ================================================ FILE: Lab7/template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Serverless SaaS - Cost by tenant Globals: Function: Timeout: 29 Resources: CURBucket: Type: AWS::S3::Bucket DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AWSCURDatabase: Type: 'AWS::Glue::Database' Properties: DatabaseInput: Name: !Sub 'costexplorerdb' CatalogId: !Ref AWS::AccountId AWSCURCrawlerComponentFunction: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - glue.amazonaws.com Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSGlueServiceRole' Policies: - PolicyName: AWSCURCrawlerComponentFunction PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub 'arn:${AWS::Partition}:logs:*:*:*' - Effect: Allow Action: - 'glue:UpdateDatabase' - 'glue:UpdatePartition' - 'glue:CreateTable' - 'glue:UpdateTable' - 'glue:ImportCatalogToGlue' Resource: '*' - Effect: Allow Action: - 's3:GetObject' - 's3:PutObject' Resource: !Sub '${CURBucket.Arn}*' - PolicyName: AWSCURKMSDecryption PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'kms:Decrypt' Resource: '*' AWSCURCrawlerLambdaExecutor: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / Policies: - PolicyName: AWSCURCrawlerLambdaExecutor PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub 'arn:${AWS::Partition}:logs:*:*:*' - Effect: Allow Action: - 'glue:StartCrawler' Resource: '*' AWSCURCrawler: Type: 'AWS::Glue::Crawler' DependsOn: - AWSCURDatabase - AWSCURCrawlerComponentFunction Properties: Name: AWSCURCrawler-Multi-tenant Description: A recurring crawler that keeps your CUR table in Athena up-to-date. Role: !GetAtt AWSCURCrawlerComponentFunction.Arn DatabaseName: !Ref AWSCURDatabase Targets: S3Targets: - Path: !Sub 's3://${CURBucket}/curoutput' Exclusions: - '**.json' - '**.yml' - '**.sql' - '**.csv' - '**.gz' - '**.zip' SchemaChangePolicy: UpdateBehavior: UPDATE_IN_DATABASE DeleteBehavior: DELETE_FROM_DATABASE AWSCURInitializer: Type: 'AWS::Lambda::Function' DependsOn: AWSCURCrawler Properties: Code: ZipFile: > const AWS = require('aws-sdk'); const response = require('./cfn-response'); exports.handler = function(event, context, callback) { if (event.RequestType === 'Delete') { response.send(event, context, response.SUCCESS); } else { const glue = new AWS.Glue(); glue.startCrawler({ Name: 'AWSCURCrawler-Multi-tenant' }, function(err, data) { if (err) { const responseData = JSON.parse(this.httpResponse.body); if (responseData['__type'] == 'CrawlerRunningException') { callback(null, responseData.Message); } else { const responseString = JSON.stringify(responseData); if (event.ResponseURL) { response.send(event, context, response.FAILED,{ msg: responseString }); } else { callback(responseString); } } } else { if (event.ResponseURL) { response.send(event, context, response.SUCCESS); } else { callback(null, response.SUCCESS); } } }); } }; Handler: 'index.handler' Timeout: 30 Runtime: nodejs16.x ReservedConcurrentExecutions: 1 Role: !GetAtt AWSCURCrawlerLambdaExecutor.Arn TenantCostandUsageAttributionTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: Date AttributeType: N - AttributeName: TenantId#ServiceName AttributeType: S KeySchema: - AttributeName: Date KeyType: HASH - AttributeName: TenantId#ServiceName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: TenantCostAndUsageAttribution QueryLogInsightsExecutionRole: Type: AWS::IAM::Role Properties: RoleName: query-log-insights-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: query-log-insight-lab7 PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:GetQueryResults - logs:StartQuery - logs:StopQuery - logs:FilterLogEvents - logs:DescribeLogGroups - cloudformation:ListStackResources Resource: - "*" - Effect: Allow Action: - s3:* Resource: - !Sub 'arn:aws:s3:::${CURBucket}*' - Effect: Allow Action: - dynamodb:* Resource: - !GetAtt TenantCostandUsageAttributionTable.Arn - Effect: Allow Action: - Athena:* Resource: - "*" - Effect: Allow Action: - glue:* Resource: - "*" GetDynamoDBUsageAndCostByTenant: Type: AWS::Serverless::Function DependsOn: QueryLogInsightsExecutionRole Properties: CodeUri: TenantUsageAndCost/ Handler: tenant_usage_and_cost.calculate_daily_dynamodb_attribution_by_tenant Runtime: python3.9 Role: !GetAtt QueryLogInsightsExecutionRole.Arn Environment: Variables: ATHENA_S3_OUTPUT: !Ref CURBucket Events: ScheduledEvent: Type: Schedule Properties: Name: CalculateDynamoUsageAndCostByTenant Schedule: rate(5 minutes) GetLambdaUsageAndCostByTenant: Type: AWS::Serverless::Function DependsOn: QueryLogInsightsExecutionRole Properties: CodeUri: TenantUsageAndCost/ Handler: tenant_usage_and_cost.calculate_daily_lambda_attribution_by_tenant Runtime: python3.9 Role: !GetAtt QueryLogInsightsExecutionRole.Arn Environment: Variables: ATHENA_S3_OUTPUT: !Ref CURBucket Events: ScheduledEvent: Type: Schedule Properties: Name: CalculateLambdaUsageAndCostByTenant Schedule: rate(5 minutes) Outputs: CURBucketname: Description: The name of S3 bucket name Value: !Ref CURBucket Export: Name: "CURBucketname" AWSCURInitializerFunctionName: Description: Function name of CUR initializer Value: !Ref AWSCURInitializer Export: Name: "AWSCURInitializerFunctionName" ================================================ FILE: README.md ================================================ # AWS Serverless SaaS Workshop The goal of this workshop is to build a multi-tenant Software-as-a-Service (SaaS) solution using AWS Serverless Services, such as Amazon API Gateway, Amazon Cognito, AWS Lambda, Amazon DynamoDB, AWS CodePipeline, and Amazon CloudWatch. By the end of this workshop you will be able to understand the challenges that are unique to a SaaS based delivery, such as onboarding, tenant isolation, data partitioning, tenant deployment pipelines, observability, and how to address them using AWS Serverless services. This workshop is inspired by the [SaaS Factory Serverless SaaS reference solution](https://github.com/aws-samples/aws-saas-factory-ref-solution-serverless-saas). At the end of this workshop, you will build a fully functional SaaS application that will be similar to this reference solution. # Starting the workshop Follow this link for detailed instructions to run this workshop in your AWS Account: https://catalog.us-east-1.prod.workshops.aws/v2/workshops/b0c6ad36-0a4b-45d8-856b-8a64f0ac76bb/en-US # License The documentation is made available under the Creative Commons Attribution-ShareAlike 4.0 International License. See the LICENSE file. The sample code within this documentation is made available under the MIT-0 license. See the LICENSE-SAMPLECODE file. ================================================ FILE: Solution/Lab1/client/Application/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab1/client/Application/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab1/client/Application/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end #cypress cypress/videos/* cypress.env.json # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab1/client/Application/README.md ================================================ # Application This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab1/client/Application/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "application": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "2mb", "maximumError": "2mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "application:build:production" }, "development": { "browserTarget": "application:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "application:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab1/client/Application/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/Application'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab1/client/Application/package.json ================================================ { "name": "application", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "e2e": "npx cypress run" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "bootstrap": "~5.2.0", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "cypress": "~10.3.1", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab1/client/Application/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Products', url: '/products', icon: 'sell', }, { name: 'Orders', url: '/orders', icon: 'shopping_cart', }, ]; ================================================ FILE: Solution/Lab1/client/Application/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: '', component: NavComponent, data: { title: 'Home', }, children: [ { path: '', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'orders', loadChildren: () => import('./views/orders/orders.module').then((m) => m.OrdersModule), }, { path: 'products', loadChildren: () => import('./views/products/products.module').then( (m) => m.ProductsModule ), }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab1/client/Application/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab1/client/Application/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'application'; } ================================================ FILE: Solution/Lab1/client/Application/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; @NgModule({ declarations: [AppComponent, NavComponent], imports: [ AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, MatSnackBarModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatIconModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab1/client/Application/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Solution/Lab1/client/Application/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Solution/Lab1/client/Application/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }}
================================================ FILE: Solution/Lab1/client/Application/src/app/nav/nav.component.scss ================================================ .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .sidebar-icon-container { height: 64px; background-color: whitesmoke; display: flex; align-items: center; justify-content: center; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .footer { position: fixed; bottom: 0; width: 100%; height: 40px; background-color: whitesmoke; color: #2d3337; // text-align: center; margin: 0px; } .footer-text { display: flex; } .content { position: absolute; width: 100%; height: 90%; max-height: 90%; overflow: auto; background-color: lightgray; } ================================================ FILE: Solution/Lab1/client/Application/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { navItems } from '../_nav'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.scss'], }) export class NavComponent implements OnInit { loading$: Observable = of(false); public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router ) { // this.configSvc.loadConfigurations().subscribe((val) => console.log(val)); this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void {} } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Solution/Lab1/client/Application/src/app/views/dashboard/dashboard.component.html ================================================

Dashboard

{{ card.title }}
================================================ FILE: Solution/Lab1/client/Application/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/dashboard/dashboard.component.ts ================================================ import { Component, ViewChild } from '@angular/core'; import { map } from 'rxjs/operators'; import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js'; import { BaseChartDirective } from 'ng2-charts'; import DataLabelsPlugin from 'chartjs-plugin-datalabels'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'], }) export class DashboardComponent { @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; public barChartOptions: ChartConfiguration['options'] = { responsive: true, maintainAspectRatio: false, // We use these empty structures as placeholders for dynamic theming. scales: { x: {}, y: { min: 10, }, }, plugins: { legend: { display: true, }, datalabels: { anchor: 'end', align: 'end', }, }, }; public barChartType: ChartType = 'bar'; public barChartPlugins = [DataLabelsPlugin]; public barChartData: ChartData<'bar'> = { labels: ['2006', '2007', '2008', '2009', '2010', '2011', '2012'], datasets: [ { data: [65, 59, 80, 81, 56, 55, 40], label: 'Series A' }, { data: [28, 48, 40, 19, 86, 27, 90], label: 'Series B' }, ], }; // events public chartClicked({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public chartHovered({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public randomize(): void { // Only Change 3 values this.barChartData.datasets[0].data = [ Math.round(Math.random() * 100), 59, 80, Math.round(Math.random() * 100), 56, Math.round(Math.random() * 100), 40, ]; this.chart?.update(); } /** Based on the screen size, switch from standard to one column per row */ cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe( map(({ matches }) => { if (matches) { return [ { title: 'Card 1', cols: 1, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 1 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; } return [ { title: 'Card 1', cols: 2, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 2 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; }) ); constructor(private breakpointObserver: BreakpointObserver) {} } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/create/create.component.html ================================================
Create new order Enter order name Name is required
{{ op.product.name }}
{{ op.product.price | currency }}
{{ op.quantity || 0 }}
================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/create/create.component.scss ================================================ // .card { // margin: 20px; // display: flex; // flex-direction: column; // align-items: flex-start; // } .order-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { Product } from '../../products/models/product.interface'; import { ProductService } from '../../products/product.service'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; interface LineItem { product: Product; quantity?: number; } @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { displayedColumns = []; orderForm: FormGroup; orderProducts: LineItem[] = []; isLoadingProducts: boolean = true; error = ''; constructor( private fb: FormBuilder, private productSvc: ProductService, private orderSvc: OrdersService, private router: Router ) { this.orderForm = this.fb.group({ name: ['', Validators.required], }); } ngOnInit(): void { this.productSvc.fetch().subscribe((products) => { this.orderProducts = products.map((p) => ({ product: p })); this.isLoadingProducts = false; }); this.orderForm = this.fb.group({ orderName: ['', Validators.required], }); } get name() { return this.orderForm.get('name'); } get productQuantity() { return this.orderProducts .filter((p) => !!p.quantity).length > 0; } add(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity ? orderProduct.quantity + 1 : 1, }; } return p; }); } remove(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity && orderProduct.quantity > 1 ? orderProduct.quantity - 1 : undefined, }; } return p; }); } submit() { const val: Order = { ...this.orderForm?.value, orderProducts: this.orderProducts .filter((p) => !!p.quantity) .map((p) => ({ productId: p.product.productId, price: p.product.price, quantity: p.quantity, })), }; this.orderSvc.create(val).subscribe(() => this.router.navigate(['orders'])); } } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/detail/detail.component.html ================================================

Order Details mostly made up

NO: {{ orderId$ | async }} | Date: {{ today() | date }}

Services / Products

Item / Details Unit Cost Sum Cost Discount Tax Total
{{ op.productId }}
{{ op.price | currency }}
Before Tax
{{ sum(op) | currency }}
{{ op.quantity }} Units
$0.00
None
{{ tax(op) | currency }}
Sales Tax 8.9%
{{ total(op) | currency }}
Sub Total Discount Total Tax Final
{{ subTotal(order) | currency }} -$0.00 {{ subTotal(order) | currency }} {{ calcTax(order) | currency }} {{ final(order) | currency }}
Comments / Notes
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Odit repudiandae numquam sit facere blanditiis, quasi distinctio ipsam? Libero odit ex expedita, facere sunt, possimus consectetur dolore, nobis iure amet vero.
================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/detail/detail.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .no-bottom-margin { margin-bottom: 0; } .nowrap { white-space: nowrap; } .invoice { font-family: Arial, Helvetica, sans-serif; width: 970px !important; margin: 50px auto; .invoice-header { padding: 25px 25px 15px; h1 { margin: 0; } .media { .media-body { font-size: 0.9em; margin: 0; } } } .invoice-body { border-radius: 10px; padding: 25px; background: #fff; } .invoice-footer { padding: 15px; font-size: 0.9em; text-align: center; color: #999; } } .logo { max-height: 70px; border-radius: 10px; } .dl-horizontal { margin: 0; dt { float: left; width: 80px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } dd { margin-left: 90px; } } .rowamount { padding-top: 15px !important; } .rowtotal { font-size: 1.3em; } .colfix { width: 12%; } .mono { font-family: monospace; } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/detail/detail.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Order } from '../models/order.interface'; import { OrderProduct } from '../models/orderproduct.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-detail', templateUrl: './detail.component.html', styleUrls: ['./detail.component.scss'], }) export class DetailComponent implements OnInit { orderId$: Observable | undefined; order$: Observable | undefined; orderProducts$: Observable | undefined; taxRate = 0.0899; constructor(private route: ActivatedRoute, private orderSvc: OrdersService) {} ngOnInit(): void { this.orderId$ = this.route.params.pipe(map((o) => o['orderId'])); this.order$ = this.orderId$.pipe(switchMap((o) => this.orderSvc.get(o))); this.orderProducts$ = this.order$.pipe(map((o) => o.orderProducts)); } today() { return new Date(); } sum(op: OrderProduct) { return op.price * op.quantity; } tax(op: OrderProduct) { return this.sum(op) * this.taxRate; } total(op: OrderProduct) { return this.sum(op) + this.tax(op); } subTotal(order: Order) { return order.orderProducts .map((op) => op.price * op.quantity) .reduce((acc, curr) => acc + curr); } calcTax(order: Order) { return this.subTotal(order) * this.taxRate; } final(order: Order) { return this.subTotal(order) + this.calcTax(order); } } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/list/list.component.html ================================================

Order List

Name {{ element.orderName }} Line Items {{ element.orderProducts?.length }} Total {{ sum(element) | currency }}
================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/list/list.component.scss ================================================ .order-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { displayedColumns: string[] = ['name', 'lineItems', 'total']; orderData: Order[] = []; isLoading: boolean = true; constructor(private orderSvc: OrdersService, private router: Router) {} ngOnInit(): void { this.orderSvc.fetch().subscribe((data) => { this.isLoading = false; this.orderData = data; }); } sum(order: Order): number { return order.orderProducts .map((p) => p.price * p.quantity) .reduce((acc, curr) => acc + curr); } } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/models/order.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { OrderProduct } from './orderproduct.interface'; export interface Order { key: string; orderId: string; orderName: string; orderProducts: OrderProduct[]; } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/models/orderproduct.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface OrderProduct { productId: string; price: number; quantity: number; } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/orders-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', data: { title: 'All Orders', }, component: ListComponent, }, { path: 'create', data: { title: 'Create Order', }, component: CreateComponent, }, { path: 'detail/:orderId', data: { title: 'View Order Detail', }, component: DetailComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class OrdersRoutingModule {} ================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/orders.module.ts ================================================ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; import { OrdersRoutingModule } from './orders-routing.module'; @NgModule({ declarations: [CreateComponent, ListComponent, DetailComponent], imports: [ CommonModule, OrdersRoutingModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class OrdersModule {} ================================================ FILE: Solution/Lab1/client/Application/src/app/views/orders/orders.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Order } from './models/order.interface'; @Injectable({ providedIn: 'root', }) export class OrdersService { orders: Order[] = []; baseUrl = environment.apiGatewayUrl; constructor(private http: HttpClient) {} fetch(): Observable { return this.http.get(`${this.baseUrl}/orders`); } get(orderId: string): Observable { const url = `${this.baseUrl}/order/${orderId}`; return this.http.get(url); } create(order: Order): Observable { return this.http.post(`${this.baseUrl}/order`, order); } } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/create/create.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required SKU Category {{category}}
================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ProductService } from '../product.service'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); } get name() { return this.productForm.get('name'); } get price() { return this.productForm.get('price'); } submit() { this.productSvc.post(this.productForm.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err.message); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/edit/edit.component.html ================================================
Edit Product Product ID Enter product name Name is required Enter product price Price is required SKU Category {{ category }}
================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/edit/edit.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/edit/edit.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.scss'], }) export class EditComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; product$: Observable | undefined; productId$: Observable | undefined; productName$: Observable | undefined; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router, private route: ActivatedRoute ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ shardId: [], productId: [], name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); this.productId$ = this.route.params.pipe(map((p) => p['productId'])); this.product$ = this.productId$.pipe( switchMap((p) => this.productSvc.get(p)) ); this.productName$ = this.product$.pipe(map((p) => p?.name)); this.product$.subscribe((val) => { this.productForm?.patchValue({ ...val, }); }); } get name() { return this.productForm?.get('name'); } get price() { return this.productForm?.get('price'); } submit() { this.productSvc.put(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => console.error(err), }); } delete() { this.productSvc.delete(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/list/list.component.html ================================================

Product List

Name {{ element.name }} Price {{ element.price | currency }} SKU {{ element.sku }}
================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/list/list.component.scss ================================================ .product-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { productData: Product[] = []; isLoading: boolean = true; displayedColumns: string[] = ['name', 'price', 'sku']; constructor(private productSvc: ProductService, private router: Router) {} ngOnInit(): void { this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onEdit(product: Product) { this.router.navigate(['products', 'edit', product.productId]); return false; } onRemove(product: Product) { this.productSvc.delete(product); this.isLoading = true; this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onCreate() { this.router.navigate(['products', 'create']); } } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/models/product.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface Product { key: string; shardId: string; productId: string; name: string; price: number; sku: string; category: string; pictureUrl?: string; } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/product.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Product } from './models/product.interface'; @Injectable({ providedIn: 'root', }) export class ProductService { constructor(private http: HttpClient) {} baseUrl = environment.apiGatewayUrl; fetch(): Observable { return this.http.get(`${this.baseUrl}/products`); } get(productId: string): Observable { const url = `${this.baseUrl}/product/${productId}`; return this.http.get(url); } delete(product: Product) { const url = `${this.baseUrl}/product/${product.productId}`; return this.http.delete(url); } put(product: Product) { const url = `${this.baseUrl}/product/${product.productId}`; return this.http.put(url, product); } post(product: Product) { return this.http.post(`${this.baseUrl}/product`, product); } } ================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/products-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'Product List', }, component: ListComponent, }, { path: 'create', data: { title: 'Create new Product', }, component: CreateComponent, }, { path: 'edit/:productId', data: { title: 'Edit Product', }, component: EditComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class ProductsRoutingModule {} ================================================ FILE: Solution/Lab1/client/Application/src/app/views/products/products.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ProductsRoutingModule } from './products-routing.module'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [CreateComponent, EditComponent, ListComponent], imports: [ CommonModule, ReactiveFormsModule, ProductsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class ProductsModule {} ================================================ FILE: Solution/Lab1/client/Application/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab1/client/Application/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab1/client/Application/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiGatewayUrl: 'https://0grqqki7qk.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab1/client/Application/src/environments/environment.ts ================================================ export const environment = { production: false, apiGatewayUrl: 'https://0grqqki7qk.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab1/client/Application/src/index.html ================================================ Application ================================================ FILE: Solution/Lab1/client/Application/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab1/client/Application/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab1/client/Application/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab1/client/Application/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab1/client/Application/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~bootstrap/scss/functions"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/maps"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; .chart-container canvas { max-height: 250px; width: auto; } .chart-container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; } ================================================ FILE: Solution/Lab1/client/Application/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab1/client/Application/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab1/client/Application/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "strictPropertyInitialization": false, "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab1/client/Application/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab1/scripts/deployment.sh ================================================ #!/bin/bash if [[ "$#" -eq 0 ]]; then echo "Invalid parameters" echo "Command to deploy client code: deployment.sh -c --stack-name " echo "Command to deploy server code: deployment.sh -s --stack-name " echo "Command to deploy server & client code: deployment.sh -s -c --stack-name " exit 1 fi while [[ "$#" -gt 0 ]]; do case $1 in -s) server=1 ;; -c) client=1 ;; --stack-name) stackname=$2 shift ;; *) echo "Unknown parameter passed: $1" exit 1 ;; esac shift done if [[ -z "$stackname" ]]; then echo "Please provide CloudFormation stack name as parameter" echo "Note: Invoke script without parameters to know the list of script parameters" exit 1 fi if [[ $server -eq 1 ]]; then echo "Server code is getting deployed" cd ../server || exit # stop execution if cd fails REGION=$(aws configure get region) DEFAULT_SAM_S3_BUCKET=$(grep s3_bucket samconfig.toml | cut -d'=' -f2 | cut -d \" -f2) echo "aws s3 ls s3://$DEFAULT_SAM_S3_BUCKET" if ! aws s3 ls "s3://${DEFAULT_SAM_S3_BUCKET}"; then echo "S3 Bucket: $DEFAULT_SAM_S3_BUCKET specified in samconfig.toml is not readable. So creating a new S3 bucket and will update samconfig.toml with new bucket name." UUID=$(uuidgen | awk '{print tolower($0)}') SAM_S3_BUCKET=sam-bootstrap-bucket-$UUID aws s3 mb "s3://${SAM_S3_BUCKET}" --region "$REGION" aws s3api put-bucket-encryption \ --bucket "$SAM_S3_BUCKET" \ --server-side-encryption-configuration '{"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]}' if [[ $? -ne 0 ]]; then exit 1 fi # Updating samconfig.toml with new bucket name ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' samconfig.toml fi echo "Validating server code using pylint" python3 -m pylint -E -d E0401 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi sam build -t template.yaml --use-container sam deploy --config-file samconfig.toml --region="$REGION" --stack-name="$stackname" cd ../scripts || exit # stop execution if cd fails fi if [[ $client -eq 1 ]]; then echo "Client code is getting deployed" APP_SITE_BUCKET=$(aws cloudformation describe-stacks --stack-name "$stackname" --query "Stacks[0].Outputs[?OutputKey=='AppBucket'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name "$stackname" --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) APP_APIGATEWAYURL=$(aws cloudformation describe-stacks --stack-name "$stackname" --query "Stacks[0].Outputs[?OutputKey=='APIGatewayURL'].OutputValue" --output text) # Configuring application UI echo "aws s3 ls s3://${APP_SITE_BUCKET}" if ! aws s3 ls "s3://${APP_SITE_BUCKET}"; then echo "Error! S3 Bucket: $APP_SITE_BUCKET not readable" exit 1 fi cd ../client/Application || exit # stop execution if cd fails echo "Configuring environment for App Client" cat <./src/environments/environment.prod.ts export const environment = { production: true, apiGatewayUrl: '$APP_APIGATEWAYURL' }; EoF cat <./src/environments/environment.ts export const environment = { production: true, apiGatewayUrl: '$APP_APIGATEWAYURL' }; EoF npm install && npm run build echo "aws s3 sync --delete --cache-control no-store dist s3://${APP_SITE_BUCKET}" if ! aws s3 sync --delete --cache-control no-store dist "s3://${APP_SITE_BUCKET}"; then exit 1 fi echo "Completed configuring environment for App Client" echo "Application site URL: https://${APP_SITE_URL}" fi ================================================ FILE: Solution/Lab1/scripts/geturl.sh ================================================ #!/bin/bash stackname="serverless-saas-workshop-lab1" APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name "$stackname" --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) echo "Application site URL: https://${APP_SITE_URL}" ================================================ FILE: Solution/Lab1/server/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Solution/Lab1/server/OrderService/order_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Order: key='' def __init__(self, orderId, orderName, orderProducts): self.orderId = orderId self.orderName = orderName self.orderProducts = orderProducts class OrderProduct: def __init__(self, productId, price, quantity): self.productId = productId self.price = price self.quantity = quantity ================================================ FILE: Solution/Lab1/server/OrderService/order_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import order_service_dal from decimal import Decimal from types import SimpleNamespace from aws_lambda_powertools import Tracer def get_order(event, context): logger.info("Request received to get a order") params = event['pathParameters'] orderId = params['id'] order = order_service_dal.get_order(event, orderId) logger.info("Request completed to get a order") return utils.generate_response(order) def create_order(event, context): logger.info("Request received to create a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) order = order_service_dal.create_order(event, payload) logger.info("Request completed to create a order") return utils.generate_response(order) def update_order(event, context): logger.info("Request received to update a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] orderId = params['id'] order = order_service_dal.update_order(event, payload, orderId) logger.info("Request completed to update a order") return utils.generate_response(order) def delete_order(event, context): logger.info("Request received to delete a order") params = event['pathParameters'] orderId = params['id'] response = order_service_dal.delete_order(event, orderId) logger.info("Request completed to delete a order") return utils.create_success_response("Successfully deleted the order") def get_orders(event, context): logger.info("Request received to get all orders") response = order_service_dal.get_orders(event) logger.info("Request completed to get all orders") return utils.generate_response(response) ================================================ FILE: Solution/Lab1/server/OrderService/order_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import boto3 from botocore.exceptions import ClientError import uuid from order_models import Order import json import utils from types import SimpleNamespace import logger import random table_name = os.environ['ORDER_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) def get_order(event, orderId): try: response = table.get_item(Key={'orderId': orderId}) item = response['Item'] order = Order(item['orderId'], item['orderName'], item['orderProducts']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a order', e) else: return order def delete_order(event, orderId): try: response = table.delete_item(Key={'orderId': orderId}) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a order', e) else: logger.info("DeleteItem succeeded:") return response def create_order(event, payload): order = Order(str(uuid.uuid4()), payload.orderName, payload.orderProducts) try: response = table.put_item(Item={ 'orderId': order.orderId, 'orderName': order.orderName, 'orderProducts': get_order_products_dict(order.orderProducts) }) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a order', e) else: logger.info("PutItem succeeded:") return order def update_order(event, payload, orderId): try: order = Order(orderId,payload.orderName, payload.orderProducts) response = table.update_item(Key={'orderId': order.orderId}, UpdateExpression="set orderName=:orderName, " +"orderProducts=:orderProducts", ExpressionAttributeValues={ ':orderName': order.orderName, ':orderProducts': get_order_products_dict(order.orderProducts) }, ReturnValues="UPDATED_NEW") except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a order', e) else: logger.info("UpdateItem succeeded:") return order def get_orders(event): orders = [] try: response = table.scan() if (len(response['Items']) > 0): for item in response['Items']: order = Order(item['orderId'], item['orderName'], item['orderProducts']) orders.append(order) except ClientError as e: logger.error("Error getting all orders") raise Exception('Error getting all orders', e) else: logger.info("Get orders succeeded") return orders def get_order_products_dict(orderProducts): orderProductList = [] for i in range(len(orderProducts)): product = orderProducts[i] orderProductList.append(vars(product)) return orderProductList ================================================ FILE: Solution/Lab1/server/OrderService/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab1/server/ProductService/product_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Product: key ='' def __init__(self, productId, sku, name, price, category): self.productId = productId self.sku = sku self.name = name self.price = price self.category = category class Category: def __init__(self, id, name): self.id = id self.name = name ================================================ FILE: Solution/Lab1/server/ProductService/product_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import product_service_dal from decimal import Decimal from types import SimpleNamespace def get_product(event, context): logger.info("Request received to get a product") params = event['pathParameters'] productId = params['id'] product = product_service_dal.get_product(event, productId) logger.info("Request completed to get a product") return utils.generate_response(product) def create_product(event, context): logger.info("Request received to create a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) logger.info(payload) product = product_service_dal.create_product(event, payload) logger.info("Request completed to create a product") return utils.generate_response(product) def update_product(event, context): logger.info("Request received to update a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] product = product_service_dal.update_product(event, payload, key) logger.info("Request completed to update a product") return utils.generate_response(product) def delete_product(event, context): logger.info("Request received to delete a product") params = event['pathParameters'] key = params['id'] response = product_service_dal.delete_product(event, key) logger.info("Request completed to delete a product") return utils.create_success_response("Successfully deleted the product") def get_products(event, context): logger.info("Request received to get all products") response = product_service_dal.get_products(event) logger.info("Request completed to get all products") return utils.generate_response(response) ================================================ FILE: Solution/Lab1/server/ProductService/product_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import boto3 from botocore.exceptions import ClientError import uuid import logger from product_models import Product from types import SimpleNamespace from boto3.dynamodb.conditions import Key table_name = os.environ['PRODUCT_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) def get_product(event, productId): try: response = table.get_item(Key={'productId': productId}) item = response['Item'] product = Product(item['productId'], item['sku'], item['name'], item['price'], item['category']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a product', e) else: logger.info("GetItem succeeded:"+ str(product)) return product def delete_product(event, productId): try: response = table.delete_item(Key={'productId': productId}) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a product', e) else: logger.info("DeleteItem succeeded:") return response def create_product(event, payload): product = Product(str(uuid.uuid4()), payload.sku,payload.name, payload.price, payload.category) try: response = table.put_item( Item= { 'productId': product.productId, 'sku': product.sku, 'name': product.name, 'price': product.price, 'category': product.category } ) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: logger.info("PutItem succeeded:") return product def update_product(event, payload, productId): try: product = Product(productId,payload.sku, payload.name, payload.price, payload.category) response = table.update_item(Key={'productId': product.productId}, UpdateExpression="set sku=:sku, #n=:productName, price=:price, category=:category", ExpressionAttributeNames= {'#n':'name'}, ExpressionAttributeValues={ ':sku': product.sku, ':productName': product.name, ':price': product.price, ':category': product.category }, ReturnValues="UPDATED_NEW") except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a product', e) else: logger.info("UpdateItem succeeded:") return product def get_products(event): products =[] try: response = table.scan() if (len(response['Items']) > 0): for item in response['Items']: product = Product(item['productId'], item['sku'], item['name'], item['price'], item['category']) products.append(product) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting all products', e) else: logger.info("Get products succeeded") return products ================================================ FILE: Solution/Lab1/server/ProductService/requirements.txt ================================================ requests pytest-mock aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab1/server/README.md ================================================ if using EE sam build --use-container && sam package --output-template-file packaged.yaml --s3-bucket aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx --region us-west-2 sam deploy --template-file packaged.yaml --config-file samconfig.toml Normally sam build -t template.yaml --use-container sam deploy --config-file samconfig.toml ================================================ FILE: Solution/Lab1/server/layers/logger.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from aws_lambda_powertools import Logger logger = Logger() """Log info messages """ def info(log_message): logger.info (log_message) """Log error messages """ def error(log_message): logger.error (log_message) ================================================ FILE: Solution/Lab1/server/layers/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] simplejson jsonpickle ================================================ FILE: Solution/Lab1/server/layers/utils.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import jsonpickle import simplejson import boto3 from enum import Enum class StatusCodes(Enum): SUCCESS = 200 UN_AUTHORIZED = 401 NOT_FOUND = 404 def create_success_response(message): return { "statusCode": StatusCodes.SUCCESS.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def generate_response(inputObject): return { "statusCode": 200, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": encode_to_json_object(inputObject), } def encode_to_json_object(inputObject): jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True) jsonpickle.set_preferred_backend('simplejson') return jsonpickle.encode(inputObject, unpicklable=False, use_decimal=True) ================================================ FILE: Solution/Lab1/server/samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas-lab1" s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx" s3_prefix = "serverless-saas-lab1" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM" ================================================ FILE: Solution/Lab1/server/template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Lab1 - Basic Serverless Application Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG Parameters: StageName: Type: String Default: "prod" Description: "Stage Name for the api" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-workshoplab1 Description: Utilities for project ContentUri: layers/ CompatibleRuntimes: - python3.9 LicenseInfo: 'MIT' RetentionPolicy: Retain Metadata: BuildMethod: python3.9 ProductTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: productId AttributeType: S KeySchema: - AttributeName: productId KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: Product-Lab1 OrderTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: orderId AttributeType: S KeySchema: - AttributeName: orderId KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: Order-Lab1 ProductFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: product-function-execution-role-lab1 Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: product-function-policy-lab1 PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query - dynamodb:Scan Resource: - !GetAtt ProductTable.Arn GetProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable GetProductsFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_products Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable CreateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.create_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable UpdateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.update_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable DeleteProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.delete_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable OrderFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: order-function-execution-role-lab1 Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: order-function-policy-lab1 PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query - dynamodb:Scan Resource: - !GetAtt OrderTable.Arn GetOrdersFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_orders Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable GetOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable CreateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.create_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable UpdateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.update_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable DeleteOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.delete_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable ApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/api-gateway/access-logs-serverless-saas-workshop-lab1-api RetentionInDays: 30 ApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: '/*' HttpMethod: '*' AccessLogSetting: DestinationArn: !GetAtt ApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: 'serverless-saas-workshop-lab1' basePath: !Join ['', ['/', !Ref StageName]] schemes: - https paths: /order/{id}: get: summary: Returns a order description: Return a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a order description: Deletes a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /orders: get: summary: Returns all orders description: Returns all orders. produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrdersFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /order: post: produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product/{id}: get: summary: Returns a product description: Return a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a product description: Deletes a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /products: get: summary: Returns all products description: Returns all products. produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductsFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product: post: produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock StageName: !Ref StageName GetProductsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt GetProductsFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] GetProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] CreateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] UpdateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] DeleteProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] GetOrdersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrdersFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] GetOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] CreateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] UpdateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] DeleteOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayApi, "/*/*/*" ] ] CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distribution" AppBucket: Type: AWS::S3::Bucket DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AppSiteReadPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref AppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId ApplicationSite: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: lab1-tenantapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'AppBucket.RegionalDomainName' Id: lab1-tenantapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: APIGatewayURL: Description: "API Gateway endpoint URL for API" Value: !Join ['', [!Sub "https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/", !Ref StageName]] ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt ApplicationSite.DomainName AppBucket: Description: The name of the bucket for uploading the Tenant Management site to Value: !Ref AppBucket ================================================ FILE: Solution/Lab2/client/Admin/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab2/client/Admin/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab2/client/Admin/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab2/client/Admin/README.md ================================================ # Admin This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab2/client/Admin/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "admin": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "admin:build:production" }, "development": { "browserTarget": "admin:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "admin:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab2/client/Admin/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab2/client/Admin/package.json ================================================ { "name": "admin", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/flex-layout": "~14.0.0-beta.40", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.1.3", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab2/client/Admin/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Tenants', url: '/tenants', icon: 'groups', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Solution/Lab2/client/Admin/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: '', component: NavComponent, data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'tenants', loadChildren: () => import('./views/tenants/tenants.module').then((m) => m.TenantsModule), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab2/client/Admin/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab2/client/Admin/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Solution/Lab2/client/Admin/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { HttpInterceptorProviders } from './interceptors'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; @NgModule({ declarations: [AppComponent, NavComponent, AuthComponent], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, HttpClientModule, HttpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab2/client/Admin/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Solution/Lab2/client/Admin/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const HttpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Solution/Lab2/client/Admin/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Solution/Lab2/client/Admin/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Solution/Lab2/client/Admin/src/app/nav/nav.component.css ================================================ .sidenav-container { height: 100%; } .sidenav-content-container { background-color: lightgray; } .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .mat-toolbar.mat-primary { position: sticky; top: 0; z-index: 1; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .spinner-container { height: 80%; display: flex; justify-content: center; align-items: center; box-sizing: border-box; } ================================================ FILE: Solution/Lab2/client/Admin/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ username$ | async }}
================================================ FILE: Solution/Lab2/client/Admin/src/app/nav/nav.component.spec.ts ================================================ import { LayoutModule } from '@angular/cdk/layout'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { NavComponent } from './nav.component'; describe('NavComponent', () => { let component: NavComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [NavComponent], imports: [ NoopAnimationsModule, LayoutModule, MatButtonModule, MatIconModule, MatListModule, MatSidenavModule, MatToolbarModule, ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(NavComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should compile', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: Solution/Lab2/client/Admin/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.css'], }) export class NavComponent implements OnInit { tenantName = ''; loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router ) { this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => err); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map((sesh) => sesh && sesh.isValid()) ); const token$ = session$.pipe(map((sesh) => sesh && sesh.getIdToken())); this.username$ = token$.pipe( map((t) => t && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Solution/Lab2/client/Admin/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/dashboard/dashboard.component.html ================================================

Traffic

December 2020
================================================ FILE: Solution/Lab2/client/Admin/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } .chart-wrapper { padding-right: 20px; position: relative; margin: auto; height: 80vh; width: 80vw; } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/dashboard/dashboard.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ChartConfiguration, ChartType } from 'chart.js'; import { TenantsService } from '../tenants/tenants.service'; interface DataSet { label: string; data: number[]; } interface ChartData { tenantId: string; dataSet: DataSet[]; totalOrders: number; } @Component({ templateUrl: 'dashboard.component.html', styleUrls: ['./dashboard.component.scss'], selector: 'app-dashboard', }) export class DashboardComponent implements OnInit { constructor(private tenantSvc: TenantsService) {} data: ChartData[] = []; // lineChart public lineChartElements = 27; public lineChartData1: Array = []; public lineChartData2: Array = []; public lineChartData3: Array = []; public lineChartData: Array = [ { data: this.lineChartData1, label: 'Current', backgroundColor: 'rgba(148,159,177,0.2)', borderColor: 'rgba(148,159,177,1)', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, { data: this.lineChartData2, label: 'Previous', backgroundColor: 'rgba(77,83,96,0.2)', borderColor: 'rgba(77,83,96,1)', pointBackgroundColor: 'rgba(77,83,96,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(77,83,96,1)', fill: 'origin', }, { data: this.lineChartData3, label: 'BEP', backgroundColor: 'rgba(255,0,0,0.3)', borderColor: 'red', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, ]; public lineChartLegend = false; /* tslint:disable:max-line-length */ public lineChartLabels: Array = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ]; /* tslint:enable:max-line-length */ public lineChartType: ChartType = 'line'; public lineChartOptions: ChartConfiguration['options'] = { maintainAspectRatio: false, elements: { line: { tension: 0.5, }, }, scales: { // We use this empty structure as a placeholder for dynamic theming. x: {}, 'y-axis-0': { position: 'left', }, 'y-axis-1': { position: 'right', grid: { color: 'rgba(80,80,80,0.3)', }, ticks: { color: '#808080', }, }, }, }; public random(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } ngOnInit(): void { for (let i = 0; i <= this.lineChartElements; i++) { this.lineChartData1.push(this.random(50, 200)); this.lineChartData2.push(this.random(80, 100)); this.lineChartData3.push(65); } } } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/tenants/create/create.component.html ================================================
Provision tenant Name Email Phone Address Plan
================================================ FILE: Solution/Lab2/client/Admin/src/app/views/tenants/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/tenants/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { TenantsService } from '../tenants.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { submitting = false; tenantForm: FormGroup = this.fb.group({ tenantName: [null, [Validators.required]], tenantEmail: [null, [Validators.email, Validators.required]], tenantTier: [null, [Validators.required]], tenantPhone: [null], tenantAddress: [null], }); constructor( private fb: FormBuilder, private tenantSvc: TenantsService, private router: Router, private _snackBar: MatSnackBar ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; const user = { ...this.tenantForm.value, }; this.tenantSvc.post(this.tenantForm.value).subscribe({ next: () => { this.submitting = false; this.openErrorMessageSnackBar('Successfully created new tenant!'); this.router.navigate(['tenants']); }, error: (err) => { this.submitting = false; this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); }, }); } } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/tenants/list/list.component.html ================================================
Tenant List
Id {{ element.tenantId }} Tenant Name {{ element.tenantName }} E-Mail {{ element.tenantEmail }} Plan {{ element.tenantTier }} Status {{ element.isActive }}
================================================ FILE: Solution/Lab2/client/Admin/src/app/views/tenants/list/list.component.scss ================================================ .tenant-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/tenants/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Tenant } from '../models/tenant'; import { TenantsService } from '../tenants.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { tenants$ = new Observable(); tenantData: Tenant[] = []; isLoading: boolean = true; displayedColumns = [ 'tenantId', 'tenantName', 'tenantEmail', 'tenantTier', 'isActive', ]; constructor(private tenantSvc: TenantsService) {} ngOnInit(): void { this.tenantSvc.fetch().subscribe((data) => { this.tenantData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/tenants/models/tenant.ts ================================================ export interface Tenant { tenantId: string; tenantName: string; tenantEmail: string; tenantTier: string; isActive: boolean; } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/tenants/tenants-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', component: ListComponent, data: { title: 'Tenant List', }, }, { path: 'create', component: CreateComponent, data: { title: 'Provision new tenant', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class TenantsRoutingModule {} ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/tenants/tenants.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ListComponent } from './list/list.component'; import { TenantsRoutingModule } from './tenants-routing.module'; import { CreateComponent } from './create/create.component'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, TenantsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ReactiveFormsModule, MatSnackBarModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class TenantsModule {} ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/tenants/tenants.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Tenant } from './models/tenant'; @Injectable({ providedIn: 'root', }) export class TenantsService { constructor(private http: HttpClient) {} baseUrl = `${environment.apiUrl}`; tenantsApiUrl = `${this.baseUrl}/tenants`; registrationApiUrl = `${this.baseUrl}/registration`; // TODO strongly-type these anys as tenants once we dial in what the tenant call should return fetch(): Observable { return this.http.get(this.tenantsApiUrl); } post(tenant: Tenant): Observable { return this.http.post(this.registrationApiUrl, tenant); } } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
Tenant Id folder_special
================================================ FILE: Solution/Lab2/client/Admin/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], tenantId: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Solution/Lab2/client/Admin/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Solution/Lab2/client/Admin/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.apiUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } } ================================================ FILE: Solution/Lab2/client/Admin/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab2/client/Admin/src/aws-exports.ts ================================================ const awsmobile = { aws_project_region: 'us-west-2', aws_cognito_region: 'us-west-2', aws_user_pools_id: 'us-west-2_nWhwjijMc', aws_user_pools_web_client_id: '61ipvhmvdrfso0cftpil9irk8k', }; export default awsmobile; ================================================ FILE: Solution/Lab2/client/Admin/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab2/client/Admin/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab2/client/Admin/src/environments/environment.ts ================================================ export const environment = { production: false, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab2/client/Admin/src/index.html ================================================ Dashboard ================================================ FILE: Solution/Lab2/client/Admin/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { Amplify } from 'aws-amplify'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import aws_exports from './aws-exports'; Amplify.configure(aws_exports); if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab2/client/Admin/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab2/client/Admin/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab2/client/Admin/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab2/client/Admin/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; ================================================ FILE: Solution/Lab2/client/Admin/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab2/client/Admin/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab2/client/Admin/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab2/client/Admin/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab2/client/Landing/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab2/client/Landing/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab2/client/Landing/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab2/client/Landing/README.md ================================================ # Landing This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab2/client/Landing/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "Landing": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/custom-theme.scss", "src/styles.scss", "node_modules/font-awesome/css/font-awesome.min.css" ] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "10kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "Landing:build:production" }, "development": { "browserTarget": "Landing:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "Landing:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab2/client/Landing/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab2/client/Landing/package.json ================================================ { "name": "landing", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "bootstrap": "~5.1.3", "font-awesome": "~4.7.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab2/client/Landing/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; export const routes: Routes = [ { path: '', redirectTo: 'landing', pathMatch: 'full', }, { path: 'landing', component: LandingComponent, }, { path: 'register', component: RegisterComponent, }, { path: '**', redirectTo: 'landing', pathMatch: 'full', }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab2/client/Landing/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab2/client/Landing/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'landing'; } ================================================ FILE: Solution/Lab2/client/Landing/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; @NgModule({ declarations: [AppComponent, LandingComponent, RegisterComponent], imports: [ AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, MatButtonModule, MatCardModule, MatIconModule, MatProgressSpinnerModule, MatSidenavModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, FormsModule, HttpClientModule, ReactiveFormsModule, MatSnackBarModule, MatInputModule, MatFormFieldModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab2/client/Landing/src/app/views/landing/landing.component.html ================================================

Serverless SaaS Reference Architecture

It's so nice it blows your mind.

Sign up now!

Serverless SaaS Reference Architecture is so awesome.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere temporibus omnis illum, officia. Architecto voluptatibus commodi voluptatem perspiciatis eos possimus, eius at molestias quaerat magnam? Odio qui quos ipsam natus.

Serverless SaaS Reference Architecture so awesome. Makes you awesome - go sign up!

Serverless SaaS Reference Architecture so great. Makes you even greater - go sign up now. Super cheap deal!

Feel lonely? Go sign up and have a friend!

Take Serverless SaaS Reference Architecture with you everywhere you go.

Serverless SaaS Reference Architecture is all you need. Anywhere - ever. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita sapiente hic voluptatum quo sunt totam accusamus distinctio minus aliquid quis!

Love Serverless SaaS Reference Architecture. So nice! So good! Could not live without!

Satisfied Customer

Reasons to sign up this product:

  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!
  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!

Why you still reading?

Sign up now!
================================================ FILE: Solution/Lab2/client/Landing/src/app/views/landing/landing.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // $color-red: #cc615f; $color-grey: #4b4b4b; $color-grey--light: #d3d3d3; $btn-amzn: #ec7211; $color-primary: #232f3e; $bp-s: 65.75em; //700px; $bp-xs: 34.375em; //550px; @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,800,300); *{ box-sizing: border-box; } html{ width: 100%; height:100%; margin: 0; padding: 0; } body{ width: 100%; height:100%; font-family: 'Open Sans','Helvetica Neue',Helvetica, sans-serif; font-size: 100%; line-height: 1.45; color: #141414; } a{ text-decoration: none; &:hover{ text-decoration: none; } } img{ max-width: 100%; } .btn{ display: inline-block; margin: 1rem 0; color: white; font-weight: 500; font-size: 1.3rem; background: $btn-amzn; letter-spacing: .02em; border: none; border-radius: 5px; padding: .8rem 1rem .9rem; text-shadow: 0 1px rgba(black,.3); box-shadow: 0 0 2px rgba(black,.2); @media (max-width: $bp-s){ padding: .5rem .7rem .6rem; font-size: 1rem; } &:hover{ background: lighten($btn-amzn,5%); color: #fff; } &:focus, &:active, &:focus:active{ background: darken($btn-amzn,5%); border-color: darken($btn-amzn,5%); box-shadow: 0 2px 5px 0 rgba(black,.5) inset; } } .container{ margin: 0 auto; width: 90%; max-width: 900px; } header{ color: white; background: #232f3e; padding: 10rem 0; text-align: center; position: relative; z-index: 1; overflow: hidden; @media (max-width: $bp-s){ padding: 2rem 0; } h1{ font-size: 3rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 2rem; } } h2{ font-weight: 300; font-size: 1.5rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } } section{ background: #fff; color: $color-grey; padding: 3.5rem 0; @media (max-width: $bp-s){ padding: 2rem 0; } &.section--primary{ background: $color-primary; color: #fff; } &.section--primary--alt{ background: desaturate(lighten($color-primary,15%),10%); color: #fff; } &.section--primary--light{ background: rgba($color-primary,.1); } &.section--grey{ background: $color-grey; color: #fff; } &.section--grey--light{ background: $color-grey--light; color: #fff; } h3{ text-align: center; font-size: 2rem; font-weight: 300; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } li{ font-size: 1.2rem; font-weight: 300; } p{ font-size: 1.2rem; font-weight: 300; } } .col{ margin: 0 1.5%; display: inline-block; vertical-align: top; } .col-7{ @extend .col; width: 64%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-3{ @extend .col; width: 29%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-5{ @extend .col; width: 30%; @media (max-width: $bp-xs){ width: 60%; margin: 0; } } .details{ text-align: left; h3{ font-size: 2rem; text-align: left; } } .features{ text-align: center; padding: 1rem; &:hover{ background: rgba(white,.1); } @media (max-width: $bp-s){ width: 100%; margin: 0; text-align: left; border-bottom: 1px solid rgba(white,.2); &:last-child{ border: none; } } i{ font-size: 4rem; margin: 0 0 2rem 0; @media (max-width: $bp-s){ font-size: 1.5rem; width: 2rem; text-align: center; margin: 0 0 1rem 0; float: left; } } p{ margin: 0 0 1rem 0; font-size: 1rem; @media (max-width: $bp-s){ margin-left: 3rem; } } } blockquote{ position: relative; margin: 0; padding: 0; text-align: center; &:before{ display: inline-block; color: $color-primary; font-size: 2rem; content: '\201C'; } p{ margin: 0; display: inline; font-size: 1.5rem; @media (max-width: $bp-s){ font-size: 1.2rem; } } cite{ font-size: 1rem; display: block; margin: .5rem 0 0 1.2rem; @media (max-width: $bp-s){ font-size: .8rem; } &:before{ content: '–'; } } } footer{ background: $color-primary; color: #fff; padding: 2rem 0; text-align: center; font-size: .8rem; color: rgba(white,.4); ul{ margin: 0; padding: 0; list-style: none; li{ display: inline-block; a{ display: block; padding: .4rem .7rem; font-size: .9rem; text-decoration: none; color: rgba(white,.7); &:hover{ color: white; } } } } } .text--center{ text-align: center; } .text--left{ text-align: left; } .bg-image{ background: $color-primary; text-align: center; position: relative; z-index: 1; overflow: hidden; //text-shadow: 2px 0 5px black; &:before{ content: ''; display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; z-index: -1; background-size: 100%; background-attachment: fixed; filter: blur(3px); opacity: .8; transform: scale(1.1); } &.bg-image-2:before{ opacity: .6; background-position: center center; } } ================================================ FILE: Solution/Lab2/client/Landing/src/app/views/landing/landing.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-landing', templateUrl: './landing.component.html', styleUrls: ['./landing.component.scss'], }) export class LandingComponent implements OnInit { constructor(private router: Router) {} ngOnInit() {} register() { this.router.navigate(['register']); return false; } } ================================================ FILE: Solution/Lab2/client/Landing/src/app/views/register/register.component.html ================================================
Provision a new Tenant Login details will be sent to the email provided. Name Email Phone Address Plan
================================================ FILE: Solution/Lab2/client/Landing/src/app/views/register/register.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .tenant-form { display: flex; align-items: center; justify-content: center; } .wide-form { min-width: 150px; max-width: 500px; width: 100%; } .full-width { width: 100%; } .mat-form-field { width: 100%; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab2/client/Landing/src/app/views/register/register.component.ts ================================================ import { MatSnackBar } from '@angular/material/snack-bar'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.scss'], }) export class RegisterComponent implements OnInit { submitting: boolean = false; tenantForm: FormGroup = this.fb.group({ tenantName: ['', [Validators.required]], tenantEmail: ['', [Validators.email, Validators.required]], tenantTier: ['', [Validators.required]], tenantPhone: [''], tenantAddress: [''], }); constructor( private fb: FormBuilder, private _snackBar: MatSnackBar, private http: HttpClient ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; this.tenantForm.disable(); const tenant = { ...this.tenantForm.value, }; this.http .post(`${environment.apiGatewayUrl}/registration`, tenant) .subscribe({ next: () => { this.openErrorMessageSnackBar('Successfully created new tenant!'); this.tenantForm.reset(); this.tenantForm.enable(); this.submitting = false; }, error: (err) => { this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); this.tenantForm.enable(); this.submitting = false; }, }); } } ================================================ FILE: Solution/Lab2/client/Landing/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab2/client/Landing/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab2/client/Landing/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab2/client/Landing/src/environments/environment.ts ================================================ export const environment = { production: false, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab2/client/Landing/src/index.html ================================================ Landing ================================================ FILE: Solution/Lab2/client/Landing/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab2/client/Landing/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab2/client/Landing/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab2/client/Landing/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab2/client/Landing/src/styles.scss ================================================ ================================================ FILE: Solution/Lab2/client/Landing/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab2/client/Landing/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab2/client/Landing/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab2/client/Landing/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab2/scripts/deploy-updates.sh ================================================ #!/bin/bash cd ../server || exit # stop execution if cd fails rm -rf .aws-sam/ python3 -m pylint -E -d E0401 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi #Deploying shared services changes echo "Deploying shared services changes" echo Y | sam sync --stack-name serverless-saas --code --resource-id LambdaFunctions/CreateUserFunction --resource-id LambdaFunctions/RegisterTenantFunction --resource-id LambdaFunctions/GetTenantFunction -u cd ../scripts || exit ./geturl.sh ================================================ FILE: Solution/Lab2/scripts/deployment.sh ================================================ #!/bin/bash if [[ "$#" -eq 0 ]]; then echo "Invalid parameters" echo "Command to deploy client code: deployment.sh -c --email " echo "Command to deploy server code: deployment.sh -s --email " echo "Command to deploy server & client code: deployment.sh -s -c --email " exit 1 fi while [[ "$#" -gt 0 ]]; do case $1 in -s) server=1 ;; -c) client=1 ;; --email) email=$2 shift ;; *) echo "Unknown parameter passed: $1" exit 1 ;; esac shift done # During AWS hosted events using event engine tool # we pre-provision cloudfront and s3 buckets which hosts UI code. # So that it improves this labs total execution time. # Below code checks if cloudfront and s3 buckets are # pre-provisioned or not and then concludes if the workshop # is running in AWS hosted event through event engine tool or not. IS_RUNNING_IN_EVENT_ENGINE=false PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" IS_RUNNING_IN_EVENT_ENGINE=true ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) ADMIN_SITE_BUCKET=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminSiteBucket'].Value" --output text) LANDING_APP_SITE_BUCKET=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSiteBucket'].Value" --output text) fi if [[ $server -eq 1 ]]; then echo "Server code is getting deployed" cd ../server || exit # stop execution if cd fails REGION=$(aws configure get region) DEFAULT_SAM_S3_BUCKET=$(grep s3_bucket samconfig.toml | cut -d'=' -f2 | cut -d \" -f2) echo "aws s3 ls s3://$DEFAULT_SAM_S3_BUCKET" if ! aws s3 ls "s3://${DEFAULT_SAM_S3_BUCKET}"; then echo "S3 Bucket: $DEFAULT_SAM_S3_BUCKET specified in samconfig.toml is not readable. So creating a new S3 bucket and will update samconfig.toml with new bucket name." UUID=$(uuidgen | awk '{print tolower($0)}') SAM_S3_BUCKET=sam-bootstrap-bucket-$UUID aws s3 mb "s3://${SAM_S3_BUCKET}" --region "$REGION" aws s3api put-bucket-encryption \ --bucket "$SAM_S3_BUCKET" \ --server-side-encryption-configuration '{"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]}' if [[ $? -ne 0 ]]; then exit 1 fi # Updating all labs samconfig.toml with new bucket name ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab3/server/shared-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab3/server/tenant-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab4/server/shared-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab4/server/tenant-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab5/server/shared-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab5/server/tenant-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab6/server/shared-samconfig.toml ex -sc '%s/s3_bucket = .*/s3_bucket = \"'$SAM_S3_BUCKET'\"/|x' ../../Lab6/server/tenant-samconfig.toml fi echo "Validating server code using pylint" python3 -m pylint -E -d E0401 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi sam build -t template.yaml --use-container if [ "$IS_RUNNING_IN_EVENT_ENGINE" = true ]; then sam deploy --config-file samconfig.toml --region="$REGION" --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE AdminUserPoolCallbackURLParameter=$ADMIN_SITE_URL else sam deploy --config-file samconfig.toml --region="$REGION" --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE fi cd ../scripts || exit # stop execution if cd fails fi if [ "$IS_RUNNING_IN_EVENT_ENGINE" = false ]; then ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) ADMIN_SITE_BUCKET=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminSiteBucket'].OutputValue" --output text) LANDING_APP_SITE_BUCKET=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSiteBucket'].OutputValue" --output text) fi if [[ $client -eq 1 ]]; then if [[ -z "$email" ]]; then echo "Please provide email address to setup an admin user" echo "Note: Invoke script without parameters to know the list of script parameters" exit 1 fi echo "Client code is getting deployed" ADMIN_APIGATEWAYURL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminApi'].OutputValue" --output text) ADMIN_APPCLIENTID=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='CognitoOperationUsersUserPoolClientId'].OutputValue" --output text) ADMIN_USERPOOL_ID=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='CognitoOperationUsersUserPoolId'].OutputValue" --output text) ADMIN_USER_GROUP_NAME=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='CognitoAdminUserGroupName'].OutputValue" --output text) # Create admin-user in OperationUsers userpool with given input email address CREATE_ADMIN_USER=$(aws cognito-idp admin-create-user \ --user-pool-id "$ADMIN_USERPOOL_ID" \ --username admin-user \ --user-attributes Name=email,Value="$email" Name=email_verified,Value="True" Name=phone_number,Value="+11234567890" Name="custom:userRole",Value="SystemAdmin" Name="custom:tenantId",Value="system_admins" \ --desired-delivery-mediums EMAIL) echo "$CREATE_ADMIN_USER" # Add admin-user to admin user group ADD_ADMIN_USER_TO_GROUP=$(aws cognito-idp admin-add-user-to-group \ --user-pool-id "$ADMIN_USERPOOL_ID" \ --username admin-user \ --group-name "$ADMIN_USER_GROUP_NAME") echo "$ADD_ADMIN_USER_TO_GROUP" # Configuring admin UI echo "aws s3 ls s3://$ADMIN_SITE_BUCKET" if ! aws s3 ls "s3://${ADMIN_SITE_BUCKET}"; then echo "Error! S3 Bucket: $ADMIN_SITE_BUCKET not readable" exit 1 fi cd ../client/Admin || exit # stop execution if cd fails echo "Configuring environment for Admin Client" cat <./src/environments/environment.prod.ts export const environment = { production: true, apiUrl: '$ADMIN_APIGATEWAYURL', }; EoF cat <./src/environments/environment.ts export const environment = { production: false, apiUrl: '$ADMIN_APIGATEWAYURL', }; EoF cat <./src/aws-exports.ts const awsmobile = { "aws_project_region": "$REGION", "aws_cognito_region": "$REGION", "aws_user_pools_id": "$ADMIN_USERPOOL_ID", "aws_user_pools_web_client_id": "$ADMIN_APPCLIENTID", }; export default awsmobile; EoF npm install && npm run build echo "aws s3 sync --delete --cache-control no-store dist s3://${ADMIN_SITE_BUCKET}" aws s3 sync --delete --cache-control no-store dist "s3://${ADMIN_SITE_BUCKET}" if [[ $? -ne 0 ]]; then exit 1 fi echo "Completed configuring environment for Admin Client" # Configuring landing UI echo "aws s3 ls s3://${LANDING_APP_SITE_BUCKET}" if ! aws s3 ls "s3://${LANDING_APP_SITE_BUCKET}"; then echo "Error! S3 Bucket: $LANDING_APP_SITE_BUCKET not readable" exit 1 fi cd ../ cd Landing || exit # stop execution if cd fails echo "Configuring environment for Landing Client" cat <./src/environments/environment.prod.ts export const environment = { production: true, apiGatewayUrl: '$ADMIN_APIGATEWAYURL' }; EoF cat <./src/environments/environment.ts export const environment = { production: false, apiGatewayUrl: '$ADMIN_APIGATEWAYURL' }; EoF npm install && npm run build echo "aws s3 sync --delete --cache-control no-store dist s3://${LANDING_APP_SITE_BUCKET}" aws s3 sync --delete --cache-control no-store dist "s3://${LANDING_APP_SITE_BUCKET}" if [[ $? -ne 0 ]]; then exit 1 fi echo "Completed configuring environment for Landing Client" echo "Successfully completed deploying Admin UI and Landing UI" fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" ================================================ FILE: Solution/Lab2/scripts/geturl.sh ================================================ #!/bin/bash PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) else ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" ================================================ FILE: Solution/Lab2/server/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Solution/Lab2/server/OrderService/order_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Order: key='' def __init__(self, orderId, orderName, orderProducts): self.orderId = orderId self.orderName = orderName self.orderProducts = orderProducts class OrderProduct: def __init__(self, productId, price, quantity): self.productId = productId self.price = price self.quantity = quantity ================================================ FILE: Solution/Lab2/server/OrderService/order_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import order_service_dal from decimal import Decimal from types import SimpleNamespace from aws_lambda_powertools import Tracer def get_order(event, context): logger.info("Request received to get a order") params = event['pathParameters'] orderId = params['id'] order = order_service_dal.get_order(event, orderId) logger.info("Request completed to get a order") return utils.generate_response(order) def create_order(event, context): logger.info("Request received to create a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) order = order_service_dal.create_order(event, payload) logger.info("Request completed to create a order") return utils.generate_response(order) def update_order(event, context): logger.info("Request received to update a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] orderId = params['id'] order = order_service_dal.update_order(event, payload, orderId) logger.info("Request completed to update a order") return utils.generate_response(order) def delete_order(event, context): logger.info("Request received to delete a order") params = event['pathParameters'] orderId = params['id'] response = order_service_dal.delete_order(event, orderId) logger.info("Request completed to delete a order") return utils.create_success_response("Successfully deleted the order") def get_orders(event, context): logger.info("Request received to get all orders") response = order_service_dal.get_orders(event) logger.info("Request completed to get all orders") return utils.generate_response(response) ================================================ FILE: Solution/Lab2/server/OrderService/order_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import boto3 from botocore.exceptions import ClientError import uuid from order_models import Order import json import utils from types import SimpleNamespace import logger import random table_name = os.environ['ORDER_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) def get_order(event, orderId): try: response = table.get_item(Key={'orderId': orderId}) item = response['Item'] order = Order(item['orderId'], item['orderName'], item['orderProducts']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a order', e) else: return order def delete_order(event, orderId): try: response = table.delete_item(Key={'orderId': orderId}) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a order', e) else: logger.info("DeleteItem succeeded:") return response def create_order(event, payload): order = Order(str(uuid.uuid4()), payload.orderName, payload.orderProducts) try: response = table.put_item(Item={ 'orderId': order.orderId, 'orderName': order.orderName, 'orderProducts': get_order_products_dict(order.orderProducts) }) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a order', e) else: logger.info("PutItem succeeded:") return order def update_order(event, payload, orderId): try: order = Order(orderId,payload.orderName, payload.orderProducts) response = table.update_item(Key={'orderId': order.orderId}, UpdateExpression="set orderName=:orderName, " +"orderProducts=:orderProducts", ExpressionAttributeValues={ ':orderName': order.orderName, ':orderProducts': get_order_products_dict(order.orderProducts) }, ReturnValues="UPDATED_NEW") except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a order', e) else: logger.info("UpdateItem succeeded:") return order def get_orders(event): orders = [] try: response = table.scan() if (len(response['Items']) > 0): for item in response['Items']: order = Order(item['orderId'], item['orderName'], item['orderProducts']) orders.append(order) except ClientError as e: logger.error("Error getting all orders") raise Exception('Error getting all orders', e) else: logger.info("Get orders succeeded") return orders def get_order_products_dict(orderProducts): orderProductList = [] for i in range(len(orderProducts)): product = orderProducts[i] orderProductList.append(vars(product)) return orderProductList ================================================ FILE: Solution/Lab2/server/OrderService/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab2/server/ProductService/product_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Product: key ='' def __init__(self, productId, sku, name, price, category): self.productId = productId self.sku = sku self.name = name self.price = price self.category = category class Category: def __init__(self, id, name): self.id = id self.name = name ================================================ FILE: Solution/Lab2/server/ProductService/product_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import product_service_dal from decimal import Decimal from types import SimpleNamespace def get_product(event, context): logger.info("Request received to get a product") params = event['pathParameters'] productId = params['id'] product = product_service_dal.get_product(event, productId) logger.info("Request completed to get a product") return utils.generate_response(product) def create_product(event, context): logger.info("Request received to create a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) logger.info(payload) product = product_service_dal.create_product(event, payload) logger.info("Request completed to create a product") return utils.generate_response(product) def update_product(event, context): logger.info("Request received to update a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] product = product_service_dal.update_product(event, payload, key) logger.info("Request completed to update a product") return utils.generate_response(product) def delete_product(event, context): logger.info("Request received to delete a product") params = event['pathParameters'] key = params['id'] response = product_service_dal.delete_product(event, key) logger.info("Request completed to delete a product") return utils.create_success_response("Successfully deleted the product") def get_products(event, context): logger.info("Request received to get all products") response = product_service_dal.get_products(event) logger.info("Request completed to get all products") return utils.generate_response(response) ================================================ FILE: Solution/Lab2/server/ProductService/product_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import boto3 from botocore.exceptions import ClientError import uuid import logger from product_models import Product from types import SimpleNamespace from boto3.dynamodb.conditions import Key table_name = os.environ['PRODUCT_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) def get_product(event, productId): try: response = table.get_item(Key={'productId': productId}) item = response['Item'] product = Product(item['productId'], item['sku'], item['name'], item['price'], item['category']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a product', e) else: logger.info("GetItem succeeded:"+ str(product)) return product def delete_product(event, productId): try: response = table.delete_item(Key={'productId': productId}) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a product', e) else: logger.info("DeleteItem succeeded:") return response def create_product(event, payload): product = Product(str(uuid.uuid4()), payload.sku,payload.name, payload.price, payload.category) try: response = table.put_item( Item= { 'productId': product.productId, 'sku': product.sku, 'name': product.name, 'price': product.price, 'category': product.category } ) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: logger.info("PutItem succeeded:") return product def update_product(event, payload, productId): try: product = Product(productId,payload.sku, payload.name, payload.price, payload.category) response = table.update_item(Key={'productId': product.productId}, UpdateExpression="set sku=:sku, #n=:productName, price=:price, category=:category", ExpressionAttributeNames= {'#n':'name'}, ExpressionAttributeValues={ ':sku': product.sku, ':productName': product.name, ':price': product.price, ':category': product.category }, ReturnValues="UPDATED_NEW") except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a product', e) else: logger.info("UpdateItem succeeded:") return product def get_products(event): products =[] try: response = table.scan() if (len(response['Items']) > 0): for item in response['Items']: product = Product(item['productId'], item['sku'], item['name'], item['price'], item['category']) products.append(product) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting all products', e) else: logger.info("Get products succeeded") return products ================================================ FILE: Solution/Lab2/server/ProductService/requirements.txt ================================================ requests pytest-mock aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab2/server/README.md ================================================ if using EE sam build --use-container && sam package --output-template-file packaged.yaml --s3-bucket aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx --region us-west-2 sam deploy --template-file packaged.yaml --config-file samconfig.toml Normally sam build -t template.yaml --use-container sam deploy --config-file samconfig.toml ================================================ FILE: Solution/Lab2/server/Resources/requirements.txt ================================================ python-jose[cryptography] ================================================ FILE: Solution/Lab2/server/Resources/shared_service_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import utils region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) userpool_id = user_pool_operation_user appclient_id = app_client_operation_user #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] policy.allowAllMethods() authResponse = policy.build() context = { 'userName': user_name, 'userPoolId': userpool_id } authResponse['context'] = context return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Solution/Lab2/server/TenantManagementService/requirements.txt ================================================ requests aws_requests_auth ================================================ FILE: Solution/Lab2/server/TenantManagementService/tenant-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import json import boto3 from boto3.dynamodb.conditions import Key import urllib.parse import utils from botocore.exceptions import ClientError import logger import requests region = os.environ['AWS_REGION'] dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') #This method has been locked down to be only called from tenant registration service def create_tenant(event, context): tenant_details = json.loads(event['body']) try: response = table_tenant_details.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'tenantName' : tenant_details['tenantName'], 'tenantAddress': tenant_details['tenantAddress'], 'tenantEmail': tenant_details['tenantEmail'], 'tenantPhone': tenant_details['tenantPhone'], 'tenantTier': tenant_details['tenantTier'], 'isActive': True } ) except Exception as e: raise Exception('Error creating a new tenant', e) else: return utils.create_success_response("Tenant Created") def get_tenants(event, context): try: response = table_tenant_details.scan() except Exception as e: raise Exception('Error getting all tenants', e) else: return utils.generate_response(response['Items']) def update_tenant(event, context): tenant_details = json.loads(event['body']) tenant_id = event['pathParameters']['tenantid'] logger.info("Request received to update tenant") response_update = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set tenantName = :tenantName, tenantAddress = :tenantAddress, tenantEmail = :tenantEmail, tenantPhone = :tenantPhone, tenantTier=:tenantTier", ExpressionAttributeValues={ ':tenantName' : tenant_details['tenantName'], ':tenantAddress': tenant_details['tenantAddress'], ':tenantEmail': tenant_details['tenantEmail'], ':tenantPhone': tenant_details['tenantPhone'], ':tenantTier': tenant_details['tenantTier'] }, ReturnValues="UPDATED_NEW" ) logger.info(response_update) logger.info("Request completed to update tenant") return utils.create_success_response("Tenant Updated") def get_tenant(event, context): tenant_id = event['pathParameters']['tenantid'] logger.info("Request received to get tenant details") tenant_details = table_tenant_details.get_item( Key={ 'tenantId': tenant_id, }, AttributesToGet=[ 'tenantName', 'tenantAddress', 'tenantEmail', 'tenantPhone' ] ) item = tenant_details['Item'] tenant_info = TenantInfo(item['tenantName'], item['tenantAddress'],item['tenantEmail'], item['tenantPhone']) logger.info(tenant_info) logger.info("Request completed to get tenant details") return utils.create_success_response(tenant_info.__dict__) def deactivate_tenant(event, context): url_disable_users = os.environ['DISABLE_USERS_BY_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) tenant_id = event['pathParameters']['tenantid'] logger.info("Request received to deactivate tenant") response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': False }, ReturnValues="ALL_NEW" ) logger.info(response) update_user_response = __invoke_disable_users(headers, auth, host, stage_name, url_disable_users, tenant_id) logger.info(update_user_response) logger.info("Request completed to deactivate tenant") return utils.create_success_response("Tenant Deactivated") def activate_tenant(event, context): url_enable_users = os.environ['ENABLE_USERS_BY_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) tenant_id = event['pathParameters']['tenantid'] logger.info("Request received to activate tenant") response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': True }, ReturnValues="ALL_NEW" ) logger.info(response) update_user_response = __invoke_enable_users(headers, auth, host, stage_name, url_enable_users, tenant_id) logger.info(update_user_response) logger.info("Request completed to activate tenant") return utils.create_success_response("Tenant activated") def __invoke_disable_users(headers, auth, host, stage_name, invoke_url, tenant_id): try: url = ''.join(['https://', host, '/', stage_name, invoke_url, '/', tenant_id]) response = requests.put(url, auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while disabling users for the tenant') except Exception as e: logger.error('Error occured while disabling users for the tenant') raise Exception('Error occured while disabling users for the tenant', e) else: return "Success invoking disable users" def __invoke_enable_users(headers, auth, host, stage_name, invoke_url, tenant_id): try: url = ''.join(['https://', host, '/', stage_name, invoke_url, '/', tenant_id]) response = requests.put(url, auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while enabling users for the tenant') except Exception as e: logger.error('Error occured while enabling users for the tenant') raise Exception('Error occured while enabling users for the tenant', e) else: return "Success invoking enable users" class TenantInfo: def __init__(self, tenant_name, tenant_address, tenant_email, tenant_phone): self.tenant_name = tenant_name self.tenant_address = tenant_address self.tenant_email = tenant_email self.tenant_phone = tenant_phone ================================================ FILE: Solution/Lab2/server/TenantManagementService/tenant-registration.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import utils import uuid import logger import requests import re region = os.environ['AWS_REGION'] create_tenant_admin_user_resource_path = os.environ['CREATE_TENANT_ADMIN_USER_RESOURCE_PATH'] create_tenant_resource_path = os.environ['CREATE_TENANT_RESOURCE_PATH'] lambda_client = boto3.client('lambda') def register_tenant(event, context): try: tenant_id = uuid.uuid1().hex tenant_details = json.loads(event['body']) tenant_details['tenantId'] = tenant_id logger.info(tenant_details) stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) create_user_response = __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name) logger.info (create_user_response) tenant_details['tenantAdminUserName'] = create_user_response['message']['tenantAdminUserName'] create_tenant_response = __create_tenant(tenant_details, headers, auth, host, stage_name) logger.info (create_tenant_response) except Exception as e: logger.error('Error registering a new tenant') raise Exception('Error registering a new tenant', e) else: return utils.create_success_response("You have been registered in our system") def __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_admin_user_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while calling the create tenant admin user service') raise Exception('Error occured while calling the create tenant admin user service', e) else: return response_json def __create_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_resource_path]) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while creating the tenant record in table') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json ================================================ FILE: Solution/Lab2/server/TenantManagementService/user-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import sys import logger import utils from boto3.dynamodb.conditions import Key client = boto3.client('cognito-idp') dynamodb = boto3.resource('dynamodb') table_tenant_user_map = dynamodb.Table('ServerlessSaaS-TenantUserMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_id = os.environ['TENANT_USER_POOL_ID'] def create_tenant_admin_user(event, context): logger.info(event) app_client_id = os.environ['TENANT_APP_CLIENT_ID'] tenant_details = json.loads(event['body']) tenant_id = tenant_details['tenantId'] logger.info(tenant_details) user_mgmt = UserManagement() #Add tenant admin now based upon user pool tenant_user_group_response = user_mgmt.create_user_group(user_pool_id,tenant_id,"User group for tenant {0}".format(tenant_id)) tenant_admin_user_name = 'tenant-admin-{0}'.format(tenant_details['tenantId']) create_tenant_admin_response = user_mgmt.create_tenant_admin(user_pool_id, tenant_admin_user_name, tenant_details) add_tenant_admin_to_group_response = user_mgmt.add_user_to_group(user_pool_id, tenant_admin_user_name, tenant_user_group_response['Group']['GroupName']) tenant_user_mapping_response = user_mgmt.create_user_tenant_mapping(tenant_admin_user_name,tenant_id) response = {"userPoolId": user_pool_id, "appClientId": app_client_id, "tenantAdminUserName": tenant_admin_user_name} return utils.create_success_response(response) #only tenant admin can create users def create_user(event, context): user_details = json.loads(event['body']) logger.info("Request received to create new user") logger.info(event) tenant_id = user_details['tenantId'] response = client.admin_create_user( Username=user_details['userName'], UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] }, { 'Name': 'custom:tenantId', 'Value': tenant_id } ] ) logger.info(response) user_mgmt = UserManagement() user_mgmt.add_user_to_group(user_pool_id, user_details['userName'], tenant_id) response_mapping = user_mgmt.create_user_tenant_mapping(user_details['userName'], tenant_id) logger.info("Request completed to create new user ") return utils.create_success_response("New user created") def get_users(event, context): users = [] logger.info("Request received to get users") logger.info(event) response = client.list_users( UserPoolId=user_pool_id ) logger.info(response) num_of_users = len(response['Users']) if (num_of_users > 0): for user in response['Users']: user_info = UserInfo() for attr in user["Attributes"]: if(attr["Name"] == "custom:tenantId"): user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] user_info.enabled = user["Enabled"] user_info.created = user["UserCreateDate"] user_info.modified = user["UserLastModifiedDate"] user_info.status = user["UserStatus"] user_info.user_name = user["Username"] users.append(user_info) # return an empty list when there are no users otherwise will result in API Gateway error return utils.generate_response(users) def get_user(event, context): user_name = event['pathParameters']['username'] logger.info("Request received to get user") user_info = get_user_info(user_pool_id, user_name) logger.info("Request completed to get new user ") return utils.create_success_response(user_info.__dict__) def update_user(event, context): user_details = json.loads(event['body']) user_name = event['pathParameters']['username'] logger.info("Request received to update user") response = client.admin_update_user_attributes( Username=user_name, UserPoolId=user_pool_id, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] } ] ) logger.info(response) logger.info("Request completed to update user") return utils.create_success_response("user updated") def disable_user(event, context): user_name = event['pathParameters']['username'] logger.info("Request received to disable new user") response = client.admin_disable_user( Username=user_name, UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to disable new user") return utils.create_success_response("User disabled") #This method uses IAM Authorization and protected using a resource policy. This method is also invoked async def disable_users_by_tenant(event, context): logger.info("Request received to disable users by tenant") logger.info(event) tenantid_to_update = event['tenantid'] filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_disable_user( Username=user['userName'], UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to disable users") return utils.create_success_response("Users disabled") #This method uses IAM Authorization and protected using a resource policy. This method is also invoked async def enable_users_by_tenant(event, context): logger.info("Request received to enable users by tenant") logger.info(event) tenantid_to_update = event['tenantid'] filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_enable_user( Username=user['userName'], UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to enable users") return utils.create_success_response("Users enables") def get_user_info(user_pool_id, user_name): response = client.admin_get_user( UserPoolId=user_pool_id, Username=user_name ) logger.info(response) user_info = UserInfo() user_info.user_name = response["Username"] for attr in response["UserAttributes"]: if(attr["Name"] == "custom:tenantId"): user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] logger.info(user_info) return user_info class UserManagement: def create_user_group(self, user_pool_id, group_name, group_description): response = client.create_group( GroupName=group_name, UserPoolId=user_pool_id, Description= group_description, Precedence=0 ) return response def create_tenant_admin(self, user_pool_id, tenant_admin_user_name, user_details): response = client.admin_create_user( Username=tenant_admin_user_name, UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['tenantEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': 'TenantAdmin' }, { 'Name': 'custom:tenantId', 'Value': user_details['tenantId'] } ] ) return response def add_user_to_group(self, user_pool_id, user_name, group_name): response = client.admin_add_user_to_group( UserPoolId=user_pool_id, Username=user_name, GroupName=group_name ) return response def create_user_tenant_mapping(self, user_name, tenant_id): response = table_tenant_user_map.put_item( Item={ 'tenantId': tenant_id, 'userName': user_name } ) return response class UserInfo: def __init__(self, user_name=None, tenant_id=None, user_role=None, email=None, status=None, enabled=None, created=None, modified=None): self.user_name = user_name self.tenant_id = tenant_id self.user_role = user_role self.email = email self.status = status self.enabled = enabled self.created = created self.modified = modified ================================================ FILE: Solution/Lab2/server/layers/logger.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from aws_lambda_powertools import Logger logger = Logger() """Log info messages """ def info(log_message): logger.info (log_message) """Log error messages """ def error(log_message): logger.error (log_message) ================================================ FILE: Solution/Lab2/server/layers/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] simplejson jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab2/server/layers/utils.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import jsonpickle import simplejson import boto3 from aws_requests_auth.aws_auth import AWSRequestsAuth from enum import Enum class StatusCodes(Enum): SUCCESS = 200 UN_AUTHORIZED = 401 NOT_FOUND = 404 def create_success_response(message): return { "statusCode": StatusCodes.SUCCESS.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def generate_response(inputObject): return { "statusCode": 200, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": encode_to_json_object(inputObject), } def encode_to_json_object(inputObject): jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True) jsonpickle.set_preferred_backend('simplejson') return jsonpickle.encode(inputObject, unpicklable=False, use_decimal=True) def get_auth(host, region): session = boto3.Session() credentials = session.get_credentials() auth = AWSRequestsAuth(aws_access_key=credentials.access_key, aws_secret_access_key=credentials.secret_key, aws_token=credentials.token, aws_host=host, aws_region=region, aws_service='execute-api') return auth def get_headers(event): return event['headers'] ================================================ FILE: Solution/Lab2/server/nested_templates/apigateway.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway, apis, api keys and usage plan as part of bootstrap Parameters: StageName: Type: String RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String Resources: ApiGatewayCloudWatchLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub apigateway-cloudwatch-publish-role-${AWS::Region} Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole ApiGatewayAttachCloudwatchLogArn: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchLogRole.Arn AdminApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/api-gateway/access-logs-serverless-saas-admin-api RetentionInDays: 30 AdminApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: "/*" HttpMethod: "*" Auth: ResourcePolicy: CustomStatements: - Effect: Allow Principal: "*" Action: "execute-api:Invoke" Resource: ["execute-api:/*/*/*"] - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/tenant" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/user/tenant-admin" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref RegisterTenantLambdaExecutionRoleArn - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/disable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/enable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn AccessLogSetting: DestinationArn: !GetAtt AdminApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ["", ["serverless-saas-admin-api-", !Ref "AWS::Region"]] basePath: !Join ["", ["/", !Ref StageName]] schemes: - https paths: /registration: post: summary: Register a new tenant description: Register a new tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref RegisterTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant/activation/{tenantid}: put: security: - api_key: [] - Authorizer: [] summary: Activate an existing tenant description: Activate an existing tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ActivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenants: get: summary: Returns all tenants description: Returns all tenants produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantsFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant: post: summary: Creates a tenant description: Creates a tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/{tenantid}: get: summary: Returns a tenant description: Return a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Disables a tenant description: Disables a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeactivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy put: summary: Updates a tenant description: Updates a tenant produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user/{username}: get: summary: Returns a user description: Return a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUserFunctionArn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Diables a user description: Disable a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUserFunctionArn - /invocations httpMethod: POST type: aws_proxy /user/tenant-admin: post: summary: Creates a tenant admin user description: Creates a tenant admin user produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantAdminUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user: post: summary: Create a user description: Create a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users: get: summary: Get all users by tenantId description: Get all users by tenantId produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUsersFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/disable/{tenantid}: put: summary: disable users by tenant id description: disable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' requestTemplates: application/json: "{\"tenantid\": \"$input.params('tenantid')\" }" options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/enable/{tenantid}: put: summary: enable users by tenant id description: enable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref EnableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' requestTemplates: application/json: "{ \"tenantid\": \"$input.params('tenantid')\" }" options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: sigv4Reference: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "awsSigv4" Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref AuthorizerFunctionArn - /invocations authorizerResultTtlInSeconds: 60 type: "token" StageName: prod Outputs: AdminApiGatewayApi: Value: !Ref AdminApiGatewayApi ================================================ FILE: Solution/Lab2/server/nested_templates/apigateway_lambdapermissions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway and apis as part of bootstrap Parameters: RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String AdminApiGatewayApi: Type: String Resources: #provide api gateway permissions to call lambda functions RegisterTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref RegisterTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateTenantAdminUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantAdminUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] EnableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref EnableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUsersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUsersFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref AuthorizerFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*" ]] CreateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantsFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeactivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeactivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ActivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ActivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ================================================ FILE: Solution/Lab2/server/nested_templates/cognito.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup cognito as part of bootstrap Parameters: AdminEmailParameter: Type: String Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Description: "Enter the role name for system admin" AdminUserPoolCallbackURLParameter: Type: String Description: "Enter Admin Management userpool call back url" Resources: CognitoUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: PooledTenant-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Thanks for signing up. " - "You username is {username} and temporary password is {####}" EmailSubject: !Join - "" - - "Your temporary password for tenant UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSClient GenerateSecret: false UserPoolId: !Ref CognitoUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - https://example.com LogoutURLs: - https://example.com AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [pooledtenant-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoUserPool CognitoOperationUsersUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: OperationUsers-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into admin UI application at " - "https://" - !Ref AdminUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for admin UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoOperationUsersUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSOperationUsersPoolClient GenerateSecret: false UserPoolId: !Ref CognitoOperationUsersUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoOperationUsersUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [operationsusers-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUserGroup: Type: AWS::Cognito::UserPoolGroup Properties: GroupName: SystemAdmins Description: Admin user group Precedence: 0 UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUser: Type: AWS::Cognito::UserPoolUser Properties: Username: admin DesiredDeliveryMediums: - EMAIL ForceAliasCreation: true UserAttributes: - Name: email Value: !Ref AdminEmailParameter - Name: custom:tenantId Value: system_admins - Name: custom:userRole Value: !Ref SystemAdminRoleNameParameter UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAddUserToGroup: Type: AWS::Cognito::UserPoolUserToGroupAttachment Properties: GroupName: !Ref CognitoAdminUserGroup Username: !Ref CognitoAdminUser UserPoolId: !Ref CognitoOperationUsersUserPool Outputs: CognitoUserPoolId: Value: !Ref CognitoUserPool CognitoUserPoolClientId: Value: !Ref CognitoUserPoolClient CognitoOperationUsersUserPoolId: Value: !Ref CognitoOperationUsersUserPool CognitoOperationUsersUserPoolClientId: Value: !Ref CognitoOperationUsersUserPoolClient CognitoOperationUsersUserPoolProviderURL: Value: !GetAtt CognitoOperationUsersUserPool.ProviderURL CognitoAdminUserGroupName: Value: !Ref CognitoAdminUserGroup ================================================ FILE: Solution/Lab2/server/nested_templates/lambdafunctions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy lambda functions as part of bootstrap Parameters: CognitoOperationUsersUserPoolId: Type: String CognitoOperationUsersUserPoolClientId: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String TenantDetailsTableArn: Type: String TenantUserMappingTableArn: Type: String Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies Description: Utilities for project ContentUri: ../layers/ CompatibleRuntimes: - python3.9 LicenseInfo: "MIT" RetentionPolicy: Retain Metadata: BuildMethod: python3.9 AuthorizerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: authorizer-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: authorizer-execution-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:List* Resource: - !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn SharedServicesAuthorizerFunction: Type: AWS::Serverless::Function DependsOn: AuthorizerExecutionRole Properties: CodeUri: ../Resources/ Handler: shared_service_authorizer.lambda_handler Runtime: python3.9 Role: !GetAtt AuthorizerExecutionRole.Arn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !Ref CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !Ref CognitoOperationUsersUserPoolClientId #Create user pool for the tenant TenantUserPoolLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-userpool-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-userpool-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn - Effect: Allow Action: - dynamodb:GetItem - dynamodb:Query Resource: - !Ref TenantUserMappingTableArn CreateUserLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub create-user-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-user-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:PutItem Resource: - !Ref TenantUserMappingTableArn - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn CreateTenantAdminUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_tenant_admin_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId TENANT_APP_CLIENT_ID: !Ref CognitoUserPoolClientId POWERTOOLS_SERVICE_NAME: "UserManagement.CreateTenantAdmin" #User management CreateUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.CreateUser" UpdateUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.update_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.UpdateUser" DisableUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUser" DisableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUsersByTenant" EnableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.enable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.EnableUsersByTenant" GetUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.GetUser" GetUsersFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_users Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.GetUsers" #Tenant Management TenantManagementLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-management-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-tenant-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:Scan - dynamodb:Query Resource: - !Ref TenantDetailsTableArn - !Join ["", [!Ref TenantDetailsTableArn, '/index/*']] CreateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.create_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.CreateTenant" ActivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.activate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.ActivateTenant" ENABLE_USERS_BY_TENANT: "/users/enable" PROVISION_TENANT: "/provisioning/" GetTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.GetTenant" DeactivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.deactivate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.DeactivateTenant" DEPROVISION_TENANT: "/provisioning/" DISABLE_USERS_BY_TENANT: "/users/disable" UpdateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.update_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.UpdateTenant" GetTenantsFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenants Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers #Tenant Registration RegisterTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-registration-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess RegisterTenantFunction: Type: AWS::Serverless::Function DependsOn: RegisterTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-registration.register_tenant Runtime: python3.9 Role: !GetAtt RegisterTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: # Need to find a better way than hard coding resource paths CREATE_TENANT_ADMIN_USER_RESOURCE_PATH: "/user/tenant-admin" CREATE_TENANT_RESOURCE_PATH: "/tenant" PROVISION_TENANT_RESOURCE_PATH: "/provisioning" POWERTOOLS_SERVICE_NAME: "TenantRegistration.RegisterTenant" Outputs: RegisterTenantLambdaExecutionRoleArn: Value: !GetAtt RegisterTenantLambdaExecutionRole.Arn TenantManagementLambdaExecutionRoleArn: Value: !GetAtt TenantManagementLambdaExecutionRole.Arn RegisterTenantFunctionArn: Value: !GetAtt RegisterTenantFunction.Arn ActivateTenantFunctionArn: Value: !GetAtt ActivateTenantFunction.Arn GetTenantsFunctionArn: Value: !GetAtt GetTenantsFunction.Arn CreateTenantFunctionArn: Value: !GetAtt CreateTenantFunction.Arn GetTenantFunctionArn: Value: !GetAtt GetTenantFunction.Arn DeactivateTenantFunctionArn: Value: !GetAtt DeactivateTenantFunction.Arn UpdateTenantFunctionArn: Value: !GetAtt UpdateTenantFunction.Arn GetUsersFunctionArn: Value: !GetAtt GetUsersFunction.Arn GetUserFunctionArn: Value: !GetAtt GetUserFunction.Arn UpdateUserFunctionArn: Value: !GetAtt UpdateUserFunction.Arn DisableUserFunctionArn: Value: !GetAtt DisableUserFunction.Arn CreateTenantAdminUserFunctionArn: Value: !GetAtt CreateTenantAdminUserFunction.Arn CreateUserFunctionArn: Value: !GetAtt CreateUserFunction.Arn DisableUsersByTenantFunctionArn: Value: !GetAtt DisableUsersByTenantFunction.Arn EnableUsersByTenantFunctionArn: Value: !GetAtt EnableUsersByTenantFunction.Arn SharedServicesAuthorizerFunctionArn: Value: !GetAtt SharedServicesAuthorizerFunction.Arn ================================================ FILE: Solution/Lab2/server/nested_templates/tables.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to create dynamodb tables as part of bootstrap Resources: TenantDetailsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: tenantName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: ServerlessSaas-TenantConfig KeySchema: - AttributeName: tenantName KeyType: HASH Projection: NonKeyAttributes: - userPoolId - appClientId - apiGatewayUrl ProjectionType: INCLUDE TableName: ServerlessSaaS-TenantDetails TenantUserMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: userName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH - AttributeName: userName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantUserMapping GlobalSecondaryIndexes: - IndexName: UserName KeySchema: - AttributeName: userName KeyType: HASH - AttributeName: tenantId KeyType: RANGE Projection: ProjectionType: ALL Outputs: TenantDetailsTableArn: Value: !GetAtt TenantDetailsTable.Arn TenantDetailsTableName: Value: !Ref TenantDetailsTable TenantUserMappingTableArn: Value: !GetAtt TenantUserMappingTable.Arn TenantUserMappingTableName: Value: !Ref TenantUserMappingTable ================================================ FILE: Solution/Lab2/server/nested_templates/userinterface.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: IsCloudFrontAndS3PreProvisioned: Type: String Default: false Description: "Tells if cloudfront and s3 buckets are pre-provisioned or not. They get pre-provisioned when the workshop is running as a part of AWS Event through AWS event engine tool." Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref IsCloudFrontAndS3PreProvisioned, true] ] Resources: CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: IsNotRunningInEventEngine Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distributions" AdminAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AdminAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AdminAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AdminAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId AdminAppSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: #Aliases: # - !Sub 'admin.${CustomDomainName}' CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS CachedMethods: - GET - HEAD - OPTIONS Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: adminapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt AdminAppBucket.RegionalDomainName Id: adminapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' LandingAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True LandingAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref LandingAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${LandingAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId LandingApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: landingapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'LandingAppBucket.RegionalDomainName' Id: landingapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: AdminBucket: Description: The name of the bucket for uploading the Admin Management site to Value: !Ref AdminAppBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt AdminAppSite.DomainName Condition: IsNotRunningInEventEngine LandingAppBucket: Description: The name of the bucket for uploading the Landing site to Value: !Ref LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt LandingApplicationSite.DomainName Condition: IsNotRunningInEventEngine ================================================ FILE: Solution/Lab2/server/samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas" s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx" s3_prefix = "serverless-saas" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" ================================================ FILE: Solution/Lab2/server/template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Lab2 - Bootstrap common resources Parameters: AdminEmailParameter: Type: String Default: "test@test.com" Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Default: "SystemAdmin" Description: "Enter the role name for system admin" StageName: Type: String Default: "prod" Description: "Stage Name for the api" EventEngineParameter: Type: String Default: false Description: "Tells if this workshop is running as a part of AWS Event through AWS EventEngine or not" AdminUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Admin Management userpool call back url" Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref EventEngineParameter, true] ] Resources: DynamoDBTables: Type: AWS::Serverless::Application Properties: Location: nested_templates/tables.yaml #Create cloudfront and s3 for UI Cde UserInterface: Type: AWS::Serverless::Application Properties: Location: nested_templates/userinterface.yaml Parameters: IsCloudFrontAndS3PreProvisioned: !Ref EventEngineParameter Cognito: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/cognito.yaml Parameters: AdminEmailParameter: !Ref AdminEmailParameter SystemAdminRoleNameParameter: !Ref SystemAdminRoleNameParameter AdminUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.AdminAppSite, !Ref AdminUserPoolCallbackURLParameter] LambdaFunctions: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/lambdafunctions.yaml Parameters: CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId CognitoOperationUsersUserPoolId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId CognitoOperationUsersUserPoolClientId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId TenantDetailsTableArn: !GetAtt DynamoDBTables.Outputs.TenantDetailsTableArn TenantUserMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantUserMappingTableArn APIs: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway.yaml Parameters: StageName: !Ref StageName RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn APIGatewayLambdaPermissions: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway_lambdapermissions.yaml Parameters: RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn AdminApiGatewayApi: !GetAtt APIs.Outputs.AdminApiGatewayApi Outputs: AdminApi: Description: "API Gateway endpoint URL for Admin API" Value: !Join ["", ["https://", !GetAtt APIs.Outputs.AdminApiGatewayApi, ".execute-api.", !Ref "AWS::Region", ".amazonaws.com/", !Ref StageName]] AdminSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant administration application Value: !GetAtt UserInterface.Outputs.AdminBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt UserInterface.Outputs.AdminAppSite Condition: IsNotRunningInEventEngine LandingApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the landing application Value: !GetAtt UserInterface.Outputs.LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt UserInterface.Outputs.LandingApplicationSite Condition: IsNotRunningInEventEngine CognitoOperationUsersUserPoolProviderURL: Description: The Admin Management userpool provider url Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolProviderURL CognitoOperationUsersUserPoolClientId: Description: The Admin Management userpool client id Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId CognitoOperationUsersUserPoolId: Description: The user pool id of Admin Management userpool Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId CognitoAdminUserGroupName: Description: The Admin Management userpool admin user group name Value: !GetAtt Cognito.Outputs.CognitoAdminUserGroupName ================================================ FILE: Solution/Lab3/client/Admin/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab3/client/Admin/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab3/client/Admin/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab3/client/Admin/README.md ================================================ # Admin This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab3/client/Admin/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "admin": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "admin:build:production" }, "development": { "browserTarget": "admin:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "admin:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab3/client/Admin/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab3/client/Admin/package.json ================================================ { "name": "admin", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/flex-layout": "~14.0.0-beta.40", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.1.3", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab3/client/Admin/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Tenants', url: '/tenants', icon: 'groups', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Solution/Lab3/client/Admin/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: '', component: NavComponent, data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'tenants', loadChildren: () => import('./views/tenants/tenants.module').then((m) => m.TenantsModule), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab3/client/Admin/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab3/client/Admin/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Solution/Lab3/client/Admin/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { HttpInterceptorProviders } from './interceptors'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; @NgModule({ declarations: [AppComponent, NavComponent, AuthComponent], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, HttpClientModule, HttpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab3/client/Admin/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Solution/Lab3/client/Admin/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const HttpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Solution/Lab3/client/Admin/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Solution/Lab3/client/Admin/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Solution/Lab3/client/Admin/src/app/nav/nav.component.css ================================================ .sidenav-container { height: 100%; } .sidenav-content-container { background-color: lightgray; } .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .mat-toolbar.mat-primary { position: sticky; top: 0; z-index: 1; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .spinner-container { height: 80%; display: flex; justify-content: center; align-items: center; box-sizing: border-box; } ================================================ FILE: Solution/Lab3/client/Admin/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ username$ | async }}
================================================ FILE: Solution/Lab3/client/Admin/src/app/nav/nav.component.spec.ts ================================================ import { LayoutModule } from '@angular/cdk/layout'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { NavComponent } from './nav.component'; describe('NavComponent', () => { let component: NavComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [NavComponent], imports: [ NoopAnimationsModule, LayoutModule, MatButtonModule, MatIconModule, MatListModule, MatSidenavModule, MatToolbarModule, ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(NavComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should compile', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: Solution/Lab3/client/Admin/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.css'], }) export class NavComponent implements OnInit { tenantName = ''; loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router ) { this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => err); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map((sesh) => sesh && sesh.isValid()) ); const token$ = session$.pipe(map((sesh) => sesh && sesh.getIdToken())); this.username$ = token$.pipe( map((t) => t && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Solution/Lab3/client/Admin/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/dashboard/dashboard.component.html ================================================

Traffic

December 2020
================================================ FILE: Solution/Lab3/client/Admin/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } .chart-wrapper { padding-right: 20px; position: relative; margin: auto; height: 80vh; width: 80vw; } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/dashboard/dashboard.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ChartConfiguration, ChartType } from 'chart.js'; import { TenantsService } from '../tenants/tenants.service'; interface DataSet { label: string; data: number[]; } interface ChartData { tenantId: string; dataSet: DataSet[]; totalOrders: number; } @Component({ templateUrl: 'dashboard.component.html', styleUrls: ['./dashboard.component.scss'], selector: 'app-dashboard', }) export class DashboardComponent implements OnInit { constructor(private tenantSvc: TenantsService) {} data: ChartData[] = []; // lineChart public lineChartElements = 27; public lineChartData1: Array = []; public lineChartData2: Array = []; public lineChartData3: Array = []; public lineChartData: Array = [ { data: this.lineChartData1, label: 'Current', backgroundColor: 'rgba(148,159,177,0.2)', borderColor: 'rgba(148,159,177,1)', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, { data: this.lineChartData2, label: 'Previous', backgroundColor: 'rgba(77,83,96,0.2)', borderColor: 'rgba(77,83,96,1)', pointBackgroundColor: 'rgba(77,83,96,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(77,83,96,1)', fill: 'origin', }, { data: this.lineChartData3, label: 'BEP', backgroundColor: 'rgba(255,0,0,0.3)', borderColor: 'red', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, ]; public lineChartLegend = false; /* tslint:disable:max-line-length */ public lineChartLabels: Array = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ]; /* tslint:enable:max-line-length */ public lineChartType: ChartType = 'line'; public lineChartOptions: ChartConfiguration['options'] = { maintainAspectRatio: false, elements: { line: { tension: 0.5, }, }, scales: { // We use this empty structure as a placeholder for dynamic theming. x: {}, 'y-axis-0': { position: 'left', }, 'y-axis-1': { position: 'right', grid: { color: 'rgba(80,80,80,0.3)', }, ticks: { color: '#808080', }, }, }, }; public random(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } ngOnInit(): void { for (let i = 0; i <= this.lineChartElements; i++) { this.lineChartData1.push(this.random(50, 200)); this.lineChartData2.push(this.random(80, 100)); this.lineChartData3.push(65); } } } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/tenants/create/create.component.html ================================================
Provision tenant Name Email Phone Address Plan
================================================ FILE: Solution/Lab3/client/Admin/src/app/views/tenants/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/tenants/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { TenantsService } from '../tenants.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { submitting = false; tenantForm: FormGroup = this.fb.group({ tenantName: [null, [Validators.required]], tenantEmail: [null, [Validators.email, Validators.required]], tenantTier: [null, [Validators.required]], tenantPhone: [null], tenantAddress: [null], }); constructor( private fb: FormBuilder, private tenantSvc: TenantsService, private router: Router, private _snackBar: MatSnackBar ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; const user = { ...this.tenantForm.value, }; this.tenantSvc.post(this.tenantForm.value).subscribe({ next: () => { this.submitting = false; this.openErrorMessageSnackBar('Successfully created new tenant!'); this.router.navigate(['tenants']); }, error: (err) => { this.submitting = false; this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); }, }); } } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/tenants/list/list.component.html ================================================
Tenant List
Id {{ element.tenantId }} Tenant Name {{ element.tenantName }} E-Mail {{ element.tenantEmail }} Plan {{ element.tenantTier }} Status {{ element.isActive }}
================================================ FILE: Solution/Lab3/client/Admin/src/app/views/tenants/list/list.component.scss ================================================ .tenant-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/tenants/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Tenant } from '../models/tenant'; import { TenantsService } from '../tenants.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { tenants$ = new Observable(); tenantData: Tenant[] = []; isLoading: boolean = true; displayedColumns = [ 'tenantId', 'tenantName', 'tenantEmail', 'tenantTier', 'isActive', ]; constructor(private tenantSvc: TenantsService) {} ngOnInit(): void { this.tenantSvc.fetch().subscribe((data) => { this.tenantData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/tenants/models/tenant.ts ================================================ export interface Tenant { tenantId: string; tenantName: string; tenantEmail: string; tenantTier: string; isActive: boolean; } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/tenants/tenants-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', component: ListComponent, data: { title: 'Tenant List', }, }, { path: 'create', component: CreateComponent, data: { title: 'Provision new tenant', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class TenantsRoutingModule {} ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/tenants/tenants.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ListComponent } from './list/list.component'; import { TenantsRoutingModule } from './tenants-routing.module'; import { CreateComponent } from './create/create.component'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, TenantsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ReactiveFormsModule, MatSnackBarModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class TenantsModule {} ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/tenants/tenants.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Tenant } from './models/tenant'; @Injectable({ providedIn: 'root', }) export class TenantsService { constructor(private http: HttpClient) {} baseUrl = `${environment.apiUrl}`; tenantsApiUrl = `${this.baseUrl}/tenants`; registrationApiUrl = `${this.baseUrl}/registration`; // TODO strongly-type these anys as tenants once we dial in what the tenant call should return fetch(): Observable { return this.http.get(this.tenantsApiUrl); } post(tenant: Tenant): Observable { return this.http.post(this.registrationApiUrl, tenant); } } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
Tenant Id folder_special
================================================ FILE: Solution/Lab3/client/Admin/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], tenantId: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Solution/Lab3/client/Admin/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Solution/Lab3/client/Admin/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.apiUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } } ================================================ FILE: Solution/Lab3/client/Admin/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab3/client/Admin/src/aws-exports.ts ================================================ const awsmobile = { aws_project_region: 'us-west-2', aws_cognito_region: 'us-west-2', aws_user_pools_id: 'us-west-2_nWhwjijMc', aws_user_pools_web_client_id: '61ipvhmvdrfso0cftpil9irk8k', }; export default awsmobile; ================================================ FILE: Solution/Lab3/client/Admin/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab3/client/Admin/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab3/client/Admin/src/environments/environment.ts ================================================ export const environment = { production: false, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab3/client/Admin/src/index.html ================================================ Dashboard ================================================ FILE: Solution/Lab3/client/Admin/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { Amplify } from 'aws-amplify'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import aws_exports from './aws-exports'; Amplify.configure(aws_exports); if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab3/client/Admin/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab3/client/Admin/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab3/client/Admin/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab3/client/Admin/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; ================================================ FILE: Solution/Lab3/client/Admin/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab3/client/Admin/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab3/client/Admin/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab3/client/Admin/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab3/client/Application/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab3/client/Application/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab3/client/Application/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end #cypress cypress/videos/* cypress.env.json # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab3/client/Application/README.md ================================================ # Application This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab3/client/Application/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "application": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "2mb", "maximumError": "2mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "6kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "application:build:production" }, "development": { "browserTarget": "application:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "application:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab3/client/Application/cypress/README.md ================================================ # Application End-to-End Testing ## Instructions To run End-to-End (e2e) tests against the Sample Application, take the following steps: 1. Make a copy of the example env file (`cypress.env.json.example`): ```bash cp cypress.env.json.example cypress.env.json ``` 2. Edit the new file and replace the sample values with real values. The following should help when deciding what to use in place of the sample values provided: - `host`: The URL where the Sample Application is running. If testing locally, this is usually `"http://localhost:4200"`. - `tenantName`: The name of the tenant used to identify the appropriate Cognito User Pool to use for Authentication. - `tenantUsername`: The username to use when logging in. - `tenantUserPassword`: The password to use when logging in. - `email`: The email address to use for testing. (This should be a valid email address.) 3. Navigate to the root of the Application project (`aws-saas-factory-ref-solution-serverless-saas/clients/Application/`) and run the following: ```bash npx cypress run ``` This will run the tests located in the `cypress/e2e` folder. The documentation [here](https://docs.cypress.io/guides/guides/command-line#cypress-run) has more information on what can be passed in as arguments when running the Cypress tests. For example, running the following will show the Cypress UI and what is happening as each of the tests are run: ```bash npx cypress run --headed ``` ================================================ FILE: Solution/Lab3/client/Application/cypress/e2e/1-getting-started/basic-access.cy.js ================================================ /// describe('check that the app redirects to /unauthorized when tenant is not set', () => { it('redirects to unauthorized when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); }) it('redirects to unauthorized when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); }) it('redirects to unauthorized when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); }) }) describe('check that the app redirects to a page with a sign-in form when tenant is set and user is not logged in', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').should('exist') cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.location('href').should('contain', '/dashboard') }) it('redirects when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) }) ================================================ FILE: Solution/Lab3/client/Application/cypress/e2e/1-getting-started/product-testing.cy.js ================================================ /// describe('check that product, order and user functionality works as expected', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.get('form input[name="username"]').type(Cypress.env('tenantUsername')) cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() cy.wait(1500) cy.get('body').then(body => { if (body.find('form input[name="confirm_password"]').length > 0) { cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form input[name="confirm_password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() } }) }) it('can create new users and display them', () => { const email_username = Cypress.env('email').split('@')[0]; const email_domain = Cypress.env('email').split('@')[1]; const random_suffix = '+test'+Date.now().toString().slice(-3); const myUser = { name: "myUser-"+Date.now(), email: email_username + random_suffix + '@' + email_domain, role: 'userRole'+Date.now().toString().slice(-5) } cy.get("a").contains("Users").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.get("button[color='primary'").contains("Add User").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="userName"]').type(myUser.name) cy.get('input[formcontrolname="userEmail"]').type(myUser.email) cy.get('input[formcontrolname="userRole"]').type(myUser.role) }) cy.intercept({ method: 'POST', url: '**/user', }).as('postUser') cy.intercept({ method: 'GET', url: '**/users', }).as('getUsers') cy.get("button").contains("Create").click() cy.wait('@postUser') cy.go('back') cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.wait('@getUsers') cy.get('table td').contains(myUser.email) }) it('can create a new order with a new product and see them listed', () => { const myProduct = { name: "myProduct-"+Date.now(), price: Date.now().toString().slice(-3), sku: Date.now().toString().slice(-5), category: "category3", } const myOrder = { name: "myOrder-"+Date.now(), } // NOW TESTING PRODUCT CREATION // cy.get("a").contains("Products").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.get("button[color='primary'").contains("Create Product").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="name"]').type(myProduct.name) cy.get('input[formcontrolname="price"]').type(myProduct.price) cy.get('input[formcontrolname="sku"]').type(myProduct.sku) cy.get('mat-select[formcontrolname="category"]').click() }) cy.get('.mat-option-text').contains(myProduct.category).click() cy.get("button").contains("Submit").should('not.be.disabled') cy.intercept({ method: 'POST', url: '**/product', }).as('postProduct') cy.intercept({ method: 'GET', url: '**/products', }).as('getProducts') cy.get("button").contains("Submit").click() cy.wait('@postProduct') cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.wait('@getProducts') cy.get('table td').contains(myProduct.name) cy.get('table td').contains(myProduct.price) cy.get("a").contains("Orders").click() // DONE TESTING PRODUCT CREATION // // NOW TESTING ORDER CREATION // cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.get("button[color='primary']").contains("Create Order").click() cy.location().should((loc) => { expect(loc.href).to.contain('/orders/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="orderName"]').type(myOrder.name) }) cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.intercept({ method: 'POST', url: '**/order', }).as('postOrder') cy.intercept({ method: 'GET', url: '**/orders', }).as('getOrders') cy.get("button").contains("Submit").click() cy.wait('@postOrder') cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.wait('@getOrders') cy.get('table td').contains(myOrder.name) cy.get('table td').contains(new Intl.NumberFormat().format(myProduct.price * 2)) // DONE TESTING ORDER CREATION // }) }) ================================================ FILE: Solution/Lab3/client/Application/cypress.config.ts ================================================ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { supportFile: false, setupNodeEvents(on, config) { // implement node event listeners here }, }, }); ================================================ FILE: Solution/Lab3/client/Application/cypress.env.json.example ================================================ { "host": "http://localhost:4200", "tenantName": "UPDATE_ME!", "tenantUsername": "UPDATE_ME!", "tenantUserPassword": "UPDATE_ME!", "email": "test@example.com" } ================================================ FILE: Solution/Lab3/client/Application/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/application'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab3/client/Application/package.json ================================================ { "name": "application", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "e2e": "npx cypress run" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.2.0", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "cypress": "~10.3.1", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab3/client/Application/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Products', url: '/products', icon: 'sell', }, { name: 'Orders', url: '/orders', icon: 'shopping_cart', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Solution/Lab3/client/Application/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { CognitoGuard } from './cognito.guard'; export const routes: Routes = [ { path: '', redirectTo: 'unauthorized', pathMatch: 'full', }, { path: '', component: NavComponent, canActivate: [CognitoGuard], data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'orders', loadChildren: () => import('./views/orders/orders.module').then((m) => m.OrdersModule), }, { path: 'products', loadChildren: () => import('./views/products/products.module').then( (m) => m.ProductsModule ), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, { path: 'unauthorized', component: UnauthorizedComponent, }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab3/client/Application/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab3/client/Application/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Solution/Lab3/client/Application/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { httpInterceptorProviders } from './interceptors'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; @NgModule({ declarations: [ AppComponent, NavComponent, AuthComponent, UnauthorizedComponent, ], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, MatSnackBarModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatIconModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, httpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab3/client/Application/src/app/cognito.guard.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, } from '@angular/router'; import { Auth } from 'aws-amplify'; import { AuthConfigurationService } from './views/auth/auth-configuration.service'; @Injectable({ providedIn: 'root' }) export class CognitoGuard implements CanActivate { constructor( private router: Router, private authConfigService: AuthConfigurationService ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Promise { if (!this.authConfigService.configureAmplifyAuth()) { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return new Promise((res, rej) => { res(false); }); } return Auth.currentSession() .then((u) => { if (u.isValid()) { return true; } else { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return false; } }) .catch((e) => { if (state.url === '/dashboard') { // if we're going to the dashboard and we're not logged in, // don't stop the flow as the amplify-authenticator will // route requests going to the dashboard to the sign-in page. return true; } console.log('Error getting current session', e); this.router.navigate(['/unauthorized']); return false; }); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const httpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Solution/Lab3/client/Application/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Solution/Lab3/client/Application/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Solution/Lab3/client/Application/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ (companyName$ | async) || "" }} {{ (username$ | async) || user.username}}
================================================ FILE: Solution/Lab3/client/Application/src/app/nav/nav.component.scss ================================================ .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .sidebar-icon-container { height: 64px; background-color: whitesmoke; display: flex; align-items: center; justify-content: center; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .footer { position: fixed; bottom: 0; width: 100%; height: 40px; background-color: whitesmoke; color: #2d3337; // text-align: center; margin: 0px; } .footer-text { display: flex; } .content { position: absolute; width: 100%; height: 90%; max-height: 90%; overflow: auto; background-color: lightgray; } ================================================ FILE: Solution/Lab3/client/Application/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; import { AuthConfigurationService } from './../views/auth/auth-configuration.service'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.scss'], }) export class NavComponent implements OnInit { loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router, private authConfigService: AuthConfigurationService ) { // this.configSvc.loadConfigurations().subscribe((val) => console.log(val)); this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => { console.log('Failed to get current session. Err: ', err); return err; }); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map( (sesh) => sesh && typeof sesh.isValid === 'function' && sesh.isValid() ) ); const token$ = session$.pipe( map( (sesh) => sesh && typeof sesh.getIdToken === 'function' && sesh.getIdToken() ) ); this.username$ = token$.pipe( map((t) => t && t.payload && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload && t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } async logout() { await Auth.signOut({ global: true }) .then((e) => { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); }) .catch((err) => { console.error('Error logging out: ', err); }); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/auth/auth-configuration.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpParams, HttpParamsOptions, } from '@angular/common/http'; import { Injectable, OnInit } from '@angular/core'; import { map, switchMap, catchError } from 'rxjs/operators'; import { throwError } from 'rxjs'; import { environment } from '../../../environments/environment'; import { ConfigParams } from './models/config-params'; import { ActivatedRoute } from '@angular/router'; import Amplify from 'aws-amplify'; import { Auth } from 'aws-amplify'; import { Router } from '@angular/router'; import { from, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AuthConfigurationService { params$: Observable; params: ConfigParams; tenantName: string; constructor( private http: HttpClient, private route: ActivatedRoute, private router: Router ) {} public setTenantConfig(tenantName: string): Promise { const url = `${environment.regApiGatewayUrl}/tenant/init/` + tenantName; this.params$ = this.http.get(url); const setup$ = this.params$.pipe( map((val) => { // remove trailing slash (/) if present val.apiGatewayUrl = val.apiGatewayUrl.replace(/\/$/, ''); localStorage.setItem('userPoolId', val.userPoolId); localStorage.setItem('tenantName', tenantName); localStorage.setItem('appClientId', val.appClientId); localStorage.setItem('apiGatewayUrl', val.apiGatewayUrl); return 'success'; }), catchError((error) => { console.log('Error setting tenant config: ', error); return throwError(error); }) ); return setup$.toPromise(); } configureAmplifyAuth(): boolean { try { const userPoolId = localStorage.getItem('userPoolId'); const appClientId = localStorage.getItem('appClientId'); if (!userPoolId || !appClientId) { return false; } const region = userPoolId?.split('_')[0]; const awsmobile = { aws_project_region: region, aws_cognito_region: region, aws_user_pools_id: userPoolId, aws_user_pools_web_client_id: appClientId, }; Amplify.configure(awsmobile); return true; } catch (err) { console.error('Unable to initialize amplify auth.', err); return false; } } cleanLocalStorage() { localStorage.removeItem('tenantName'); localStorage.removeItem('userPoolId'); localStorage.removeItem('appClientId'); localStorage.removeItem('apiGatewayUrl'); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Solution/Lab3/client/Application/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/auth/models/config-params.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ export interface ConfigParams { appClientId: string; userPoolId: string; apiGatewayUrl: string; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Solution/Lab3/client/Application/src/app/views/dashboard/dashboard.component.html ================================================

Dashboard

{{ card.title }}
================================================ FILE: Solution/Lab3/client/Application/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/dashboard/dashboard.component.ts ================================================ import { Component, ViewChild } from '@angular/core'; import { map } from 'rxjs/operators'; import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js'; import { BaseChartDirective } from 'ng2-charts'; import DataLabelsPlugin from 'chartjs-plugin-datalabels'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'], }) export class DashboardComponent { @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; public barChartOptions: ChartConfiguration['options'] = { responsive: true, maintainAspectRatio: false, // We use these empty structures as placeholders for dynamic theming. scales: { x: {}, y: { min: 10, }, }, plugins: { legend: { display: true, }, datalabels: { anchor: 'end', align: 'end', }, }, }; public barChartType: ChartType = 'bar'; public barChartPlugins = [DataLabelsPlugin]; public barChartData: ChartData<'bar'> = { labels: ['2006', '2007', '2008', '2009', '2010', '2011', '2012'], datasets: [ { data: [65, 59, 80, 81, 56, 55, 40], label: 'Series A' }, { data: [28, 48, 40, 19, 86, 27, 90], label: 'Series B' }, ], }; // events public chartClicked({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public chartHovered({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public randomize(): void { // Only Change 3 values this.barChartData.datasets[0].data = [ Math.round(Math.random() * 100), 59, 80, Math.round(Math.random() * 100), 56, Math.round(Math.random() * 100), 40, ]; this.chart?.update(); } /** Based on the screen size, switch from standard to one column per row */ cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe( map(({ matches }) => { if (matches) { return [ { title: 'Card 1', cols: 1, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 1 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; } return [ { title: 'Card 1', cols: 2, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 2 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; }) ); constructor(private breakpointObserver: BreakpointObserver) {} } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Solution/Lab3/client/Application/src/app/views/error/404.component.html ================================================

404

Oops! You're lost.

The page you are looking for was not found.

================================================ FILE: Solution/Lab3/client/Application/src/app/views/error/404.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '404.component.html', }) export class P404Component { constructor() {} } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/error/500.component.html ================================================

500

Houston, we have a problem!

The page you are looking for is temporarily unavailable.

================================================ FILE: Solution/Lab3/client/Application/src/app/views/error/500.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '500.component.html', }) export class P500Component { constructor() {} } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/error/unauthorized.component.html ================================================
Unauthorized Enter your tenant name and click submit below Tenant Name home
================================================ FILE: Solution/Lab3/client/Application/src/app/views/error/unauthorized.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .center-screen { display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; min-height: 100vh; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/error/unauthorized.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AuthConfigurationService } from './../auth/auth-configuration.service'; import { Observable } from 'rxjs'; import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-unauthorized', templateUrl: './unauthorized.component.html', styleUrls: ['./unauthorized.component.scss'], }) export class UnauthorizedComponent implements OnInit { tenantForm: FormGroup; params$: Observable; error = false; errorMessage: string; tenantNameRequired: boolean = true; constructor( private fb: FormBuilder, private authConfigService: AuthConfigurationService, private _snackBar: MatSnackBar, private router: Router ) { if ( 'userPoolId' in environment && 'appClientId' in environment && 'apiGatewayUrl' in environment ) { // If a tenant's cognito configuration is provided in the // "environment" object, then we take that instead of asking // the visitor to provide the name of their tenant in order // to do a look-up for that tenant's cognito configuration. localStorage.setItem('tenantName', 'PooledTenants'); localStorage.setItem('userPoolId', (environment as any).userPoolId); localStorage.setItem('appClientId', (environment as any).appClientId); localStorage.setItem('apiGatewayUrl', (environment as any).apiGatewayUrl); this.tenantNameRequired = false; } } ngOnInit(): void { this.tenantForm = this.fb.group({ tenantName: [null, [Validators.required]], }); } isFieldInvalid(field: string) { const formField = this.tenantForm.get(field); return ( formField && formField.invalid && (formField.dirty || formField.touched) ); } displayFieldCss(field: string) { return { 'is-invalid': this.isFieldInvalid(field), }; } hasRequiredError(field: string) { return !!this.tenantForm.get(field)?.hasError('required'); } openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } login() { if (!this.tenantNameRequired) { this.router.navigate(['/dashboard']); return true; } let tenantName = this.tenantForm.value.tenantName; if (!tenantName) { this.errorMessage = 'No tenant name provided.'; this.error = true; this.openErrorMessageSnackBar(this.errorMessage); return false; } this.authConfigService .setTenantConfig(tenantName) .then((val) => { this.router.navigate(['/dashboard']); }) .catch((errorResponse) => { this.error = true; this.errorMessage = errorResponse.error.message || 'An unexpected error occurred!'; this.openErrorMessageSnackBar(this.errorMessage); }); return false; } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/create/create.component.html ================================================
Create new order Enter order name Name is required
{{ op.product.name }}
{{ op.product.price | currency }}
{{ op.quantity || 0 }}
================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/create/create.component.scss ================================================ // .card { // margin: 20px; // display: flex; // flex-direction: column; // align-items: flex-start; // } .order-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { Product } from '../../products/models/product.interface'; import { ProductService } from '../../products/product.service'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; interface LineItem { product: Product; quantity?: number; } @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { displayedColumns = []; orderForm: FormGroup; orderProducts: LineItem[] = []; isLoadingProducts: boolean = true; error = ''; constructor( private fb: FormBuilder, private productSvc: ProductService, private orderSvc: OrdersService, private router: Router ) { this.orderForm = this.fb.group({ name: ['', Validators.required], }); } ngOnInit(): void { this.productSvc.fetch().subscribe((products) => { this.orderProducts = products.map((p) => ({ product: p })); this.isLoadingProducts = false; }); this.orderForm = this.fb.group({ orderName: ['', Validators.required], }); } get name() { return this.orderForm.get('name'); } get productQuantity() { return this.orderProducts .filter((p) => !!p.quantity).length > 0; } add(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity ? orderProduct.quantity + 1 : 1, }; } return p; }); } remove(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity && orderProduct.quantity > 1 ? orderProduct.quantity - 1 : undefined, }; } return p; }); } submit() { const val: Order = { ...this.orderForm?.value, orderProducts: this.orderProducts .filter((p) => !!p.quantity) .map((p) => ({ productId: p.product.productId, price: p.product.price, quantity: p.quantity, })), }; this.orderSvc.create(val).subscribe(() => this.router.navigate(['orders'])); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/detail/detail.component.html ================================================

Order Details mostly made up

NO: {{ orderId$ | async }} | Date: {{ today() | date }}
  • {{ tenantName() }}

Services / Products

Item / Details Unit Cost Sum Cost Discount Tax Total
{{ op.productId }}
{{ op.price | currency }}
Before Tax
{{ sum(op) | currency }}
{{ op.quantity }} Units
$0.00
None
{{ tax(op) | currency }}
Sales Tax 8.9%
{{ total(op) | currency }}
Sub Total Discount Total Tax Final
{{ subTotal(order) | currency }} -$0.00 {{ subTotal(order) | currency }} {{ calcTax(order) | currency }} {{ final(order) | currency }}
Comments / Notes
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Odit repudiandae numquam sit facere blanditiis, quasi distinctio ipsam? Libero odit ex expedita, facere sunt, possimus consectetur dolore, nobis iure amet vero.
================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/detail/detail.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .no-bottom-margin { margin-bottom: 0; } .nowrap { white-space: nowrap; } .invoice { font-family: Arial, Helvetica, sans-serif; width: 970px !important; margin: 50px auto; .invoice-header { padding: 25px 25px 15px; h1 { margin: 0; } .media { .media-body { font-size: 0.9em; margin: 0; } } } .invoice-body { border-radius: 10px; padding: 25px; background: #fff; } .invoice-footer { padding: 15px; font-size: 0.9em; text-align: center; color: #999; } } .logo { max-height: 70px; border-radius: 10px; } .dl-horizontal { margin: 0; dt { float: left; width: 80px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } dd { margin-left: 90px; } } .rowamount { padding-top: 15px !important; } .rowtotal { font-size: 1.3em; } .colfix { width: 12%; } .mono { font-family: monospace; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/detail/detail.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Order } from '../models/order.interface'; import { OrderProduct } from '../models/orderproduct.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-detail', templateUrl: './detail.component.html', styleUrls: ['./detail.component.scss'], }) export class DetailComponent implements OnInit { orderId$: Observable | undefined; order$: Observable | undefined; orderProducts$: Observable | undefined; taxRate = 0.0899; constructor(private route: ActivatedRoute, private orderSvc: OrdersService) {} ngOnInit(): void { this.orderId$ = this.route.params.pipe(map((o) => o['orderId'])); this.order$ = this.orderId$.pipe(switchMap((o) => this.orderSvc.get(o))); this.orderProducts$ = this.order$.pipe(map((o) => o.orderProducts)); } today() { return new Date(); } tenantName() { return ''; } sum(op: OrderProduct) { return op.price * op.quantity; } tax(op: OrderProduct) { return this.sum(op) * this.taxRate; } total(op: OrderProduct) { return this.sum(op) + this.tax(op); } subTotal(order: Order) { return order.orderProducts .map((op) => op.price * op.quantity) .reduce((acc, curr) => acc + curr); } calcTax(order: Order) { return this.subTotal(order) * this.taxRate; } final(order: Order) { return this.subTotal(order) + this.calcTax(order); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/list/list.component.html ================================================

Order List

Name {{ element.orderName }} Line Items {{ element.orderProducts?.length }} Total {{ sum(element) | currency }}
================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/list/list.component.scss ================================================ .order-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { displayedColumns: string[] = ['name', 'lineItems', 'total']; orderData: Order[] = []; isLoading: boolean = true; constructor(private orderSvc: OrdersService, private router: Router) {} ngOnInit(): void { this.orderSvc.fetch().subscribe((data) => { this.isLoading = false; this.orderData = data; }); } sum(order: Order): number { return order.orderProducts .map((p) => p.price * p.quantity) .reduce((acc, curr) => acc + curr); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/models/order.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { OrderProduct } from './orderproduct.interface'; export interface Order { key: string; shardId: string; orderId: string; orderName: string; orderProducts: OrderProduct[]; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/models/orderproduct.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface OrderProduct { productId: string; price: number; quantity: number; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/orders-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', data: { title: 'All Orders', }, component: ListComponent, }, { path: 'create', data: { title: 'Create Order', }, component: CreateComponent, }, { path: 'detail/:orderId', data: { title: 'View Order Detail', }, component: DetailComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class OrdersRoutingModule {} ================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/orders.module.ts ================================================ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; import { OrdersRoutingModule } from './orders-routing.module'; @NgModule({ declarations: [CreateComponent, ListComponent, DetailComponent], imports: [ CommonModule, OrdersRoutingModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class OrdersModule {} ================================================ FILE: Solution/Lab3/client/Application/src/app/views/orders/orders.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Order } from './models/order.interface'; @Injectable({ providedIn: 'root', }) export class OrdersService { orders: Order[] = []; baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; constructor(private http: HttpClient) {} fetch(): Observable { return this.http.get(`${this.baseUrl}/orders`); } get(orderId: string): Observable { const url = `${this.baseUrl}/order/${orderId}`; return this.http.get(url); } create(order: Order): Observable { return this.http.post(`${this.baseUrl}/order`, order); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/create/create.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required SKU Category {{category}}
================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ProductService } from '../product.service'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); } get name() { return this.productForm.get('name'); } get price() { return this.productForm.get('price'); } submit() { this.productSvc.post(this.productForm.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err.message); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/edit/edit.component.html ================================================
Edit Product Product ID Enter product name Name is required Enter product price Price is required SKU Category {{ category }}
================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/edit/edit.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/edit/edit.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.scss'], }) export class EditComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; product$: Observable | undefined; productId$: Observable | undefined; productName$: Observable | undefined; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router, private route: ActivatedRoute ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ shardId: [], productId: [], name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); this.productId$ = this.route.params.pipe(map((p) => p['productId'])); this.product$ = this.productId$.pipe( switchMap((p) => this.productSvc.get(p)) ); this.productName$ = this.product$.pipe(map((p) => p?.name)); this.product$.subscribe((val) => { this.productForm?.patchValue({ ...val, }); }); } get name() { return this.productForm?.get('name'); } get price() { return this.productForm?.get('price'); } submit() { this.productSvc.put(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => console.error(err), }); } delete() { this.productSvc.delete(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/list/list.component.html ================================================

Product List

Name {{ element.name }} Price {{ element.price | currency }} SKU {{ element.sku }}
================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/list/list.component.scss ================================================ .product-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { productData: Product[] = []; isLoading: boolean = true; displayedColumns: string[] = ['name', 'price', 'sku']; constructor(private productSvc: ProductService, private router: Router) {} ngOnInit(): void { this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onEdit(product: Product) { this.router.navigate(['products', 'edit', product.productId]); return false; } onRemove(product: Product) { this.productSvc.delete(product); this.isLoading = true; this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onCreate() { this.router.navigate(['products', 'create']); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/models/product.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface Product { key: string; shardId: string; productId: string; name: string; price: number; sku: string; category: string; pictureUrl?: string; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/product.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Product } from './models/product.interface'; @Injectable({ providedIn: 'root', }) export class ProductService { constructor(private http: HttpClient) {} baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; fetch(): Observable { return this.http.get(`${this.baseUrl}/products`); } get(productId: string): Observable { const url = `${this.baseUrl}/product/${productId}`; return this.http.get(url); } delete(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.delete(url); } put(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.put(url, product); } post(product: Product) { return this.http.post(`${this.baseUrl}/product`, product); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/products-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'Product List', }, component: ListComponent, }, { path: 'create', data: { title: 'Create new Product', }, component: CreateComponent, }, { path: 'edit/:productId', data: { title: 'Edit Product', }, component: EditComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class ProductsRoutingModule {} ================================================ FILE: Solution/Lab3/client/Application/src/app/views/products/products.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ProductsRoutingModule } from './products-routing.module'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [CreateComponent, EditComponent, ListComponent], imports: [ CommonModule, ReactiveFormsModule, ProductsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class ProductsModule {} ================================================ FILE: Solution/Lab3/client/Application/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
================================================ FILE: Solution/Lab3/client/Application/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Solution/Lab3/client/Application/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Solution/Lab3/client/Application/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Solution/Lab3/client/Application/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Solution/Lab3/client/Application/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { find, mergeMap, defaultIfEmpty } from 'rxjs/operators'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.regApiGatewayUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } update(email: string, user: User) {} } ================================================ FILE: Solution/Lab3/client/Application/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab3/client/Application/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab3/client/Application/src/environments/environment.prod.ts ================================================ export const environment = { production: false, regApiGatewayUrl: 'https://ulp15c9bv2.execute-api.us-west-2.amazonaws.com/prod/', }; ================================================ FILE: Solution/Lab3/client/Application/src/environments/environment.ts ================================================ export const environment = { production: false, regApiGatewayUrl: 'https://ulp15c9bv2.execute-api.us-west-2.amazonaws.com/prod/', }; ================================================ FILE: Solution/Lab3/client/Application/src/index.html ================================================ Application ================================================ FILE: Solution/Lab3/client/Application/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab3/client/Application/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab3/client/Application/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab3/client/Application/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab3/client/Application/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "~bootstrap/scss/functions"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/maps"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; .chart-container canvas { max-height: 250px; width: auto; } .chart-container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; } ================================================ FILE: Solution/Lab3/client/Application/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab3/client/Application/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab3/client/Application/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "strictPropertyInitialization": false, "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab3/client/Application/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab3/client/Landing/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab3/client/Landing/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab3/client/Landing/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab3/client/Landing/README.md ================================================ # Landing This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab3/client/Landing/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "Landing": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/custom-theme.scss", "src/styles.scss", "node_modules/font-awesome/css/font-awesome.min.css" ] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "10kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "Landing:build:production" }, "development": { "browserTarget": "Landing:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "Landing:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab3/client/Landing/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab3/client/Landing/package.json ================================================ { "name": "landing", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "bootstrap": "~5.1.3", "font-awesome": "~4.7.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab3/client/Landing/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; export const routes: Routes = [ { path: '', redirectTo: 'landing', pathMatch: 'full', }, { path: 'landing', component: LandingComponent, }, { path: 'register', component: RegisterComponent, }, { path: '**', redirectTo: 'landing', pathMatch: 'full', }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab3/client/Landing/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab3/client/Landing/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'landing'; } ================================================ FILE: Solution/Lab3/client/Landing/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; @NgModule({ declarations: [AppComponent, LandingComponent, RegisterComponent], imports: [ AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, MatButtonModule, MatCardModule, MatIconModule, MatProgressSpinnerModule, MatSidenavModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, FormsModule, HttpClientModule, ReactiveFormsModule, MatSnackBarModule, MatInputModule, MatFormFieldModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab3/client/Landing/src/app/views/landing/landing.component.html ================================================

Serverless SaaS Reference Architecture

It's so nice it blows your mind.

Sign up now!

Serverless SaaS Reference Architecture is so awesome.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere temporibus omnis illum, officia. Architecto voluptatibus commodi voluptatem perspiciatis eos possimus, eius at molestias quaerat magnam? Odio qui quos ipsam natus.

Serverless SaaS Reference Architecture so awesome. Makes you awesome - go sign up!

Serverless SaaS Reference Architecture so great. Makes you even greater - go sign up now. Super cheap deal!

Feel lonely? Go sign up and have a friend!

Take Serverless SaaS Reference Architecture with you everywhere you go.

Serverless SaaS Reference Architecture is all you need. Anywhere - ever. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita sapiente hic voluptatum quo sunt totam accusamus distinctio minus aliquid quis!

Love Serverless SaaS Reference Architecture. So nice! So good! Could not live without!

Satisfied Customer

Reasons to sign up this product:

  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!
  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!

Why you still reading?

Sign up now!
================================================ FILE: Solution/Lab3/client/Landing/src/app/views/landing/landing.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // $color-red: #cc615f; $color-grey: #4b4b4b; $color-grey--light: #d3d3d3; $btn-amzn: #ec7211; $color-primary: #232f3e; $bp-s: 65.75em; //700px; $bp-xs: 34.375em; //550px; @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,800,300); *{ box-sizing: border-box; } html{ width: 100%; height:100%; margin: 0; padding: 0; } body{ width: 100%; height:100%; font-family: 'Open Sans','Helvetica Neue',Helvetica, sans-serif; font-size: 100%; line-height: 1.45; color: #141414; } a{ text-decoration: none; &:hover{ text-decoration: none; } } img{ max-width: 100%; } .btn{ display: inline-block; margin: 1rem 0; color: white; font-weight: 500; font-size: 1.3rem; background: $btn-amzn; letter-spacing: .02em; border: none; border-radius: 5px; padding: .8rem 1rem .9rem; text-shadow: 0 1px rgba(black,.3); box-shadow: 0 0 2px rgba(black,.2); @media (max-width: $bp-s){ padding: .5rem .7rem .6rem; font-size: 1rem; } &:hover{ background: lighten($btn-amzn,5%); color: #fff; } &:focus, &:active, &:focus:active{ background: darken($btn-amzn,5%); border-color: darken($btn-amzn,5%); box-shadow: 0 2px 5px 0 rgba(black,.5) inset; } } .container{ margin: 0 auto; width: 90%; max-width: 900px; } header{ color: white; background: #232f3e; padding: 10rem 0; text-align: center; position: relative; z-index: 1; overflow: hidden; @media (max-width: $bp-s){ padding: 2rem 0; } h1{ font-size: 3rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 2rem; } } h2{ font-weight: 300; font-size: 1.5rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } } section{ background: #fff; color: $color-grey; padding: 3.5rem 0; @media (max-width: $bp-s){ padding: 2rem 0; } &.section--primary{ background: $color-primary; color: #fff; } &.section--primary--alt{ background: desaturate(lighten($color-primary,15%),10%); color: #fff; } &.section--primary--light{ background: rgba($color-primary,.1); } &.section--grey{ background: $color-grey; color: #fff; } &.section--grey--light{ background: $color-grey--light; color: #fff; } h3{ text-align: center; font-size: 2rem; font-weight: 300; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } li{ font-size: 1.2rem; font-weight: 300; } p{ font-size: 1.2rem; font-weight: 300; } } .col{ margin: 0 1.5%; display: inline-block; vertical-align: top; } .col-7{ @extend .col; width: 64%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-3{ @extend .col; width: 29%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-5{ @extend .col; width: 30%; @media (max-width: $bp-xs){ width: 60%; margin: 0; } } .details{ text-align: left; h3{ font-size: 2rem; text-align: left; } } .features{ text-align: center; padding: 1rem; &:hover{ background: rgba(white,.1); } @media (max-width: $bp-s){ width: 100%; margin: 0; text-align: left; border-bottom: 1px solid rgba(white,.2); &:last-child{ border: none; } } i{ font-size: 4rem; margin: 0 0 2rem 0; @media (max-width: $bp-s){ font-size: 1.5rem; width: 2rem; text-align: center; margin: 0 0 1rem 0; float: left; } } p{ margin: 0 0 1rem 0; font-size: 1rem; @media (max-width: $bp-s){ margin-left: 3rem; } } } blockquote{ position: relative; margin: 0; padding: 0; text-align: center; &:before{ display: inline-block; color: $color-primary; font-size: 2rem; content: '\201C'; } p{ margin: 0; display: inline; font-size: 1.5rem; @media (max-width: $bp-s){ font-size: 1.2rem; } } cite{ font-size: 1rem; display: block; margin: .5rem 0 0 1.2rem; @media (max-width: $bp-s){ font-size: .8rem; } &:before{ content: '–'; } } } footer{ background: $color-primary; color: #fff; padding: 2rem 0; text-align: center; font-size: .8rem; color: rgba(white,.4); ul{ margin: 0; padding: 0; list-style: none; li{ display: inline-block; a{ display: block; padding: .4rem .7rem; font-size: .9rem; text-decoration: none; color: rgba(white,.7); &:hover{ color: white; } } } } } .text--center{ text-align: center; } .text--left{ text-align: left; } .bg-image{ background: $color-primary; text-align: center; position: relative; z-index: 1; overflow: hidden; //text-shadow: 2px 0 5px black; &:before{ content: ''; display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; z-index: -1; background-size: 100%; background-attachment: fixed; filter: blur(3px); opacity: .8; transform: scale(1.1); } &.bg-image-2:before{ opacity: .6; background-position: center center; } } ================================================ FILE: Solution/Lab3/client/Landing/src/app/views/landing/landing.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-landing', templateUrl: './landing.component.html', styleUrls: ['./landing.component.scss'], }) export class LandingComponent implements OnInit { constructor(private router: Router) {} ngOnInit() {} register() { this.router.navigate(['register']); return false; } } ================================================ FILE: Solution/Lab3/client/Landing/src/app/views/register/register.component.html ================================================
Provision a new Tenant Login details will be sent to the email provided. Name Email Phone Address Plan
================================================ FILE: Solution/Lab3/client/Landing/src/app/views/register/register.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .tenant-form { display: flex; align-items: center; justify-content: center; } .wide-form { min-width: 150px; max-width: 500px; width: 100%; } .full-width { width: 100%; } .mat-form-field { width: 100%; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab3/client/Landing/src/app/views/register/register.component.ts ================================================ import { MatSnackBar } from '@angular/material/snack-bar'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.scss'], }) export class RegisterComponent implements OnInit { submitting: boolean = false; tenantForm: FormGroup = this.fb.group({ tenantName: ['', [Validators.required]], tenantEmail: ['', [Validators.email, Validators.required]], tenantTier: ['', [Validators.required]], tenantPhone: [''], tenantAddress: [''], }); constructor( private fb: FormBuilder, private _snackBar: MatSnackBar, private http: HttpClient ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; this.tenantForm.disable(); const tenant = { ...this.tenantForm.value, }; this.http .post(`${environment.apiGatewayUrl}/registration`, tenant) .subscribe({ next: () => { this.openErrorMessageSnackBar('Successfully created new tenant!'); this.tenantForm.reset(); this.tenantForm.enable(); this.submitting = false; }, error: (err) => { this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); this.tenantForm.enable(); this.submitting = false; }, }); } } ================================================ FILE: Solution/Lab3/client/Landing/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab3/client/Landing/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab3/client/Landing/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab3/client/Landing/src/environments/environment.ts ================================================ export const environment = { production: false, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab3/client/Landing/src/index.html ================================================ Landing ================================================ FILE: Solution/Lab3/client/Landing/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab3/client/Landing/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab3/client/Landing/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab3/client/Landing/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab3/client/Landing/src/styles.scss ================================================ ================================================ FILE: Solution/Lab3/client/Landing/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab3/client/Landing/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab3/client/Landing/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab3/client/Landing/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab3/client/dummy.txt ================================================ ================================================ FILE: Solution/Lab3/scripts/deploy-updates.sh ================================================ #!/bin/bash cd ../server || exit # stop execution if cd fails rm -rf .aws-sam/ python3 -m pylint -E -d E0401 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi #Deploying shared services changes echo "Deploying shared services changes" echo Y | sam sync --stack-name serverless-saas -t shared-template.yaml --code --resource-id LambdaFunctions/ServerlessSaaSLayers --resource-id LambdaFunctions/SharedServicesAuthorizerFunction -u #Deploying tenant services changes echo "Deploying tenant services changes" rm -rf .aws-sam/ echo Y | sam sync --stack-name stack-pooled -t tenant-template.yaml --code --resource-id ServerlessSaaSLayers --resource-id BusinessServicesAuthorizerFunction --resource-id CreateProductFunction -u cd ../scripts || exit ./geturl.sh ================================================ FILE: Solution/Lab3/scripts/deployment.sh ================================================ #!/bin/bash if [[ "$#" -eq 0 ]]; then echo "Invalid parameters" echo "Command to deploy client code: deployment.sh -c" echo "Command to deploy bootstrap server code: deployment.sh -b" echo "Command to deploy tenant server code: deployment.sh -t" echo "Command to deploy bootstrap & tenant server code: deployment.sh -s" echo "Command to deploy server & client code: deployment.sh -s -c" exit 1 fi while [[ "$#" -gt 0 ]]; do case $1 in -s) server=1 ;; -b) bootstrap=1 ;; -t) tenant=1 ;; -c) client=1 ;; *) echo "Unknown parameter passed: $1"; exit 1 ;; esac shift done # During AWS hosted events using event engine tool # we pre-provision cloudfront and s3 buckets which hosts UI code. # So that it improves this labs total execution time. # Below code checks if cloudfront and s3 buckets are # pre-provisioned or not and then concludes if the workshop # is running in AWS hosted event through event engine tool or not. IS_RUNNING_IN_EVENT_ENGINE=false PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" IS_RUNNING_IN_EVENT_ENGINE=true ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_BUCKET=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSiteBucket'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) fi if [[ $server -eq 1 ]] || [[ $bootstrap -eq 1 ]] || [[ $tenant -eq 1 ]]; then echo "Validating server code using pylint" cd ../server python3 -m pylint -E -d E0401,E1111 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi cd ../scripts fi if [[ $server -eq 1 ]] || [[ $bootstrap -eq 1 ]]; then echo "Bootstrap server code is getting deployed" cd ../server REGION=$(aws configure get region) sam build -t shared-template.yaml --use-container if [ "$IS_RUNNING_IN_EVENT_ENGINE" = true ]; then sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE AdminUserPoolCallbackURLParameter=$ADMIN_SITE_URL TenantUserPoolCallbackURLParameter=$APP_SITE_URL else sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE fi cd ../scripts fi if [[ $server -eq 1 ]] || [[ $tenant -eq 1 ]]; then echo "Tenant server code is getting deployed" cd ../server REGION=$(aws configure get region) sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml --region=$REGION cd ../scripts fi if [ "$IS_RUNNING_IN_EVENT_ENGINE" = false ]; then ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_BUCKET=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSiteBucket'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi if [[ $client -eq 1 ]]; then echo "Client code is getting deployed" ADMIN_APIGATEWAYURL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminApi'].OutputValue" --output text) APP_APIGATEWAYURL=$(aws cloudformation describe-stacks --stack-name stack-pooled --query "Stacks[0].Outputs[?OutputKey=='TenantAPI'].OutputValue" --output text) APP_APPCLIENTID=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='CognitoTenantAppClientId'].OutputValue" --output text) APP_USERPOOLID=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='CognitoTenantUserPoolId'].OutputValue" --output text) # Admin UI and Landing UI are configured in Lab2 echo "Admin UI and Landing UI are configured in Lab2. Only App UI will be configured in this Lab3." # Configuring app UI echo "aws s3 ls s3://$APP_SITE_BUCKET" aws s3 ls s3://$APP_SITE_BUCKET if [ $? -ne 0 ]; then echo "Error! S3 Bucket: $APP_SITE_BUCKET not readable" exit 1 fi cd ../client/Application echo "Configuring environment for App Client" cat << EoF > ./src/environments/environment.prod.ts export const environment = { production: true, regApiGatewayUrl: '$ADMIN_APIGATEWAYURL', apiGatewayUrl: '$APP_APIGATEWAYURL', userPoolId: '$APP_USERPOOLID', appClientId: '$APP_APPCLIENTID', }; EoF cat << EoF > ./src/environments/environment.ts export const environment = { production: true, regApiGatewayUrl: '$ADMIN_APIGATEWAYURL', apiGatewayUrl: '$APP_APIGATEWAYURL', userPoolId: '$APP_USERPOOLID', appClientId: '$APP_APPCLIENTID', }; EoF npm install --legacy-peer-deps && npm run build echo "aws s3 sync --delete --cache-control no-store dist s3://$APP_SITE_BUCKET" aws s3 sync --delete --cache-control no-store dist s3://$APP_SITE_BUCKET if [[ $? -ne 0 ]]; then exit 1 fi echo "Completed configuring environment for App Client" echo "Successfully completed deploying Application UI" fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Solution/Lab3/scripts/geturl.sh ================================================ PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) else ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Solution/Lab3/server/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Solution/Lab3/server/OrderService/order_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Order: key='' def __init__(self, shardId, orderId, orderName, orderProducts): self.shardId = shardId self.orderId = orderId self.key = shardId + ':' + orderId self.orderName = orderName self.orderProducts = orderProducts class OrderProduct: def __init__(self, productId, price, quantity): self.productId = productId self.price = price self.quantity = quantity ================================================ FILE: Solution/Lab3/server/OrderService/order_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import order_service_dal from decimal import Decimal from types import SimpleNamespace from aws_lambda_powertools import Tracer tracer = Tracer() @tracer.capture_lambda_handler def get_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a order") params = event['pathParameters'] key = params['id'] logger.log_with_tenant_context(event, params) order = order_service_dal.get_order(event, key) logger.log_with_tenant_context(event, "Request completed to get a order") metrics_manager.record_metric(event, "SingleOrderRequested", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def create_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) order = order_service_dal.create_order(event, payload) logger.log_with_tenant_context(event, "Request completed to create a order") metrics_manager.record_metric(event, "OrderCreated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def update_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] order = order_service_dal.update_order(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a order") metrics_manager.record_metric(event, "OrderUpdated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def delete_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a order") params = event['pathParameters'] key = params['id'] response = order_service_dal.delete_order(event, key) logger.log_with_tenant_context(event, "Request completed to delete a order") metrics_manager.record_metric(event, "OrderDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the order") @tracer.capture_lambda_handler def get_orders(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all orders") response = order_service_dal.get_orders(event, tenantId) metrics_manager.record_metric(event, "OrdersRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all orders") return utils.generate_response(response) ================================================ FILE: Solution/Lab3/server/OrderService/order_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid from order_models import Order import json import utils from types import SimpleNamespace import logger import random import threading from boto3.dynamodb.conditions import Key import metrics_manager table_name = os.environ['ORDER_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) suffix_start = 1 suffix_end = 10 def get_order(event, key): try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) response = table.get_item(Key={'shardId': shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a order', e) else: return order def delete_order(event, key): try: shardId = key.split(":")[0] orderId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a order', e) else: logger.info("DeleteItem succeeded:") return response def create_order(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) order = Order(shardId, str(uuid.uuid4()), payload.orderName, payload.orderProducts) try: response = table.put_item(Item={ 'shardId':shardId, 'orderId': order.orderId, 'orderName': order.orderName, 'orderProducts': get_order_products_dict(order.orderProducts) }, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a order', e) else: logger.info("PutItem succeeded:") return order def update_order(event, payload, key): try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) order = Order(shardId, orderId,payload.orderName, payload.orderProducts) response = table.update_item(Key={'shardId':order.shardId, 'orderId': order.orderId}, UpdateExpression="set orderName=:orderName, " +"orderProducts=:orderProducts", ExpressionAttributeValues={ ':orderName': order.orderName, ':orderProducts': get_order_products_dict(order.orderProducts) }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a order', e) else: logger.info("UpdateItem succeeded:") return order def get_orders(event, tenantId): get_all_products_response = [] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error("Error getting all orders") raise Exception('Error getting all orders', e) else: logger.info("Get orders succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) get_all_products_response.append(order) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def get_order_products_dict(orderProducts): orderProductList = [] for i in range(len(orderProducts)): product = orderProducts[i] orderProductList.append(vars(product)) return orderProductList ================================================ FILE: Solution/Lab3/server/OrderService/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab3/server/ProductService/product_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Product: key ='' def __init__(self, shardId, productId, sku, name, price, category): self.shardId = shardId self.productId = productId self.key = shardId + ':' + productId self.sku = sku self.name = name self.price = price self.category = category class Category: def __init__(self, id, name): self.id = id self.name = name ================================================ FILE: Solution/Lab3/server/ProductService/product_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import product_service_dal from aws_lambda_powertools import Tracer from decimal import Decimal from types import SimpleNamespace tracer = Tracer() @tracer.capture_lambda_handler def get_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a product") params = event['pathParameters'] logger.log_with_tenant_context(event, params) key = params['id'] logger.log_with_tenant_context(event, key) product = product_service_dal.get_product(event, key) logger.log_with_tenant_context(event, "Request completed to get a product") metrics_manager.record_metric(event, "SingleProductRequested", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def create_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) product = product_service_dal.create_product(event, payload) logger.log_with_tenant_context(event, "Request completed to create a product") metrics_manager.record_metric(event, "ProductCreated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def update_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] product = product_service_dal.update_product(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a product") metrics_manager.record_metric(event, "ProductUpdated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def delete_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a product") params = event['pathParameters'] key = params['id'] response = product_service_dal.delete_product(event, key) logger.log_with_tenant_context(event, "Request completed to delete a product") metrics_manager.record_metric(event, "ProductDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the product") @tracer.capture_lambda_handler def get_products(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all products") response = product_service_dal.get_products(event, tenantId) metrics_manager.record_metric(event, "ProductsRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all products") return utils.generate_response(response) ================================================ FILE: Solution/Lab3/server/ProductService/product_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid import json import logger import random import threading import metrics_manager from product_models import Product from types import SimpleNamespace from boto3.dynamodb.conditions import Key table_name = os.environ['PRODUCT_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(table_name) suffix_start = 1 suffix_end = 10 def get_product(event, key): try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) response = table.get_item(Key={'shardId': shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a product', e) else: logger.info("GetItem succeeded:"+ str(product)) return product def delete_product(event, key): try: shardId = key.split(":")[0] productId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a product', e) else: logger.info("DeleteItem succeeded:") return response def create_product(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) product = Product(shardId, str(uuid.uuid4()), payload.sku,payload.name, payload.price, payload.category) try: response = table.put_item( Item= { 'shardId': shardId, 'productId': product.productId, 'sku': product.sku, 'name': product.name, 'price': product.price, 'category': product.category }, ReturnConsumedCapacity='TOTAL' ) metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: logger.info("PutItem succeeded:") return product def update_product(event, payload, key): try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) product = Product(shardId,productId,payload.sku, payload.name, payload.price, payload.category) response = table.update_item(Key={'shardId':product.shardId, 'productId': product.productId}, UpdateExpression="set sku=:sku, #n=:productName, price=:price, category=:category", ExpressionAttributeNames= {'#n':'name'}, ExpressionAttributeValues={ ':sku': product.sku, ':productName': product.name, ':price': product.price, ':category': product.category }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a product', e) else: logger.info("UpdateItem succeeded:") return product def get_products(event, tenantId): get_all_products_response =[] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting all products', e) else: logger.info("Get products succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) get_all_products_response.append(product) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) ================================================ FILE: Solution/Lab3/server/ProductService/requirements.txt ================================================ requests pytest-mock aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab3/server/README.md ================================================ sam build -t shared-template.yaml --use-container sam deploy --config-file shared-samconfig.toml sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml ================================================ FILE: Solution/Lab3/server/Resources/requirements.txt ================================================ python-jose[cryptography] ================================================ FILE: Solution/Lab3/server/Resources/shared_service_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import utils import auth_manager region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] tenant_userpool_id = os.environ['TENANT_USER_POOL'] tenant_appclient_id = os.environ['TENANT_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) if(auth_manager.isSaaSProvider(unauthorized_claims['custom:userRole'])): userpool_id = user_pool_operation_user appclient_id = app_client_operation_user else: #get tenant user pool and app client to validate jwt token against userpool_id = tenant_userpool_id appclient_id = tenant_appclient_id #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] #only tenant admin and system admin can do certain actions like create and disable users if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): policy.allowAllMethods() if (auth_manager.isTenantAdmin(user_role)): policy.denyMethod(HttpVerb.POST, "tenant-activation") policy.denyMethod(HttpVerb.GET, "tenants") else: #if not tenant admin or system admin then only allow to get info and update info policy.allowMethod(HttpVerb.GET, "user/*") policy.allowMethod(HttpVerb.PUT, "user/*") authResponse = policy.build() context = { 'userName': user_name, 'userPoolId': userpool_id, 'tenantId': tenant_id, 'userRole': user_role } authResponse['context'] = context return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Solution/Lab3/server/Resources/tenant_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import auth_manager import utils region = os.environ['AWS_REGION'] userpool_id = os.environ['TENANT_USER_POOL'] appclient_id = os.environ['TENANT_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] #roles are not fine-grained enough to allow selectively policy.allowAllMethods() authResponse = policy.build() context = { 'userName': user_name, 'tenantId': tenant_id } authResponse['context'] = context return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Solution/Lab3/server/TenantManagementService/requirements.txt ================================================ requests aws_requests_auth ================================================ FILE: Solution/Lab3/server/TenantManagementService/tenant-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import json import boto3 from boto3.dynamodb.conditions import Key import urllib.parse import utils from botocore.exceptions import ClientError import logger import requests import metrics_manager import auth_manager from aws_lambda_powertools import Tracer tracer = Tracer() region = os.environ['AWS_REGION'] dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') #This method has been locked down to be only called from tenant registration service def create_tenant(event, context): tenant_details = json.loads(event['body']) try: response = table_tenant_details.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'tenantName' : tenant_details['tenantName'], 'tenantAddress': tenant_details['tenantAddress'], 'tenantEmail': tenant_details['tenantEmail'], 'tenantPhone': tenant_details['tenantPhone'], 'tenantTier': tenant_details['tenantTier'], 'isActive': True } ) except Exception as e: raise Exception('Error creating a new tenant', e) else: return utils.create_success_response("Tenant Created") def get_tenants(event, context): try: response = table_tenant_details.scan() except Exception as e: raise Exception('Error getting all tenants', e) else: return utils.generate_response(response['Items']) @tracer.capture_lambda_handler def update_tenant(event, context): requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_details = json.loads(event['body']) tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response_update = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set tenantName = :tenantName, tenantAddress = :tenantAddress, tenantEmail = :tenantEmail, tenantPhone = :tenantPhone, tenantTier=:tenantTier", ExpressionAttributeValues={ ':tenantName' : tenant_details['tenantName'], ':tenantAddress': tenant_details['tenantAddress'], ':tenantEmail': tenant_details['tenantEmail'], ':tenantPhone': tenant_details['tenantPhone'], ':tenantTier': tenant_details['tenantTier'] }, ReturnValues="UPDATED_NEW" ) logger.log_with_tenant_context(event, response_update) logger.log_with_tenant_context(event, "Request completed to update tenant") return utils.create_success_response("Tenant Updated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_tenant(event, context): requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get tenant details") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): tenant_details = table_tenant_details.get_item( Key={ 'tenantId': tenant_id, }, AttributesToGet=[ 'tenantName', 'tenantAddress', 'tenantEmail', 'tenantPhone' ] ) item = tenant_details['Item'] tenant_info = TenantInfo(item['tenantName'], item['tenantAddress'],item['tenantEmail'], item['tenantPhone']) logger.log_with_tenant_context(event, tenant_info) logger.log_with_tenant_context(event, "Request completed to get tenant details") return utils.create_success_response(tenant_info.__dict__) else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def deactivate_tenant(event, context): url_disable_users = os.environ['DISABLE_USERS_BY_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to deactivate tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': False }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) update_details = {} update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_disable_users(update_details, headers, auth, host, stage_name, url_disable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to deactivate tenant") return utils.create_success_response("Tenant Deactivated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def activate_tenant(event, context): url_enable_users = os.environ['ENABLE_USERS_BY_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to activate tenant") if (auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': True }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) update_details = {} update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_enable_users(update_details, headers, auth, host, stage_name, url_enable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to activate tenant") return utils.create_success_response("Tenant Activated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only system admin can activate tenant!") return utils.create_unauthorized_response() def __invoke_disable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url, '/']) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while disabling users for the tenant') except Exception as e: logger.error('Error occured while disabling users for the tenant') raise Exception('Error occured while disabling users for the tenant', e) else: return "Success invoking disable users" def __invoke_enable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url, '/']) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while enabling users for the tenant') except Exception as e: logger.error('Error occured while enabling users for the tenant') raise Exception('Error occured while enabling users for the tenant', e) else: return "Success invoking enable users" class TenantInfo: def __init__(self, tenant_name, tenant_address, tenant_email, tenant_phone): self.tenant_name = tenant_name self.tenant_address = tenant_address self.tenant_email = tenant_email self.tenant_phone = tenant_phone ================================================ FILE: Solution/Lab3/server/TenantManagementService/tenant-registration.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import utils import uuid import logger import requests import re region = os.environ['AWS_REGION'] create_tenant_admin_user_resource_path = os.environ['CREATE_TENANT_ADMIN_USER_RESOURCE_PATH'] create_tenant_resource_path = os.environ['CREATE_TENANT_RESOURCE_PATH'] lambda_client = boto3.client('lambda') def register_tenant(event, context): try: tenant_id = uuid.uuid1().hex tenant_details = json.loads(event['body']) tenant_details['tenantId'] = tenant_id logger.info(tenant_details) stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) create_user_response = __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name) logger.info (create_user_response) tenant_details['tenantAdminUserName'] = create_user_response['message']['tenantAdminUserName'] create_tenant_response = __create_tenant(tenant_details, headers, auth, host, stage_name) logger.info (create_tenant_response) except Exception as e: logger.error('Error registering a new tenant') raise Exception('Error registering a new tenant', e) else: return utils.create_success_response("You have been registered in our system") def __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_admin_user_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while calling the create tenant admin user service') raise Exception('Error occured while calling the create tenant admin user service', e) else: return response_json def __create_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_resource_path]) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while creating the tenant record in table') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json ================================================ FILE: Solution/Lab3/server/TenantManagementService/user-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import sys import logger import metrics_manager import auth_manager import utils from boto3.dynamodb.conditions import Key from aws_lambda_powertools import Tracer tracer = Tracer() client = boto3.client('cognito-idp') dynamodb = boto3.resource('dynamodb') table_tenant_user_map = dynamodb.Table('ServerlessSaaS-TenantUserMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_id = os.environ['TENANT_USER_POOL_ID'] def create_tenant_admin_user(event, context): app_client_id = os.environ['TENANT_APP_CLIENT_ID'] tenant_details = json.loads(event['body']) tenant_id = tenant_details['tenantId'] logger.info(tenant_details) user_mgmt = UserManagement() #Add tenant admin now based upon user pool tenant_user_group_response = user_mgmt.create_user_group(user_pool_id,tenant_id,"User group for tenant {0}".format(tenant_id)) tenant_admin_user_name = 'tenant-admin-{0}'.format(tenant_details['tenantId']) create_tenant_admin_response = user_mgmt.create_tenant_admin(user_pool_id, tenant_admin_user_name, tenant_details) add_tenant_admin_to_group_response = user_mgmt.add_user_to_group(user_pool_id, tenant_admin_user_name, tenant_user_group_response['Group']['GroupName']) tenant_user_mapping_response = user_mgmt.create_user_tenant_mapping(tenant_admin_user_name,tenant_id) response = {"userPoolId": user_pool_id, "appClientId": app_client_id, "tenantAdminUserName": tenant_admin_user_name} return utils.create_success_response(response) @tracer.capture_lambda_handler #only tenant admin can create users def create_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to create new user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = user_details['tenantId'] else: user_tenant_id = tenant_id if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): metrics_manager.record_metric(event, "UserCreated", "Count", 1) response = client.admin_create_user( Username=user_details['userName'], UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] }, { 'Name': 'custom:tenantId', 'Value': user_tenant_id } ] ) logger.log_with_tenant_context(event, response) user_mgmt = UserManagement() user_mgmt.add_user_to_group(user_pool_id, user_details['userName'], user_tenant_id) response_mapping = user_mgmt.create_user_tenant_mapping(user_details['userName'], user_tenant_id) logger.log_with_tenant_context(event, "Request completed to create new user ") return utils.create_success_response("New user created") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can create user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_users(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] users = [] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get users") if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): response = client.list_users( UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) num_of_users = len(response['Users']) metrics_manager.record_metric(event, "Number of users", "Count", num_of_users) if (num_of_users > 0): for user in response['Users']: is_same_tenant_user = False user_info = UserInfo() for attr in user["Attributes"]: if(attr["Name"] == "custom:tenantId" and attr["Value"] == tenant_id): is_same_tenant_user = True user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] if(is_same_tenant_user): user_info.enabled = user["Enabled"] user_info.created = user["UserCreateDate"] user_info.modified = user["UserLastModifiedDate"] user_info.status = user["UserStatus"] user_info.user_name = user["Username"] users.append(user_info) return utils.generate_response(users) else: logger.log_with_tenant_context(event, "Request completed as unauthorized.") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get user") if (auth_manager.isTenantUser(user_role) and user_name != requesting_user_name): logger.log_with_tenant_context(event, "Request completed as unauthorized. User can only get its information.") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserInfoRequested", "Count", 1) user_info = get_user_info(event, user_pool_id, user_name) logger.log_with_tenant_context(event, "Request completed to get new user ") return utils.create_success_response(user_info.__dict__) @tracer.capture_lambda_handler def update_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update user") if (auth_manager.isTenantUser(user_role)): logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update user!") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserUpdated", "Count", 1) response = client.admin_update_user_attributes( Username=user_name, UserPoolId=user_pool_id, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] } ] ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to update user") return utils.create_success_response("user updated") @tracer.capture_lambda_handler def disable_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to disable new user") if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): metrics_manager.record_metric(event, "UserDisabled", "Count", 1) response = client.admin_disable_user( Username=user_name, UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to disable new user") return utils.create_success_response("User disabled") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can disable user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def disable_users_by_tenant(event, context): logger.info("Request received to disable users by tenant") tenantid_to_update = event['tenantId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if ((auth_manager.isTenantAdmin(user_role) and tenantid_to_update == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_disable_user( Username=user['userName'], UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to disable users") return utils.create_success_response("Users disabled") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def enable_users_by_tenant(event, context): logger.info("Request received to enable users by tenant") tenantid_to_update = event['tenantId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if (auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_enable_user( Username=user['userName'], UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to enable users") return utils.create_success_response("Users enables") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() def get_user_info(event, user_pool_id, user_name): metrics_manager.record_metric(event, "UserInfoRequested", "Count", 1) response = client.admin_get_user( UserPoolId=user_pool_id, Username=user_name ) logger.log_with_tenant_context(event, response) user_info = UserInfo() user_info.user_name = response["Username"] for attr in response["UserAttributes"]: if(attr["Name"] == "custom:tenantId"): user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] logger.log_with_tenant_context(event, user_info) return user_info class UserManagement: def create_user_group(self, user_pool_id, group_name, group_description): response = client.create_group( GroupName=group_name, UserPoolId=user_pool_id, Description= group_description, Precedence=0 ) return response def create_tenant_admin(self, user_pool_id, tenant_admin_user_name, user_details): response = client.admin_create_user( Username=tenant_admin_user_name, UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['tenantEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': 'TenantAdmin' }, { 'Name': 'custom:tenantId', 'Value': user_details['tenantId'] } ] ) return response def add_user_to_group(self, user_pool_id, user_name, group_name): response = client.admin_add_user_to_group( UserPoolId=user_pool_id, Username=user_name, GroupName=group_name ) return response def create_user_tenant_mapping(self, user_name, tenant_id): response = table_tenant_user_map.put_item( Item={ 'tenantId': tenant_id, 'userName': user_name } ) return response class UserInfo: def __init__(self, user_name=None, tenant_id=None, user_role=None, email=None, status=None, enabled=None, created=None, modified=None): self.user_name = user_name self.tenant_id = tenant_id self.user_role = user_role self.email = email self.status = status self.enabled = enabled self.created = created self.modified = modified ================================================ FILE: Solution/Lab3/server/layers/auth_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils # These are the roles being supported in this reference architecture class UserRoles: SYSTEM_ADMIN = "SystemAdmin" CUSTOMER_SUPPORT = "CustomerSupport" TENANT_ADMIN = "TenantAdmin" TENANT_USER = "TenantUser" def isTenantAdmin(user_role): if (user_role == UserRoles.TENANT_ADMIN): return True else: return False def isSystemAdmin(user_role): if (user_role == UserRoles.SYSTEM_ADMIN): return True else: return False def isSaaSProvider(user_role): if (user_role == UserRoles.SYSTEM_ADMIN or user_role == UserRoles.CUSTOMER_SUPPORT): return True else: return False def isTenantUser(user_role): if (user_role == UserRoles.TENANT_USER): return True else: return False ================================================ FILE: Solution/Lab3/server/layers/logger.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from aws_lambda_powertools import Logger logger = Logger() """Log info messages """ def info(log_message): logger.info (log_message) """Log error messages """ def error(log_message): logger.error (log_message) def log_with_tenant_context(event, log_message): logger.structure_logs(append=True, tenant_id= event['requestContext']['authorizer']['tenantId']) logger.info (log_message) ================================================ FILE: Solution/Lab3/server/layers/metrics_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json from aws_lambda_powertools import Metrics metrics = Metrics() def record_metric(event, metric_name, metric_unit, metric_value): """ Record the metric in Cloudwatch using EMF format Args: event ([type]): [description] metric_name ([type]): [description] metric_unit ([type]): [description] metric_value ([type]): [description] """ metrics.add_dimension(name="tenant_id", value=event['requestContext']['authorizer']['tenantId']) metrics.add_metric(name=metric_name, unit=metric_unit, value=metric_value) metrics_object = metrics.serialize_metric_set() metrics.clear_metrics() print(json.dumps(metrics_object)) ================================================ FILE: Solution/Lab3/server/layers/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] simplejson jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab3/server/layers/utils.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import jsonpickle import simplejson import boto3 from aws_requests_auth.aws_auth import AWSRequestsAuth from enum import Enum class StatusCodes(Enum): SUCCESS = 200 UN_AUTHORIZED = 401 NOT_FOUND = 404 def create_success_response(message): return { "statusCode": StatusCodes.SUCCESS.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def create_unauthorized_response(): return { "statusCode": StatusCodes.UN_AUTHORIZED.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": "User not authorized to perform this action" }), } def generate_response(inputObject): return { "statusCode": 200, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": encode_to_json_object(inputObject), } def encode_to_json_object(inputObject): jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True) jsonpickle.set_preferred_backend('simplejson') return jsonpickle.encode(inputObject, unpicklable=False, use_decimal=True) def get_auth(host, region): session = boto3.Session() credentials = session.get_credentials() auth = AWSRequestsAuth(aws_access_key=credentials.access_key, aws_secret_access_key=credentials.secret_key, aws_token=credentials.token, aws_host=host, aws_region=region, aws_service='execute-api') return auth def get_headers(event): return event['headers'] ================================================ FILE: Solution/Lab3/server/nested_templates/apigateway.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway, apis, api keys and usage plan as part of bootstrap Parameters: StageName: Type: String RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String Resources: ApiGatewayCloudWatchLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub apigateway-cloudwatch-publish-role-${AWS::Region} Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole ApiGatewayAttachCloudwatchLogArn: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchLogRole.Arn AdminApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/api-gateway/access-logs-serverless-saas-admin-api RetentionInDays: 30 AdminApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: "/*" HttpMethod: "*" Auth: ResourcePolicy: CustomStatements: - Effect: Allow Principal: "*" Action: "execute-api:Invoke" Resource: ["execute-api:/*/*/*"] - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/tenant" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/user/tenant-admin" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref RegisterTenantLambdaExecutionRoleArn - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/disable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/enable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn AccessLogSetting: DestinationArn: !GetAtt AdminApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ["", ["serverless-saas-admin-api-", !Ref "AWS::Region"]] basePath: !Join ["", ["/", !Ref StageName]] schemes: - https paths: /registration: post: summary: Register a new tenant description: Register a new tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref RegisterTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant/activation/{tenantid}: put: security: - api_key: [] - Authorizer: [] summary: Activate an existing tenant description: Activate an existing tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ActivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenants: get: summary: Returns all tenants description: Returns all tenants produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantsFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant: post: summary: Creates a tenant description: Creates a tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/{tenantid}: get: summary: Returns a tenant description: Return a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Disables a tenant description: Disables a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeactivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy put: summary: Updates a tenant description: Updates a tenant produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user/{username}: get: summary: Returns a user description: Return a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUserFunctionArn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Diables a user description: Disable a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUserFunctionArn - /invocations httpMethod: POST type: aws_proxy /user/tenant-admin: post: summary: Creates a tenant admin user description: Creates a tenant admin user produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantAdminUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user: post: summary: Create a user description: Create a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users: get: summary: Get all users by tenantId description: Get all users by tenantId produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUsersFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/disable: put: summary: disable users by tenant id description: disable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/enable: put: summary: enable users by tenant id description: enable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref EnableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: sigv4Reference: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "awsSigv4" Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref AuthorizerFunctionArn - /invocations authorizerResultTtlInSeconds: 60 type: "token" StageName: prod Outputs: AdminApiGatewayApi: Value: !Ref AdminApiGatewayApi ================================================ FILE: Solution/Lab3/server/nested_templates/apigateway_lambdapermissions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway and apis as part of bootstrap Parameters: RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String AdminApiGatewayApi: Type: String Resources: #provide api gateway permissions to call lambda functions RegisterTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref RegisterTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateTenantAdminUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantAdminUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] EnableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref EnableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUsersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUsersFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref AuthorizerFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*" ]] CreateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantsFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeactivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeactivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ActivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ActivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ================================================ FILE: Solution/Lab3/server/nested_templates/cognito.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup cognito as part of bootstrap Parameters: AdminEmailParameter: Type: String Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Description: "Enter the role name for system admin" AdminUserPoolCallbackURLParameter: Type: String Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Description: "Enter Tenant Management userpool call back url" Resources: CognitoUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: PooledTenant-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into tenant UI application at " - "https://" - !Ref TenantUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for tenant UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSClient GenerateSecret: false UserPoolId: !Ref CognitoUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [pooledtenant-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoUserPool CognitoOperationUsersUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: OperationUsers-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into admin UI application at " - "https://" - !Ref AdminUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for admin UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoOperationUsersUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSOperationUsersPoolClient GenerateSecret: false UserPoolId: !Ref CognitoOperationUsersUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoOperationUsersUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [operationsusers-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUserGroup: Type: AWS::Cognito::UserPoolGroup Properties: GroupName: SystemAdmins Description: Admin user group Precedence: 0 UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUser: Type: AWS::Cognito::UserPoolUser Properties: Username: admin DesiredDeliveryMediums: - EMAIL ForceAliasCreation: true UserAttributes: - Name: email Value: !Ref AdminEmailParameter - Name: custom:tenantId Value: system_admins - Name: custom:userRole Value: !Ref SystemAdminRoleNameParameter UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAddUserToGroup: Type: AWS::Cognito::UserPoolUserToGroupAttachment Properties: GroupName: !Ref CognitoAdminUserGroup Username: !Ref CognitoAdminUser UserPoolId: !Ref CognitoOperationUsersUserPool Outputs: CognitoUserPoolId: Value: !Ref CognitoUserPool CognitoUserPoolClientId: Value: !Ref CognitoUserPoolClient CognitoOperationUsersUserPoolId: Value: !Ref CognitoOperationUsersUserPool CognitoOperationUsersUserPoolClientId: Value: !Ref CognitoOperationUsersUserPoolClient CognitoOperationUsersUserPoolProviderURL: Value: !GetAtt CognitoOperationUsersUserPool.ProviderURL ================================================ FILE: Solution/Lab3/server/nested_templates/lambdafunctions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy lambda functions as part of bootstrap Parameters: CognitoOperationUsersUserPoolId: Type: String CognitoOperationUsersUserPoolClientId: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String TenantDetailsTableArn: Type: String TenantUserMappingTableArn: Type: String Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies Description: Utilities for project ContentUri: ../layers/ CompatibleRuntimes: - python3.9 LicenseInfo: "MIT" RetentionPolicy: Retain Metadata: BuildMethod: python3.9 AuthorizerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: authorizer-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: authorizer-execution-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:List* Resource: - !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn SharedServicesAuthorizerFunction: Type: AWS::Serverless::Function DependsOn: AuthorizerExecutionRole Properties: CodeUri: ../Resources/ Handler: shared_service_authorizer.lambda_handler Runtime: python3.9 Role: !GetAtt AuthorizerExecutionRole.Arn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !Ref CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !Ref CognitoOperationUsersUserPoolClientId TENANT_USER_POOL: !Ref CognitoUserPoolId TENANT_APP_CLIENT: !Ref CognitoUserPoolClientId #Create user pool for the tenant TenantUserPoolLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-userpool-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-userpool-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn - Effect: Allow Action: - dynamodb:GetItem - dynamodb:Query Resource: - !Ref TenantUserMappingTableArn CreateUserLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub create-user-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-user-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:PutItem Resource: - !Ref TenantUserMappingTableArn - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn CreateTenantAdminUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_tenant_admin_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId TENANT_APP_CLIENT_ID: !Ref CognitoUserPoolClientId POWERTOOLS_SERVICE_NAME: "UserManagement.CreateTenantAdmin" #User management CreateUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.CreateUser" UpdateUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.update_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.UpdateUser" DisableUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUser" DisableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUsersByTenant" EnableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.enable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.EnableUsersByTenant" GetUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.GetUser" GetUsersFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_users Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.GetUsers" #Tenant Management TenantManagementLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-management-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-tenant-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:Scan - dynamodb:Query Resource: - !Ref TenantDetailsTableArn - !Join ["", [!Ref TenantDetailsTableArn, '/index/*']] CreateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.create_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.CreateTenant" ActivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.activate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.ActivateTenant" ENABLE_USERS_BY_TENANT: "/users/enable" PROVISION_TENANT: "/provisioning/" GetTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.GetTenant" DeactivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.deactivate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.DeactivateTenant" DEPROVISION_TENANT: "/provisioning/" DISABLE_USERS_BY_TENANT: "/users/disable" UpdateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.update_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.UpdateTenant" GetTenantsFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenants Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers #Tenant Registration RegisterTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-registration-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess RegisterTenantFunction: Type: AWS::Serverless::Function DependsOn: RegisterTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-registration.register_tenant Runtime: python3.9 Role: !GetAtt RegisterTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: # Need to find a better way than hard coding resource paths CREATE_TENANT_ADMIN_USER_RESOURCE_PATH: "/user/tenant-admin" CREATE_TENANT_RESOURCE_PATH: "/tenant" PROVISION_TENANT_RESOURCE_PATH: "/provisioning" POWERTOOLS_SERVICE_NAME: "TenantRegistration.RegisterTenant" Outputs: RegisterTenantLambdaExecutionRoleArn: Value: !GetAtt RegisterTenantLambdaExecutionRole.Arn TenantManagementLambdaExecutionRoleArn: Value: !GetAtt TenantManagementLambdaExecutionRole.Arn RegisterTenantFunctionArn: Value: !GetAtt RegisterTenantFunction.Arn ActivateTenantFunctionArn: Value: !GetAtt ActivateTenantFunction.Arn GetTenantsFunctionArn: Value: !GetAtt GetTenantsFunction.Arn CreateTenantFunctionArn: Value: !GetAtt CreateTenantFunction.Arn GetTenantFunctionArn: Value: !GetAtt GetTenantFunction.Arn DeactivateTenantFunctionArn: Value: !GetAtt DeactivateTenantFunction.Arn UpdateTenantFunctionArn: Value: !GetAtt UpdateTenantFunction.Arn GetUsersFunctionArn: Value: !GetAtt GetUsersFunction.Arn GetUserFunctionArn: Value: !GetAtt GetUserFunction.Arn UpdateUserFunctionArn: Value: !GetAtt UpdateUserFunction.Arn DisableUserFunctionArn: Value: !GetAtt DisableUserFunction.Arn CreateTenantAdminUserFunctionArn: Value: !GetAtt CreateTenantAdminUserFunction.Arn CreateUserFunctionArn: Value: !GetAtt CreateUserFunction.Arn DisableUsersByTenantFunctionArn: Value: !GetAtt DisableUsersByTenantFunction.Arn EnableUsersByTenantFunctionArn: Value: !GetAtt EnableUsersByTenantFunction.Arn SharedServicesAuthorizerFunctionArn: Value: !GetAtt SharedServicesAuthorizerFunction.Arn ================================================ FILE: Solution/Lab3/server/nested_templates/tables.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to create dynamodb tables as part of bootstrap Resources: TenantDetailsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: tenantName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: ServerlessSaas-TenantConfig KeySchema: - AttributeName: tenantName KeyType: HASH Projection: NonKeyAttributes: - userPoolId - appClientId - apiGatewayUrl ProjectionType: INCLUDE TableName: ServerlessSaaS-TenantDetails TenantUserMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: userName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH - AttributeName: userName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantUserMapping GlobalSecondaryIndexes: - IndexName: UserName KeySchema: - AttributeName: userName KeyType: HASH - AttributeName: tenantId KeyType: RANGE Projection: ProjectionType: ALL Outputs: TenantDetailsTableArn: Value: !GetAtt TenantDetailsTable.Arn TenantDetailsTableName: Value: !Ref TenantDetailsTable TenantUserMappingTableArn: Value: !GetAtt TenantUserMappingTable.Arn TenantUserMappingTableName: Value: !Ref TenantUserMappingTable ================================================ FILE: Solution/Lab3/server/nested_templates/userinterface.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: IsCloudFrontAndS3PreProvisioned: Type: String Default: false Description: "Tells if cloudfront and s3 buckets are pre-provisioned or not. They get pre-provisioned when the workshop is running as a part of AWS Event through AWS event engine tool." Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref IsCloudFrontAndS3PreProvisioned, true] ] Resources: CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: IsNotRunningInEventEngine Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distributions" AdminAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AdminAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AdminAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AdminAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId AdminAppSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: #Aliases: # - !Sub 'admin.${CustomDomainName}' CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS CachedMethods: - GET - HEAD - OPTIONS Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: adminapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt AdminAppBucket.RegionalDomainName Id: adminapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' LandingAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True LandingAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref LandingAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${LandingAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId LandingApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: landingapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'LandingAppBucket.RegionalDomainName' Id: landingapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' AppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId ApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: tenantapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'AppBucket.RegionalDomainName' Id: tenantapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: AdminBucket: Description: The name of the bucket for uploading the Admin Management site to Value: !Ref AdminAppBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt AdminAppSite.DomainName Condition: IsNotRunningInEventEngine LandingAppBucket: Description: The name of the bucket for uploading the Landing site to Value: !Ref LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt LandingApplicationSite.DomainName Condition: IsNotRunningInEventEngine AppBucket: Description: The name of the bucket for uploading the Tenant Management site to Value: !Ref AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt ApplicationSite.DomainName Condition: IsNotRunningInEventEngine ================================================ FILE: Solution/Lab3/server/shared-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas" s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx" s3_prefix = "serverless-saas" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" ================================================ FILE: Solution/Lab3/server/shared-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to Bootstrap the Common Resources Parameters: AdminEmailParameter: Type: String Default: "test@test.com" Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Default: "SystemAdmin" Description: "Enter the role name for system admin" StageName: Type: String Default: "prod" Description: "Stage Name for the api" EventEngineParameter: Type: String Default: false Description: "Tells if this workshop is running as a part of AWS Event through AWS EventEngine or not" AdminUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Tenant Management userpool call back url" Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref EventEngineParameter, true] ] Resources: DynamoDBTables: Type: AWS::Serverless::Application Properties: Location: nested_templates/tables.yaml #Create cloudfront and s3 for UI Cde UserInterface: Type: AWS::Serverless::Application Properties: Location: nested_templates/userinterface.yaml Parameters: IsCloudFrontAndS3PreProvisioned: !Ref EventEngineParameter Cognito: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/cognito.yaml Parameters: AdminEmailParameter: !Ref AdminEmailParameter SystemAdminRoleNameParameter: !Ref SystemAdminRoleNameParameter AdminUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.AdminAppSite, !Ref AdminUserPoolCallbackURLParameter] TenantUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.ApplicationSite, !Ref TenantUserPoolCallbackURLParameter] LambdaFunctions: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/lambdafunctions.yaml Parameters: CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId CognitoOperationUsersUserPoolId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId CognitoOperationUsersUserPoolClientId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId TenantDetailsTableArn: !GetAtt DynamoDBTables.Outputs.TenantDetailsTableArn TenantUserMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantUserMappingTableArn APIs: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway.yaml Parameters: StageName: !Ref StageName RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn APIGatewayLambdaPermissions: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway_lambdapermissions.yaml Parameters: RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn AdminApiGatewayApi: !GetAtt APIs.Outputs.AdminApiGatewayApi Outputs: AdminApi: Description: "API Gateway endpoint URL for Admin API" Value: !Join ["", ["https://", !GetAtt APIs.Outputs.AdminApiGatewayApi, ".execute-api.", !Ref "AWS::Region", ".amazonaws.com/", !Ref StageName]] AdminSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant administration application Value: !GetAtt UserInterface.Outputs.AdminBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt UserInterface.Outputs.AdminAppSite Condition: IsNotRunningInEventEngine LandingApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the landing application Value: !GetAtt UserInterface.Outputs.LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt UserInterface.Outputs.LandingApplicationSite Condition: IsNotRunningInEventEngine ApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant application Value: !GetAtt UserInterface.Outputs.AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt UserInterface.Outputs.ApplicationSite Condition: IsNotRunningInEventEngine CognitoOperationUsersUserPoolProviderURL: Description: The Admin Management userpool provider url Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolProviderURL CognitoOperationUsersUserPoolClientId: Description: The Admin Management userpool client id Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId CognitoTenantUserPoolId: Description: The user pool id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolId Export: Name: "Serverless-SaaS-CognitoTenantUserPoolId" CognitoTenantAppClientId: Description: The app client id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolClientId Export: Name: "Serverless-SaaS-CognitoTenantAppClientId" ================================================ FILE: Solution/Lab3/server/tenant-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "stack-pooled" s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx" s3_prefix = "serverless-saas-tenant" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM" ================================================ FILE: Solution/Lab3/server/tenant-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Serverless SaaS Reference Architecture Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Parameters: StageName: Type: String Default: "prod" Description: "Stage Name for the api" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies-pooled Description: Utilities for project ContentUri: layers/ CompatibleRuntimes: - python3.9 LicenseInfo: 'MIT' RetentionPolicy: Retain Metadata: BuildMethod: python3.9 ProductTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: productId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: productId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: Product-pooled OrderTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: orderId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: orderId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: Order-pooled ProductFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: pooled-product-function-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: pooled-product-function-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt ProductTable.Arn GetProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable GetProductsFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_products Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable CreateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.create_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable UpdateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.update_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable DeleteProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.delete_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable OrderFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: pooled-order-function-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: pooled-order-function-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt OrderTable.Arn GetOrdersFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_orders Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable GetOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable CreateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.create_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable UpdateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.update_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable DeleteOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.delete_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable #Tenant Authorizer TenantAuthorizerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: tenant-authorizer-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: tenant-authorizer-execution-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:List* Resource: - !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* BusinessServicesAuthorizerFunction: Type: AWS::Serverless::Function DependsOn: TenantAuthorizerExecutionRole Properties: CodeUri: Resources/ Handler: tenant_authorizer.lambda_handler Runtime: python3.9 Role: !GetAtt TenantAuthorizerExecutionRole.Arn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL: !ImportValue Serverless-SaaS-CognitoTenantUserPoolId TENANT_APP_CLIENT: !ImportValue Serverless-SaaS-CognitoTenantAppClientId ApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/api-gateway/access-logs-serverless-saas-tenant-api-pooled RetentionInDays: 30 ApiGatewayTenantApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: '/*' HttpMethod: '*' AccessLogSetting: DestinationArn: !GetAtt ApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: 'pooled-serverless-saas-tenant-api' basePath: !Join ['', ['/', !Ref StageName]] x-amazon-apigateway-api-key-source : "AUTHORIZER" schemes: - https paths: /order/{id}: get: summary: Returns a order description: Return a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a order description: Deletes a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /orders: get: summary: Returns all orders description: Returns all orders. produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrdersFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /order: post: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product/{id}: get: summary: Returns a product description: Return a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a product description: Deletes a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /products: get: summary: Returns all products description: Returns all products. produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductsFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product: post: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt BusinessServicesAuthorizerFunction.Arn - /invocations authorizerResultTtlInSeconds: 30 type: "token" StageName: !Ref StageName GetProductsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt GetProductsFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrdersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrdersFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt BusinessServicesAuthorizerFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref ApiGatewayTenantApi, "/*/*" ]] Outputs: TenantApiGatewayId: Description: Id for Tenant API Gateway Value: !Ref ApiGatewayTenantApi TenantAPI: Description: "API Gateway endpoint URL for Tenant API" Value: !Join ['', [!Sub "https://${ApiGatewayTenantApi}.execute-api.${AWS::Region}.amazonaws.com/", !Ref StageName]] ================================================ FILE: Solution/Lab4/client/Admin/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab4/client/Admin/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab4/client/Admin/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab4/client/Admin/README.md ================================================ # Admin This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab4/client/Admin/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "admin": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "admin:build:production" }, "development": { "browserTarget": "admin:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "admin:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab4/client/Admin/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab4/client/Admin/package.json ================================================ { "name": "admin", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/flex-layout": "~14.0.0-beta.40", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.1.3", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab4/client/Admin/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Tenants', url: '/tenants', icon: 'groups', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Solution/Lab4/client/Admin/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: '', component: NavComponent, data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'tenants', loadChildren: () => import('./views/tenants/tenants.module').then((m) => m.TenantsModule), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab4/client/Admin/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab4/client/Admin/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Solution/Lab4/client/Admin/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { HttpInterceptorProviders } from './interceptors'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; @NgModule({ declarations: [AppComponent, NavComponent, AuthComponent], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, HttpClientModule, HttpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab4/client/Admin/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Solution/Lab4/client/Admin/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const HttpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Solution/Lab4/client/Admin/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Solution/Lab4/client/Admin/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Solution/Lab4/client/Admin/src/app/nav/nav.component.css ================================================ .sidenav-container { height: 100%; } .sidenav-content-container { background-color: lightgray; } .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .mat-toolbar.mat-primary { position: sticky; top: 0; z-index: 1; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .spinner-container { height: 80%; display: flex; justify-content: center; align-items: center; box-sizing: border-box; } ================================================ FILE: Solution/Lab4/client/Admin/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ username$ | async }}
================================================ FILE: Solution/Lab4/client/Admin/src/app/nav/nav.component.spec.ts ================================================ import { LayoutModule } from '@angular/cdk/layout'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { NavComponent } from './nav.component'; describe('NavComponent', () => { let component: NavComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [NavComponent], imports: [ NoopAnimationsModule, LayoutModule, MatButtonModule, MatIconModule, MatListModule, MatSidenavModule, MatToolbarModule, ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(NavComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should compile', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: Solution/Lab4/client/Admin/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.css'], }) export class NavComponent implements OnInit { tenantName = ''; loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router ) { this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => err); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map((sesh) => sesh && sesh.isValid()) ); const token$ = session$.pipe(map((sesh) => sesh && sesh.getIdToken())); this.username$ = token$.pipe( map((t) => t && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Solution/Lab4/client/Admin/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/dashboard/dashboard.component.html ================================================

Traffic

December 2020
================================================ FILE: Solution/Lab4/client/Admin/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } .chart-wrapper { padding-right: 20px; position: relative; margin: auto; height: 80vh; width: 80vw; } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/dashboard/dashboard.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ChartConfiguration, ChartType } from 'chart.js'; import { TenantsService } from '../tenants/tenants.service'; interface DataSet { label: string; data: number[]; } interface ChartData { tenantId: string; dataSet: DataSet[]; totalOrders: number; } @Component({ templateUrl: 'dashboard.component.html', styleUrls: ['./dashboard.component.scss'], selector: 'app-dashboard', }) export class DashboardComponent implements OnInit { constructor(private tenantSvc: TenantsService) {} data: ChartData[] = []; // lineChart public lineChartElements = 27; public lineChartData1: Array = []; public lineChartData2: Array = []; public lineChartData3: Array = []; public lineChartData: Array = [ { data: this.lineChartData1, label: 'Current', backgroundColor: 'rgba(148,159,177,0.2)', borderColor: 'rgba(148,159,177,1)', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, { data: this.lineChartData2, label: 'Previous', backgroundColor: 'rgba(77,83,96,0.2)', borderColor: 'rgba(77,83,96,1)', pointBackgroundColor: 'rgba(77,83,96,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(77,83,96,1)', fill: 'origin', }, { data: this.lineChartData3, label: 'BEP', backgroundColor: 'rgba(255,0,0,0.3)', borderColor: 'red', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, ]; public lineChartLegend = false; /* tslint:disable:max-line-length */ public lineChartLabels: Array = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ]; /* tslint:enable:max-line-length */ public lineChartType: ChartType = 'line'; public lineChartOptions: ChartConfiguration['options'] = { maintainAspectRatio: false, elements: { line: { tension: 0.5, }, }, scales: { // We use this empty structure as a placeholder for dynamic theming. x: {}, 'y-axis-0': { position: 'left', }, 'y-axis-1': { position: 'right', grid: { color: 'rgba(80,80,80,0.3)', }, ticks: { color: '#808080', }, }, }, }; public random(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } ngOnInit(): void { for (let i = 0; i <= this.lineChartElements; i++) { this.lineChartData1.push(this.random(50, 200)); this.lineChartData2.push(this.random(80, 100)); this.lineChartData3.push(65); } } } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/tenants/create/create.component.html ================================================
Provision tenant Name Email Phone Address Plan
================================================ FILE: Solution/Lab4/client/Admin/src/app/views/tenants/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/tenants/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { TenantsService } from '../tenants.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { submitting = false; tenantForm: FormGroup = this.fb.group({ tenantName: [null, [Validators.required]], tenantEmail: [null, [Validators.email, Validators.required]], tenantTier: [null, [Validators.required]], tenantPhone: [null], tenantAddress: [null], }); constructor( private fb: FormBuilder, private tenantSvc: TenantsService, private router: Router, private _snackBar: MatSnackBar ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; const user = { ...this.tenantForm.value, }; this.tenantSvc.post(this.tenantForm.value).subscribe({ next: () => { this.submitting = false; this.openErrorMessageSnackBar('Successfully created new tenant!'); this.router.navigate(['tenants']); }, error: (err) => { this.submitting = false; this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); }, }); } } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/tenants/list/list.component.html ================================================
Tenant List
Id {{ element.tenantId }} Tenant Name {{ element.tenantName }} E-Mail {{ element.tenantEmail }} Plan {{ element.tenantTier }} Status {{ element.isActive }}
================================================ FILE: Solution/Lab4/client/Admin/src/app/views/tenants/list/list.component.scss ================================================ .tenant-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/tenants/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Tenant } from '../models/tenant'; import { TenantsService } from '../tenants.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { tenants$ = new Observable(); tenantData: Tenant[] = []; isLoading: boolean = true; displayedColumns = [ 'tenantId', 'tenantName', 'tenantEmail', 'tenantTier', 'isActive', ]; constructor(private tenantSvc: TenantsService) {} ngOnInit(): void { this.tenantSvc.fetch().subscribe((data) => { this.tenantData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/tenants/models/tenant.ts ================================================ export interface Tenant { tenantId: string; tenantName: string; tenantEmail: string; tenantTier: string; isActive: boolean; } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/tenants/tenants-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', component: ListComponent, data: { title: 'Tenant List', }, }, { path: 'create', component: CreateComponent, data: { title: 'Provision new tenant', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class TenantsRoutingModule {} ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/tenants/tenants.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ListComponent } from './list/list.component'; import { TenantsRoutingModule } from './tenants-routing.module'; import { CreateComponent } from './create/create.component'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, TenantsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ReactiveFormsModule, MatSnackBarModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class TenantsModule {} ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/tenants/tenants.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Tenant } from './models/tenant'; @Injectable({ providedIn: 'root', }) export class TenantsService { constructor(private http: HttpClient) {} baseUrl = `${environment.apiUrl}`; tenantsApiUrl = `${this.baseUrl}/tenants`; registrationApiUrl = `${this.baseUrl}/registration`; // TODO strongly-type these anys as tenants once we dial in what the tenant call should return fetch(): Observable { return this.http.get(this.tenantsApiUrl); } post(tenant: Tenant): Observable { return this.http.post(this.registrationApiUrl, tenant); } } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
Tenant Id folder_special
================================================ FILE: Solution/Lab4/client/Admin/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], tenantId: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Solution/Lab4/client/Admin/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Solution/Lab4/client/Admin/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.apiUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } } ================================================ FILE: Solution/Lab4/client/Admin/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab4/client/Admin/src/aws-exports.ts ================================================ const awsmobile = { aws_project_region: 'us-west-2', aws_cognito_region: 'us-west-2', aws_user_pools_id: 'us-west-2_nWhwjijMc', aws_user_pools_web_client_id: '61ipvhmvdrfso0cftpil9irk8k', }; export default awsmobile; ================================================ FILE: Solution/Lab4/client/Admin/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab4/client/Admin/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab4/client/Admin/src/environments/environment.ts ================================================ export const environment = { production: false, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab4/client/Admin/src/index.html ================================================ Dashboard ================================================ FILE: Solution/Lab4/client/Admin/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { Amplify } from 'aws-amplify'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import aws_exports from './aws-exports'; Amplify.configure(aws_exports); if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab4/client/Admin/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab4/client/Admin/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab4/client/Admin/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab4/client/Admin/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; ================================================ FILE: Solution/Lab4/client/Admin/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab4/client/Admin/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab4/client/Admin/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab4/client/Admin/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab4/client/Application/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab4/client/Application/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab4/client/Application/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end #cypress cypress/videos/* cypress.env.json # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab4/client/Application/README.md ================================================ # Application This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab4/client/Application/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "application": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "2mb", "maximumError": "2mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "6kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "application:build:production" }, "development": { "browserTarget": "application:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "application:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab4/client/Application/cypress/README.md ================================================ # Application End-to-End Testing ## Instructions To run End-to-End (e2e) tests against the Sample Application, take the following steps: 1. Make a copy of the example env file (`cypress.env.json.example`): ```bash cp cypress.env.json.example cypress.env.json ``` 2. Edit the new file and replace the sample values with real values. The following should help when deciding what to use in place of the sample values provided: - `host`: The URL where the Sample Application is running. If testing locally, this is usually `"http://localhost:4200"`. - `tenantName`: The name of the tenant used to identify the appropriate Cognito User Pool to use for Authentication. - `tenantUsername`: The username to use when logging in. - `tenantUserPassword`: The password to use when logging in. - `email`: The email address to use for testing. (This should be a valid email address.) 3. Navigate to the root of the Application project (`aws-saas-factory-ref-solution-serverless-saas/clients/Application/`) and run the following: ```bash npx cypress run ``` This will run the tests located in the `cypress/e2e` folder. The documentation [here](https://docs.cypress.io/guides/guides/command-line#cypress-run) has more information on what can be passed in as arguments when running the Cypress tests. For example, running the following will show the Cypress UI and what is happening as each of the tests are run: ```bash npx cypress run --headed ``` ================================================ FILE: Solution/Lab4/client/Application/cypress/e2e/1-getting-started/basic-access.cy.js ================================================ /// describe('check that the app redirects to /unauthorized when tenant is not set', () => { it('redirects to unauthorized when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); }) it('redirects to unauthorized when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); }) it('redirects to unauthorized when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); }) }) describe('check that the app redirects to a page with a sign-in form when tenant is set and user is not logged in', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').should('exist') cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.location('href').should('contain', '/dashboard') }) it('redirects when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) }) ================================================ FILE: Solution/Lab4/client/Application/cypress/e2e/1-getting-started/product-testing.cy.js ================================================ /// describe('check that product, order and user functionality works as expected', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.get('form input[name="username"]').type(Cypress.env('tenantUsername')) cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() cy.wait(1500) cy.get('body').then(body => { if (body.find('form input[name="confirm_password"]').length > 0) { cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form input[name="confirm_password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() } }) }) it('can create new users and display them', () => { const email_username = Cypress.env('email').split('@')[0]; const email_domain = Cypress.env('email').split('@')[1]; const random_suffix = '+test'+Date.now().toString().slice(-3); const myUser = { name: "myUser-"+Date.now(), email: email_username + random_suffix + '@' + email_domain, role: 'userRole'+Date.now().toString().slice(-5) } cy.get("a").contains("Users").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.get("button[color='primary'").contains("Add User").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="userName"]').type(myUser.name) cy.get('input[formcontrolname="userEmail"]').type(myUser.email) cy.get('input[formcontrolname="userRole"]').type(myUser.role) }) cy.intercept({ method: 'POST', url: '**/user', }).as('postUser') cy.intercept({ method: 'GET', url: '**/users', }).as('getUsers') cy.get("button").contains("Create").click() cy.wait('@postUser') cy.go('back') cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.wait('@getUsers') cy.get('table td').contains(myUser.email) }) it('can create a new order with a new product and see them listed', () => { const myProduct = { name: "myProduct-"+Date.now(), price: Date.now().toString().slice(-3), sku: Date.now().toString().slice(-5), category: "category3", } const myOrder = { name: "myOrder-"+Date.now(), } // NOW TESTING PRODUCT CREATION // cy.get("a").contains("Products").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.get("button[color='primary'").contains("Create Product").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="name"]').type(myProduct.name) cy.get('input[formcontrolname="price"]').type(myProduct.price) cy.get('input[formcontrolname="sku"]').type(myProduct.sku) cy.get('mat-select[formcontrolname="category"]').click() }) cy.get('.mat-option-text').contains(myProduct.category).click() cy.get("button").contains("Submit").should('not.be.disabled') cy.intercept({ method: 'POST', url: '**/product', }).as('postProduct') cy.intercept({ method: 'GET', url: '**/products', }).as('getProducts') cy.get("button").contains("Submit").click() cy.wait('@postProduct') cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.wait('@getProducts') cy.get('table td').contains(myProduct.name) cy.get('table td').contains(myProduct.price) cy.get("a").contains("Orders").click() // DONE TESTING PRODUCT CREATION // // NOW TESTING ORDER CREATION // cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.get("button[color='primary']").contains("Create Order").click() cy.location().should((loc) => { expect(loc.href).to.contain('/orders/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="orderName"]').type(myOrder.name) }) cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.intercept({ method: 'POST', url: '**/order', }).as('postOrder') cy.intercept({ method: 'GET', url: '**/orders', }).as('getOrders') cy.get("button").contains("Submit").click() cy.wait('@postOrder') cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.wait('@getOrders') cy.get('table td').contains(myOrder.name) cy.get('table td').contains(new Intl.NumberFormat().format(myProduct.price * 2)) // DONE TESTING ORDER CREATION // }) }) ================================================ FILE: Solution/Lab4/client/Application/cypress.config.ts ================================================ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { supportFile: false, setupNodeEvents(on, config) { // implement node event listeners here }, }, }); ================================================ FILE: Solution/Lab4/client/Application/cypress.env.json.example ================================================ { "host": "http://localhost:4200", "tenantName": "UPDATE_ME!", "tenantUsername": "UPDATE_ME!", "tenantUserPassword": "UPDATE_ME!", "email": "test@example.com" } ================================================ FILE: Solution/Lab4/client/Application/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/application'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab4/client/Application/package.json ================================================ { "name": "application", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "e2e": "npx cypress run" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.2.0", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "cypress": "~10.3.1", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab4/client/Application/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Products', url: '/products', icon: 'sell', }, { name: 'Orders', url: '/orders', icon: 'shopping_cart', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Solution/Lab4/client/Application/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { CognitoGuard } from './cognito.guard'; export const routes: Routes = [ { path: '', redirectTo: 'unauthorized', pathMatch: 'full', }, { path: '', component: NavComponent, canActivate: [CognitoGuard], data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'orders', loadChildren: () => import('./views/orders/orders.module').then((m) => m.OrdersModule), }, { path: 'products', loadChildren: () => import('./views/products/products.module').then( (m) => m.ProductsModule ), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, { path: 'unauthorized', component: UnauthorizedComponent, }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab4/client/Application/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab4/client/Application/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Solution/Lab4/client/Application/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { httpInterceptorProviders } from './interceptors'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { environment } from 'src/environments/environment'; @NgModule({ declarations: [ AppComponent, NavComponent, AuthComponent, UnauthorizedComponent, ], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, MatSnackBarModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatIconModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, httpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab4/client/Application/src/app/cognito.guard.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, } from '@angular/router'; import { Auth } from 'aws-amplify'; import { AuthConfigurationService } from './views/auth/auth-configuration.service'; @Injectable({ providedIn: 'root' }) export class CognitoGuard implements CanActivate { constructor( private router: Router, private authConfigService: AuthConfigurationService ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Promise { if (!this.authConfigService.configureAmplifyAuth()) { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return new Promise((res, rej) => { res(false); }); } return Auth.currentSession() .then((u) => { if (u.isValid()) { return true; } else { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return false; } }) .catch((e) => { if (state.url === '/dashboard') { // if we're going to the dashboard and we're not logged in, // don't stop the flow as the amplify-authenticator will // route requests going to the dashboard to the sign-in page. return true; } console.log('Error getting current session', e); this.router.navigate(['/unauthorized']); return false; }); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const httpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Solution/Lab4/client/Application/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Solution/Lab4/client/Application/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Solution/Lab4/client/Application/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ (companyName$ | async) || "" }} {{ (username$ | async) || user.username}}
================================================ FILE: Solution/Lab4/client/Application/src/app/nav/nav.component.scss ================================================ .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .sidebar-icon-container { height: 64px; background-color: whitesmoke; display: flex; align-items: center; justify-content: center; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .footer { position: fixed; bottom: 0; width: 100%; height: 40px; background-color: whitesmoke; color: #2d3337; // text-align: center; margin: 0px; } .footer-text { display: flex; } .content { position: absolute; width: 100%; height: 90%; max-height: 90%; overflow: auto; background-color: lightgray; } ================================================ FILE: Solution/Lab4/client/Application/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; import { AuthConfigurationService } from './../views/auth/auth-configuration.service'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.scss'], }) export class NavComponent implements OnInit { loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router, private authConfigService: AuthConfigurationService ) { // this.configSvc.loadConfigurations().subscribe((val) => console.log(val)); this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => { console.log('Failed to get current session. Err: ', err); return err; }); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map( (sesh) => sesh && typeof sesh.isValid === 'function' && sesh.isValid() ) ); const token$ = session$.pipe( map( (sesh) => sesh && typeof sesh.getIdToken === 'function' && sesh.getIdToken() ) ); this.username$ = token$.pipe( map((t) => t && t.payload && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload && t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } async logout() { await Auth.signOut({ global: true }) .then((e) => { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); }) .catch((err) => { console.error('Error logging out: ', err); }); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/auth/auth-configuration.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpParams, HttpParamsOptions, } from '@angular/common/http'; import { Injectable, OnInit } from '@angular/core'; import { map, switchMap, catchError } from 'rxjs/operators'; import { throwError } from 'rxjs'; import { environment } from '../../../environments/environment'; import { ConfigParams } from './models/config-params'; import { ActivatedRoute } from '@angular/router'; import Amplify from 'aws-amplify'; import { Auth } from 'aws-amplify'; import { Router } from '@angular/router'; import { from, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AuthConfigurationService { params$: Observable; params: ConfigParams; tenantName: string; constructor( private http: HttpClient, private route: ActivatedRoute, private router: Router ) {} public setTenantConfig(tenantName: string): Promise { const url = `${environment.regApiGatewayUrl}/tenant/init/` + tenantName; this.params$ = this.http.get(url); const setup$ = this.params$.pipe( map((val) => { // remove trailing slash (/) if present val.apiGatewayUrl = val.apiGatewayUrl.replace(/\/$/, ''); localStorage.setItem('userPoolId', val.userPoolId); localStorage.setItem('tenantName', tenantName); localStorage.setItem('appClientId', val.appClientId); localStorage.setItem('apiGatewayUrl', val.apiGatewayUrl); return 'success'; }), catchError((error) => { console.log('Error setting tenant config: ', error); return throwError(error); }) ); return setup$.toPromise(); } configureAmplifyAuth(): boolean { try { const userPoolId = localStorage.getItem('userPoolId'); const appClientId = localStorage.getItem('appClientId'); if (!userPoolId || !appClientId) { return false; } const region = userPoolId?.split('_')[0]; const awsmobile = { aws_project_region: region, aws_cognito_region: region, aws_user_pools_id: userPoolId, aws_user_pools_web_client_id: appClientId, }; Amplify.configure(awsmobile); return true; } catch (err) { console.error('Unable to initialize amplify auth.', err); return false; } } cleanLocalStorage() { localStorage.removeItem('tenantName'); localStorage.removeItem('userPoolId'); localStorage.removeItem('appClientId'); localStorage.removeItem('apiGatewayUrl'); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Solution/Lab4/client/Application/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/auth/models/config-params.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ export interface ConfigParams { appClientId: string; userPoolId: string; apiGatewayUrl: string; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Solution/Lab4/client/Application/src/app/views/dashboard/dashboard.component.html ================================================

Dashboard

{{ card.title }}
================================================ FILE: Solution/Lab4/client/Application/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/dashboard/dashboard.component.ts ================================================ import { Component, ViewChild } from '@angular/core'; import { map } from 'rxjs/operators'; import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js'; import { BaseChartDirective } from 'ng2-charts'; import DataLabelsPlugin from 'chartjs-plugin-datalabels'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'], }) export class DashboardComponent { @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; public barChartOptions: ChartConfiguration['options'] = { responsive: true, maintainAspectRatio: false, // We use these empty structures as placeholders for dynamic theming. scales: { x: {}, y: { min: 10, }, }, plugins: { legend: { display: true, }, datalabels: { anchor: 'end', align: 'end', }, }, }; public barChartType: ChartType = 'bar'; public barChartPlugins = [DataLabelsPlugin]; public barChartData: ChartData<'bar'> = { labels: ['2006', '2007', '2008', '2009', '2010', '2011', '2012'], datasets: [ { data: [65, 59, 80, 81, 56, 55, 40], label: 'Series A' }, { data: [28, 48, 40, 19, 86, 27, 90], label: 'Series B' }, ], }; // events public chartClicked({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public chartHovered({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public randomize(): void { // Only Change 3 values this.barChartData.datasets[0].data = [ Math.round(Math.random() * 100), 59, 80, Math.round(Math.random() * 100), 56, Math.round(Math.random() * 100), 40, ]; this.chart?.update(); } /** Based on the screen size, switch from standard to one column per row */ cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe( map(({ matches }) => { if (matches) { return [ { title: 'Card 1', cols: 1, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 1 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; } return [ { title: 'Card 1', cols: 2, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 2 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; }) ); constructor(private breakpointObserver: BreakpointObserver) {} } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Solution/Lab4/client/Application/src/app/views/error/404.component.html ================================================

404

Oops! You're lost.

The page you are looking for was not found.

================================================ FILE: Solution/Lab4/client/Application/src/app/views/error/404.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '404.component.html', }) export class P404Component { constructor() {} } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/error/500.component.html ================================================

500

Houston, we have a problem!

The page you are looking for is temporarily unavailable.

================================================ FILE: Solution/Lab4/client/Application/src/app/views/error/500.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '500.component.html', }) export class P500Component { constructor() {} } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/error/unauthorized.component.html ================================================
Unauthorized Enter your tenant name and click submit below Tenant Name home
================================================ FILE: Solution/Lab4/client/Application/src/app/views/error/unauthorized.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .center-screen { display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; min-height: 100vh; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/error/unauthorized.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AuthConfigurationService } from './../auth/auth-configuration.service'; import { Observable } from 'rxjs'; import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-unauthorized', templateUrl: './unauthorized.component.html', styleUrls: ['./unauthorized.component.scss'], }) export class UnauthorizedComponent implements OnInit { tenantForm: FormGroup; params$: Observable; error = false; errorMessage: string; tenantNameRequired: boolean = false; constructor( private fb: FormBuilder, private authConfigService: AuthConfigurationService, private _snackBar: MatSnackBar, private router: Router ) { if ( environment.userPoolId && environment.appClientId && environment.apiGatewayUrl ) { // If a tenant's cognito configuration is provided in the // "environment" object, then we take that instead of asking // the visitor to provide the name of their tenant in order // to do a look-up for that tenant's cognito configuration. localStorage.setItem('tenantName', 'PooledTenants'); localStorage.setItem('userPoolId', environment.userPoolId); localStorage.setItem('appClientId', environment.appClientId); localStorage.setItem('apiGatewayUrl', environment.apiGatewayUrl); this.tenantNameRequired = false; } } ngOnInit(): void { this.tenantForm = this.fb.group({ tenantName: [null, [Validators.required]], }); } isFieldInvalid(field: string) { const formField = this.tenantForm.get(field); return ( formField && formField.invalid && (formField.dirty || formField.touched) ); } displayFieldCss(field: string) { return { 'is-invalid': this.isFieldInvalid(field), }; } hasRequiredError(field: string) { return !!this.tenantForm.get(field)?.hasError('required'); } openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } login() { if (!this.tenantNameRequired) { this.router.navigate(['/dashboard']); return true; } let tenantName = this.tenantForm.value.tenantName; if (!tenantName) { this.errorMessage = 'No tenant name provided.'; this.error = true; this.openErrorMessageSnackBar(this.errorMessage); return false; } this.authConfigService .setTenantConfig(tenantName) .then((val) => { this.router.navigate(['/dashboard']); }) .catch((errorResponse) => { this.error = true; this.errorMessage = errorResponse.error.message || 'An unexpected error occurred!'; this.openErrorMessageSnackBar(this.errorMessage); }); return false; } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/create/create.component.html ================================================
Create new order Enter order name Name is required
{{ op.product.name }}
{{ op.product.price | currency }}
{{ op.quantity || 0 }}
================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/create/create.component.scss ================================================ // .card { // margin: 20px; // display: flex; // flex-direction: column; // align-items: flex-start; // } .order-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { Product } from '../../products/models/product.interface'; import { ProductService } from '../../products/product.service'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; interface LineItem { product: Product; quantity?: number; } @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { displayedColumns = []; orderForm: FormGroup; orderProducts: LineItem[] = []; isLoadingProducts: boolean = true; error = ''; constructor( private fb: FormBuilder, private productSvc: ProductService, private orderSvc: OrdersService, private router: Router ) { this.orderForm = this.fb.group({ name: ['', Validators.required], }); } ngOnInit(): void { this.productSvc.fetch().subscribe((products) => { this.orderProducts = products.map((p) => ({ product: p })); this.isLoadingProducts = false; }); this.orderForm = this.fb.group({ orderName: ['', Validators.required], }); } get name() { return this.orderForm.get('name'); } get productQuantity() { return this.orderProducts .filter((p) => !!p.quantity).length > 0; } add(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity ? orderProduct.quantity + 1 : 1, }; } return p; }); } remove(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity && orderProduct.quantity > 1 ? orderProduct.quantity - 1 : undefined, }; } return p; }); } submit() { const val: Order = { ...this.orderForm?.value, orderProducts: this.orderProducts .filter((p) => !!p.quantity) .map((p) => ({ productId: p.product.productId, price: p.product.price, quantity: p.quantity, })), }; this.orderSvc.create(val).subscribe(() => this.router.navigate(['orders'])); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/detail/detail.component.html ================================================

Order Details mostly made up

NO: {{ orderId$ | async }} | Date: {{ today() | date }}
  • {{ tenantName() }}

Services / Products

Item / Details Unit Cost Sum Cost Discount Tax Total
{{ op.productId }}
{{ op.price | currency }}
Before Tax
{{ sum(op) | currency }}
{{ op.quantity }} Units
$0.00
None
{{ tax(op) | currency }}
Sales Tax 8.9%
{{ total(op) | currency }}
Sub Total Discount Total Tax Final
{{ subTotal(order) | currency }} -$0.00 {{ subTotal(order) | currency }} {{ calcTax(order) | currency }} {{ final(order) | currency }}
Comments / Notes
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Odit repudiandae numquam sit facere blanditiis, quasi distinctio ipsam? Libero odit ex expedita, facere sunt, possimus consectetur dolore, nobis iure amet vero.
================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/detail/detail.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .no-bottom-margin { margin-bottom: 0; } .nowrap { white-space: nowrap; } .invoice { font-family: Arial, Helvetica, sans-serif; width: 970px !important; margin: 50px auto; .invoice-header { padding: 25px 25px 15px; h1 { margin: 0; } .media { .media-body { font-size: 0.9em; margin: 0; } } } .invoice-body { border-radius: 10px; padding: 25px; background: #fff; } .invoice-footer { padding: 15px; font-size: 0.9em; text-align: center; color: #999; } } .logo { max-height: 70px; border-radius: 10px; } .dl-horizontal { margin: 0; dt { float: left; width: 80px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } dd { margin-left: 90px; } } .rowamount { padding-top: 15px !important; } .rowtotal { font-size: 1.3em; } .colfix { width: 12%; } .mono { font-family: monospace; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/detail/detail.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Order } from '../models/order.interface'; import { OrderProduct } from '../models/orderproduct.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-detail', templateUrl: './detail.component.html', styleUrls: ['./detail.component.scss'], }) export class DetailComponent implements OnInit { orderId$: Observable | undefined; order$: Observable | undefined; orderProducts$: Observable | undefined; taxRate = 0.0899; constructor(private route: ActivatedRoute, private orderSvc: OrdersService) {} ngOnInit(): void { this.orderId$ = this.route.params.pipe(map((o) => o['orderId'])); this.order$ = this.orderId$.pipe(switchMap((o) => this.orderSvc.get(o))); this.orderProducts$ = this.order$.pipe(map((o) => o.orderProducts)); } today() { return new Date(); } tenantName() { return ''; } sum(op: OrderProduct) { return op.price * op.quantity; } tax(op: OrderProduct) { return this.sum(op) * this.taxRate; } total(op: OrderProduct) { return this.sum(op) + this.tax(op); } subTotal(order: Order) { return order.orderProducts .map((op) => op.price * op.quantity) .reduce((acc, curr) => acc + curr); } calcTax(order: Order) { return this.subTotal(order) * this.taxRate; } final(order: Order) { return this.subTotal(order) + this.calcTax(order); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/list/list.component.html ================================================

Order List

Name {{ element.orderName }} Line Items {{ element.orderProducts?.length }} Total {{ sum(element) | currency }}
================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/list/list.component.scss ================================================ .order-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { displayedColumns: string[] = ['name', 'lineItems', 'total']; orderData: Order[] = []; isLoading: boolean = true; constructor(private orderSvc: OrdersService, private router: Router) {} ngOnInit(): void { this.orderSvc.fetch().subscribe((data) => { this.isLoading = false; this.orderData = data; }); } sum(order: Order): number { return order.orderProducts .map((p) => p.price * p.quantity) .reduce((acc, curr) => acc + curr); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/models/order.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { OrderProduct } from './orderproduct.interface'; export interface Order { key: string; shardId: string; orderId: string; orderName: string; orderProducts: OrderProduct[]; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/models/orderproduct.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface OrderProduct { productId: string; price: number; quantity: number; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/orders-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', data: { title: 'All Orders', }, component: ListComponent, }, { path: 'create', data: { title: 'Create Order', }, component: CreateComponent, }, { path: 'detail/:orderId', data: { title: 'View Order Detail', }, component: DetailComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class OrdersRoutingModule {} ================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/orders.module.ts ================================================ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; import { OrdersRoutingModule } from './orders-routing.module'; @NgModule({ declarations: [CreateComponent, ListComponent, DetailComponent], imports: [ CommonModule, OrdersRoutingModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class OrdersModule {} ================================================ FILE: Solution/Lab4/client/Application/src/app/views/orders/orders.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Order } from './models/order.interface'; @Injectable({ providedIn: 'root', }) export class OrdersService { orders: Order[] = []; baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; constructor(private http: HttpClient) {} fetch(): Observable { return this.http.get(`${this.baseUrl}/orders`); } get(orderId: string): Observable { const url = `${this.baseUrl}/order/${orderId}`; return this.http.get(url); } create(order: Order): Observable { return this.http.post(`${this.baseUrl}/order`, order); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/create/create.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required SKU Category {{category}}
================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ProductService } from '../product.service'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); } get name() { return this.productForm.get('name'); } get price() { return this.productForm.get('price'); } submit() { this.productSvc.post(this.productForm.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err.message); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/edit/edit.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required Description
================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/edit/edit.component.scss ================================================ ================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/edit/edit.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.scss'], }) export class EditComponent implements OnInit { productForm: FormGroup | undefined; product$: Observable | undefined; productId$: Observable | undefined; productName$: Observable | undefined; constructor( private route: ActivatedRoute, private router: Router, private productSvc: ProductService, private fb: FormBuilder ) {} ngOnInit(): void { this.productId$ = this.route.params.pipe(map((p) => p['productId'])); this.product$ = this.productId$.pipe( switchMap((p) => this.productSvc.get(p)) ); this.productName$ = this.product$.pipe(map((p) => p?.name)); this.productForm = this.fb.group({ productId: [''], name: [''], price: [''], description: [''], }); this.product$.subscribe((val) => { this.productForm?.patchValue({ ...val, }); }); } get name() { return this.productForm?.get('name'); } get price() { return this.productForm?.get('price'); } submit() { this.productSvc.put(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => console.error(err), }); } delete() { this.productSvc.delete(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/list/list.component.html ================================================

Product List

Name {{ element.name }} Price {{ element.price | currency }} SKU {{ element.sku }}
================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/list/list.component.scss ================================================ .product-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { productData: Product[] = []; isLoading: boolean = true; displayedColumns: string[] = ['name', 'price', 'sku']; constructor(private productSvc: ProductService, private router: Router) {} ngOnInit(): void { this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onEdit(product: Product) { this.router.navigate(['products', 'edit', product.productId]); return false; } onRemove(product: Product) { this.productSvc.delete(product); this.isLoading = true; this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onCreate() { this.router.navigate(['products', 'create']); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/models/product.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface Product { key: string; shardId: string; productId: string; name: string; price: number; sku: string; category: string; pictureUrl?: string; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/product.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Product } from './models/product.interface'; @Injectable({ providedIn: 'root', }) export class ProductService { constructor(private http: HttpClient) {} baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; fetch(): Observable { return this.http.get(`${this.baseUrl}/products`); } get(productId: string): Observable { const url = `${this.baseUrl}/product/${productId}`; return this.http.get(url); } delete(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.delete(url); } put(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.put(url, product); } post(product: Product) { return this.http.post(`${this.baseUrl}/product`, product); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/products-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'Product List', }, component: ListComponent, }, { path: 'create', data: { title: 'Create new Product', }, component: CreateComponent, }, { path: 'edit/:productId', data: { title: 'Edit Product', }, component: EditComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class ProductsRoutingModule {} ================================================ FILE: Solution/Lab4/client/Application/src/app/views/products/products.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ProductsRoutingModule } from './products-routing.module'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [CreateComponent, EditComponent, ListComponent], imports: [ CommonModule, ReactiveFormsModule, ProductsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class ProductsModule {} ================================================ FILE: Solution/Lab4/client/Application/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
================================================ FILE: Solution/Lab4/client/Application/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Solution/Lab4/client/Application/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Solution/Lab4/client/Application/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Solution/Lab4/client/Application/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Solution/Lab4/client/Application/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { find, mergeMap, defaultIfEmpty } from 'rxjs/operators'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.regApiGatewayUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } update(email: string, user: User) {} } ================================================ FILE: Solution/Lab4/client/Application/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab4/client/Application/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab4/client/Application/src/environments/environment.prod.ts ================================================ export const environment = { production: true, regApiGatewayUrl: 'https://3vby5hwma9.execute-api.us-west-2.amazonaws.com/prod/', apiGatewayUrl: 'https://hj4p6t6ob5.execute-api.us-west-2.amazonaws.com/prod/', userPoolId: 'us-west-2_HAYgKc4Ws', appClientId: '7e7hpl565vdrvkf4ief77bgnm', }; ================================================ FILE: Solution/Lab4/client/Application/src/environments/environment.ts ================================================ export const environment = { production: false, regApiGatewayUrl: 'https://k1lecgl9ye.execute-api.us-west-2.amazonaws.com/prod/', apiGatewayUrl: 'https://885cs6u6m8.execute-api.us-west-2.amazonaws.com/prod/', userPoolId: 'us-west-2_bCwYPJqrb', appClientId: '6lv6qhvmh6ivgd94qsftruc994', }; ================================================ FILE: Solution/Lab4/client/Application/src/index.html ================================================ Application ================================================ FILE: Solution/Lab4/client/Application/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab4/client/Application/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab4/client/Application/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab4/client/Application/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab4/client/Application/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "~bootstrap/scss/functions"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/maps"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; .chart-container canvas { max-height: 250px; width: auto; } .chart-container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; } ================================================ FILE: Solution/Lab4/client/Application/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab4/client/Application/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab4/client/Application/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "strictPropertyInitialization": false, "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab4/client/Application/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab4/client/Landing/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab4/client/Landing/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab4/client/Landing/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab4/client/Landing/README.md ================================================ # Landing This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab4/client/Landing/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "Landing": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/custom-theme.scss", "src/styles.scss", "node_modules/font-awesome/css/font-awesome.min.css" ] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "10kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "Landing:build:production" }, "development": { "browserTarget": "Landing:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "Landing:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab4/client/Landing/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab4/client/Landing/package.json ================================================ { "name": "landing", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "bootstrap": "~5.1.3", "font-awesome": "~4.7.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab4/client/Landing/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; export const routes: Routes = [ { path: '', redirectTo: 'landing', pathMatch: 'full', }, { path: 'landing', component: LandingComponent, }, { path: 'register', component: RegisterComponent, }, { path: '**', redirectTo: 'landing', pathMatch: 'full', }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab4/client/Landing/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab4/client/Landing/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'landing'; } ================================================ FILE: Solution/Lab4/client/Landing/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; @NgModule({ declarations: [AppComponent, LandingComponent, RegisterComponent], imports: [ AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, MatButtonModule, MatCardModule, MatIconModule, MatProgressSpinnerModule, MatSidenavModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, FormsModule, HttpClientModule, ReactiveFormsModule, MatSnackBarModule, MatInputModule, MatFormFieldModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab4/client/Landing/src/app/views/landing/landing.component.html ================================================

Serverless SaaS Reference Architecture

It's so nice it blows your mind.

Sign up now!

Serverless SaaS Reference Architecture is so awesome.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere temporibus omnis illum, officia. Architecto voluptatibus commodi voluptatem perspiciatis eos possimus, eius at molestias quaerat magnam? Odio qui quos ipsam natus.

Serverless SaaS Reference Architecture so awesome. Makes you awesome - go sign up!

Serverless SaaS Reference Architecture so great. Makes you even greater - go sign up now. Super cheap deal!

Feel lonely? Go sign up and have a friend!

Take Serverless SaaS Reference Architecture with you everywhere you go.

Serverless SaaS Reference Architecture is all you need. Anywhere - ever. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita sapiente hic voluptatum quo sunt totam accusamus distinctio minus aliquid quis!

Love Serverless SaaS Reference Architecture. So nice! So good! Could not live without!

Satisfied Customer

Reasons to sign up this product:

  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!
  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!

Why you still reading?

Sign up now!
================================================ FILE: Solution/Lab4/client/Landing/src/app/views/landing/landing.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // $color-red: #cc615f; $color-grey: #4b4b4b; $color-grey--light: #d3d3d3; $btn-amzn: #ec7211; $color-primary: #232f3e; $bp-s: 65.75em; //700px; $bp-xs: 34.375em; //550px; @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,800,300); *{ box-sizing: border-box; } html{ width: 100%; height:100%; margin: 0; padding: 0; } body{ width: 100%; height:100%; font-family: 'Open Sans','Helvetica Neue',Helvetica, sans-serif; font-size: 100%; line-height: 1.45; color: #141414; } a{ text-decoration: none; &:hover{ text-decoration: none; } } img{ max-width: 100%; } .btn{ display: inline-block; margin: 1rem 0; color: white; font-weight: 500; font-size: 1.3rem; background: $btn-amzn; letter-spacing: .02em; border: none; border-radius: 5px; padding: .8rem 1rem .9rem; text-shadow: 0 1px rgba(black,.3); box-shadow: 0 0 2px rgba(black,.2); @media (max-width: $bp-s){ padding: .5rem .7rem .6rem; font-size: 1rem; } &:hover{ background: lighten($btn-amzn,5%); color: #fff; } &:focus, &:active, &:focus:active{ background: darken($btn-amzn,5%); border-color: darken($btn-amzn,5%); box-shadow: 0 2px 5px 0 rgba(black,.5) inset; } } .container{ margin: 0 auto; width: 90%; max-width: 900px; } header{ color: white; background: #232f3e; padding: 10rem 0; text-align: center; position: relative; z-index: 1; overflow: hidden; @media (max-width: $bp-s){ padding: 2rem 0; } h1{ font-size: 3rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 2rem; } } h2{ font-weight: 300; font-size: 1.5rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } } section{ background: #fff; color: $color-grey; padding: 3.5rem 0; @media (max-width: $bp-s){ padding: 2rem 0; } &.section--primary{ background: $color-primary; color: #fff; } &.section--primary--alt{ background: desaturate(lighten($color-primary,15%),10%); color: #fff; } &.section--primary--light{ background: rgba($color-primary,.1); } &.section--grey{ background: $color-grey; color: #fff; } &.section--grey--light{ background: $color-grey--light; color: #fff; } h3{ text-align: center; font-size: 2rem; font-weight: 300; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } li{ font-size: 1.2rem; font-weight: 300; } p{ font-size: 1.2rem; font-weight: 300; } } .col{ margin: 0 1.5%; display: inline-block; vertical-align: top; } .col-7{ @extend .col; width: 64%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-3{ @extend .col; width: 29%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-5{ @extend .col; width: 30%; @media (max-width: $bp-xs){ width: 60%; margin: 0; } } .details{ text-align: left; h3{ font-size: 2rem; text-align: left; } } .features{ text-align: center; padding: 1rem; &:hover{ background: rgba(white,.1); } @media (max-width: $bp-s){ width: 100%; margin: 0; text-align: left; border-bottom: 1px solid rgba(white,.2); &:last-child{ border: none; } } i{ font-size: 4rem; margin: 0 0 2rem 0; @media (max-width: $bp-s){ font-size: 1.5rem; width: 2rem; text-align: center; margin: 0 0 1rem 0; float: left; } } p{ margin: 0 0 1rem 0; font-size: 1rem; @media (max-width: $bp-s){ margin-left: 3rem; } } } blockquote{ position: relative; margin: 0; padding: 0; text-align: center; &:before{ display: inline-block; color: $color-primary; font-size: 2rem; content: '\201C'; } p{ margin: 0; display: inline; font-size: 1.5rem; @media (max-width: $bp-s){ font-size: 1.2rem; } } cite{ font-size: 1rem; display: block; margin: .5rem 0 0 1.2rem; @media (max-width: $bp-s){ font-size: .8rem; } &:before{ content: '–'; } } } footer{ background: $color-primary; color: #fff; padding: 2rem 0; text-align: center; font-size: .8rem; color: rgba(white,.4); ul{ margin: 0; padding: 0; list-style: none; li{ display: inline-block; a{ display: block; padding: .4rem .7rem; font-size: .9rem; text-decoration: none; color: rgba(white,.7); &:hover{ color: white; } } } } } .text--center{ text-align: center; } .text--left{ text-align: left; } .bg-image{ background: $color-primary; text-align: center; position: relative; z-index: 1; overflow: hidden; //text-shadow: 2px 0 5px black; &:before{ content: ''; display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; z-index: -1; background-size: 100%; background-attachment: fixed; filter: blur(3px); opacity: .8; transform: scale(1.1); } &.bg-image-2:before{ opacity: .6; background-position: center center; } } ================================================ FILE: Solution/Lab4/client/Landing/src/app/views/landing/landing.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-landing', templateUrl: './landing.component.html', styleUrls: ['./landing.component.scss'], }) export class LandingComponent implements OnInit { constructor(private router: Router) {} ngOnInit() {} register() { this.router.navigate(['register']); return false; } } ================================================ FILE: Solution/Lab4/client/Landing/src/app/views/register/register.component.html ================================================
Provision a new Tenant Login details will be sent to the email provided. Name Email Phone Address Plan
================================================ FILE: Solution/Lab4/client/Landing/src/app/views/register/register.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .tenant-form { display: flex; align-items: center; justify-content: center; } .wide-form { min-width: 150px; max-width: 500px; width: 100%; } .full-width { width: 100%; } .mat-form-field { width: 100%; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab4/client/Landing/src/app/views/register/register.component.ts ================================================ import { MatSnackBar } from '@angular/material/snack-bar'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.scss'], }) export class RegisterComponent implements OnInit { submitting: boolean = false; tenantForm: FormGroup = this.fb.group({ tenantName: ['', [Validators.required]], tenantEmail: ['', [Validators.email, Validators.required]], tenantTier: ['', [Validators.required]], tenantPhone: [''], tenantAddress: [''], }); constructor( private fb: FormBuilder, private _snackBar: MatSnackBar, private http: HttpClient ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; this.tenantForm.disable(); const tenant = { ...this.tenantForm.value, }; this.http .post(`${environment.apiGatewayUrl}/registration`, tenant) .subscribe({ next: () => { this.openErrorMessageSnackBar('Successfully created new tenant!'); this.tenantForm.reset(); this.tenantForm.enable(); this.submitting = false; }, error: (err) => { this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); this.tenantForm.enable(); this.submitting = false; }, }); } } ================================================ FILE: Solution/Lab4/client/Landing/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab4/client/Landing/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab4/client/Landing/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab4/client/Landing/src/environments/environment.ts ================================================ export const environment = { production: false, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab4/client/Landing/src/index.html ================================================ Landing ================================================ FILE: Solution/Lab4/client/Landing/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab4/client/Landing/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab4/client/Landing/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab4/client/Landing/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab4/client/Landing/src/styles.scss ================================================ ================================================ FILE: Solution/Lab4/client/Landing/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab4/client/Landing/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab4/client/Landing/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab4/client/Landing/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab4/client/dummy.txt ================================================ ================================================ FILE: Solution/Lab4/scripts/deployment.sh ================================================ #!/bin/bash if [[ "$#" -eq 0 ]]; then echo "Invalid parameters" echo "Command to deploy client code: deployment.sh -c" echo "Command to deploy bootstrap server code: deployment.sh -b" echo "Command to deploy tenant server code: deployment.sh -t" echo "Command to deploy bootstrap & tenant server code: deployment.sh -s" echo "Command to deploy server & client code: deployment.sh -s -c" exit 1 fi while [[ "$#" -gt 0 ]]; do case $1 in -s) server=1 ;; -b) bootstrap=1 ;; -t) tenant=1 ;; -c) client=1 ;; *) echo "Unknown parameter passed: $1"; exit 1 ;; esac shift done # During AWS hosted events using event engine tool # we pre-provision cloudfront and s3 buckets which hosts UI code. # So that it improves this labs total execution time. # Below code checks if cloudfront and s3 buckets are # pre-provisioned or not and then concludes if the workshop # is running in AWS hosted event through event engine tool or not. IS_RUNNING_IN_EVENT_ENGINE=false PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" IS_RUNNING_IN_EVENT_ENGINE=true ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) fi if [[ $server -eq 1 ]] || [[ $bootstrap -eq 1 ]] || [[ $tenant -eq 1 ]]; then echo "Validating server code using pylint" cd ../server python3 -m pylint -E -d E0401,E1111 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi cd ../scripts fi if [[ $server -eq 1 ]] || [[ $bootstrap -eq 1 ]]; then echo "Bootstrap server code is getting deployed" cd ../server REGION=$(aws configure get region) sam build -t shared-template.yaml --use-container if [ "$IS_RUNNING_IN_EVENT_ENGINE" = true ]; then sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE AdminUserPoolCallbackURLParameter=$ADMIN_SITE_URL TenantUserPoolCallbackURLParameter=$APP_SITE_URL else sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE fi cd ../scripts fi if [[ $server -eq 1 ]] || [[ $tenant -eq 1 ]]; then echo "Tenant server code is getting deployed" cd ../server REGION=$(aws configure get region) sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml --region=$REGION cd ../scripts fi if [[ $client -eq 1 ]]; then # Admin UI and Landing UI are configured in Lab2 # App UI is configured in Lab3 echo "Admin UI and Landing UI are configured in Lab2. App UI is configured in Lab3. So, no UI code is built in this Lab4" if [ "$IS_RUNNING_IN_EVENT_ENGINE" = false ]; then ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" fi ================================================ FILE: Solution/Lab4/scripts/geturl.sh ================================================ PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) else ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Solution/Lab4/server/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Solution/Lab4/server/OrderService/order_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Order: key='' def __init__(self, shardId, orderId, orderName, orderProducts): self.shardId = shardId self.orderId = orderId self.key = shardId + ':' + orderId self.orderName = orderName self.orderProducts = orderProducts class OrderProduct: def __init__(self, productId, price, quantity): self.productId = productId self.price = price self.quantity = quantity ================================================ FILE: Solution/Lab4/server/OrderService/order_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import order_service_dal from decimal import Decimal from types import SimpleNamespace from aws_lambda_powertools import Tracer tracer = Tracer() @tracer.capture_lambda_handler def get_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a order") params = event['pathParameters'] key = params['id'] logger.log_with_tenant_context(event, params) order = order_service_dal.get_order(event, key) logger.log_with_tenant_context(event, "Request completed to get a order") metrics_manager.record_metric(event, "SingleOrderRequested", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def create_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) order = order_service_dal.create_order(event, payload) logger.log_with_tenant_context(event, "Request completed to create a order") metrics_manager.record_metric(event, "OrderCreated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def update_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] order = order_service_dal.update_order(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a order") metrics_manager.record_metric(event, "OrderUpdated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def delete_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a order") params = event['pathParameters'] key = params['id'] response = order_service_dal.delete_order(event, key) logger.log_with_tenant_context(event, "Request completed to delete a order") metrics_manager.record_metric(event, "OrderDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the order") @tracer.capture_lambda_handler def get_orders(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all orders") response = order_service_dal.get_orders(event, tenantId) metrics_manager.record_metric(event, "OrdersRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all orders") return utils.generate_response(response) ================================================ FILE: Solution/Lab4/server/OrderService/order_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid from order_models import Order import json import utils from types import SimpleNamespace import logger import random import threading from boto3.dynamodb.conditions import Key import metrics_manager table_name = os.environ['ORDER_TABLE_NAME'] dynamodb = None suffix_start = 1 suffix_end = 10 def get_order(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) response = table.get_item(Key={'shardId': shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a order', e) else: return order def delete_order(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a order', e) else: logger.info("DeleteItem succeeded:") return response def create_order(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] table = __get_dynamodb_table(event, dynamodb) suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) order = Order(shardId, str(uuid.uuid4()), payload.orderName, payload.orderProducts) try: response = table.put_item(Item={ 'shardId':shardId, 'orderId': order.orderId, 'orderName': order.orderName, 'orderProducts': get_order_products_dict(order.orderProducts) }, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a order', e) else: logger.info("PutItem succeeded:") return order def update_order(event, payload, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) order = Order(shardId, orderId,payload.orderName, payload.orderProducts) response = table.update_item(Key={'shardId':order.shardId, 'orderId': order.orderId}, UpdateExpression="set orderName=:orderName, " +"orderProducts=:orderProducts", ExpressionAttributeValues={ ':orderName': order.orderName, ':orderProducts': get_order_products_dict(order.orderProducts) }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a order', e) else: logger.info("UpdateItem succeeded:") return order def get_orders(event, tenantId): table = __get_dynamodb_table(event, dynamodb) get_all_products_response = [] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error("Error getting all orders") raise Exception('Error getting all orders', e) else: logger.info("Get orders succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) get_all_products_response.append(order) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def __get_dynamodb_table(event, dynamodb): """ Args: event ([type]): [description] Returns: [type]: [description] """ accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken ) return dynamodb.Table(table_name) def get_order_products_dict(orderProducts): orderProductList = [] for i in range(len(orderProducts)): product = orderProducts[i] orderProductList.append(vars(product)) return orderProductList ================================================ FILE: Solution/Lab4/server/OrderService/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab4/server/ProductService/product_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Product: key ='' def __init__(self, shardId, productId, sku, name, price, category): self.shardId = shardId self.productId = productId self.key = shardId + ':' + productId self.sku = sku self.name = name self.price = price self.category = category class Category: def __init__(self, id, name): self.id = id self.name = name ================================================ FILE: Solution/Lab4/server/ProductService/product_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import product_service_dal from aws_lambda_powertools import Tracer from decimal import Decimal from types import SimpleNamespace tracer = Tracer() @tracer.capture_lambda_handler def get_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a product") params = event['pathParameters'] logger.log_with_tenant_context(event, params) key = params['id'] logger.log_with_tenant_context(event, key) product = product_service_dal.get_product(event, key) logger.log_with_tenant_context(event, "Request completed to get a product") metrics_manager.record_metric(event, "SingleProductRequested", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def create_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) product = product_service_dal.create_product(event, payload) logger.log_with_tenant_context(event, "Request completed to create a product") metrics_manager.record_metric(event, "ProductCreated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def update_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] product = product_service_dal.update_product(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a product") metrics_manager.record_metric(event, "ProductUpdated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def delete_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a product") params = event['pathParameters'] key = params['id'] response = product_service_dal.delete_product(event, key) logger.log_with_tenant_context(event, "Request completed to delete a product") metrics_manager.record_metric(event, "ProductDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the product") @tracer.capture_lambda_handler def get_products(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all products") response = product_service_dal.get_products(event, tenantId) metrics_manager.record_metric(event, "ProductsRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all products") return utils.generate_response(response) ================================================ FILE: Solution/Lab4/server/ProductService/product_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid import json import logger import random import threading import metrics_manager from product_models import Product from types import SimpleNamespace from boto3.dynamodb.conditions import Key table_name = os.environ['PRODUCT_TABLE_NAME'] dynamodb = None suffix_start = 1 suffix_end = 10 def get_product(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) response = table.get_item(Key={'shardId': shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a product', e) else: logger.info("GetItem succeeded:"+ str(product)) return product def delete_product(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a product', e) else: logger.info("DeleteItem succeeded:") return response def create_product(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] table = __get_dynamodb_table(event, dynamodb) suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) product = Product(shardId, str(uuid.uuid4()), payload.sku,payload.name, payload.price, payload.category) try: response = table.put_item( Item= { 'shardId': shardId, 'productId': product.productId, 'sku': product.sku, 'name': product.name, 'price': product.price, 'category': product.category }, ReturnConsumedCapacity='TOTAL' ) metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: logger.info("PutItem succeeded:") return product def update_product(event, payload, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) product = Product(shardId,productId,payload.sku, payload.name, payload.price, payload.category) response = table.update_item(Key={'shardId':product.shardId, 'productId': product.productId}, UpdateExpression="set sku=:sku, #n=:productName, price=:price, category=:category", ExpressionAttributeNames= {'#n':'name'}, ExpressionAttributeValues={ ':sku': product.sku, ':productName': product.name, ':price': product.price, ':category': product.category }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a product', e) else: logger.info("UpdateItem succeeded:") return product def get_products(event, tenantId): table = __get_dynamodb_table(event, dynamodb) get_all_products_response =[] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting all products', e) else: logger.info("Get products succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) get_all_products_response.append(product) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def __get_dynamodb_table(event, dynamodb): """ Args: event ([type]): [description] Returns: [type]: [description] """ accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken ) return dynamodb.Table(table_name) ================================================ FILE: Solution/Lab4/server/ProductService/requirements.txt ================================================ requests pytest-mock aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab4/server/README.md ================================================ sam build -t shared-template.yaml --use-container sam deploy --config-file shared-samconfig.toml sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml ================================================ FILE: Solution/Lab4/server/Resources/requirements.txt ================================================ python-jose[cryptography] ================================================ FILE: Solution/Lab4/server/Resources/shared_service_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import utils import auth_manager region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] tenant_userpool_id = os.environ['TENANT_USER_POOL'] tenant_appclient_id = os.environ['TENANT_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) if(auth_manager.isSaaSProvider(unauthorized_claims['custom:userRole'])): userpool_id = user_pool_operation_user appclient_id = app_client_operation_user else: #get tenant user pool and app client to validate jwt token against userpool_id = tenant_userpool_id appclient_id = tenant_appclient_id #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] #only tenant admin and system admin can do certain actions like create and disable users if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): policy.allowAllMethods() if (auth_manager.isTenantAdmin(user_role)): policy.denyMethod(HttpVerb.POST, "tenant-activation") policy.denyMethod(HttpVerb.GET, "tenants") else: #if not tenant admin or system admin then only allow to get info and update info policy.allowMethod(HttpVerb.GET, "user/*") policy.allowMethod(HttpVerb.PUT, "user/*") authResponse = policy.build() # Generate STS credentials to be used for FGAC # Important Note: # We are generating STS token inside Authorizer to take advantage of the caching behavior of authorizer # Another option is to generate the STS token inside the lambda function itself, as mentioned in this blog post: https://aws.amazon.com/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/ # Finally, you can also consider creating one Authorizer per microservice in cases where you want the IAM policy specific to that service iam_policy = auth_manager.getPolicyForUser(user_role, utils.Service_Identifier.SHARED_SERVICES.value, tenant_id, region, aws_account_id) logger.info(iam_policy) role_arn = "arn:aws:iam::{}:role/authorizer-access-role".format(aws_account_id) assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName="tenant-aware-session", Policy=iam_policy, ) credentials = assumed_role["Credentials"] #pass sts credentials to lambda context = { 'accesskey': credentials['AccessKeyId'], 'secretkey' : credentials['SecretAccessKey'], 'sessiontoken' : credentials["SessionToken"], 'userName': user_name, 'userPoolId': userpool_id, 'tenantId': tenant_id, 'userRole': user_role } authResponse['context'] = context return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Solution/Lab4/server/Resources/tenant_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import auth_manager import utils region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) userpool_id = os.environ['TENANT_USER_POOL'] appclient_id = os.environ['TENANT_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] #roles are not fine-grained enough to allow selectively policy.allowAllMethods() authResponse = policy.build() # Generate STS credentials to be used for FGAC # Important Note: # We are generating STS token inside Authorizer to take advantage of the caching behavior of authorizer # Another option is to generate the STS token inside the lambda function itself, as mentioned in this blog post: https://aws.amazon.com/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/ # Finally, you can also consider creating one Authorizer per microservice in cases where you want the IAM policy specific to that service iam_policy = auth_manager.getPolicyForUser(user_role, utils.Service_Identifier.BUSINESS_SERVICES.value, tenant_id, region, aws_account_id) logger.info(iam_policy) role_arn = "arn:aws:iam::{}:role/authorizer-access-role".format(aws_account_id) assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName="tenant-aware-session", Policy=iam_policy, ) credentials = assumed_role["Credentials"] #pass sts credentials to lambda context = { 'accesskey': credentials['AccessKeyId'], # $context.authorizer.key -> value 'secretkey' : credentials['SecretAccessKey'], 'sessiontoken' : credentials["SessionToken"], 'userName': user_name, 'tenantId': tenant_id, 'userPoolId': userpool_id, 'userRole': user_role } authResponse['context'] = context return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Solution/Lab4/server/TenantManagementService/requirements.txt ================================================ requests aws_requests_auth ================================================ FILE: Solution/Lab4/server/TenantManagementService/tenant-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import json import boto3 from boto3.dynamodb.conditions import Key import urllib.parse import utils from botocore.exceptions import ClientError import logger import requests import metrics_manager import auth_manager from aws_lambda_powertools import Tracer tracer = Tracer() region = os.environ['AWS_REGION'] dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') #This method has been locked down to be only called from tenant registration service def create_tenant(event, context): tenant_details = json.loads(event['body']) try: response = table_tenant_details.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'tenantName' : tenant_details['tenantName'], 'tenantAddress': tenant_details['tenantAddress'], 'tenantEmail': tenant_details['tenantEmail'], 'tenantPhone': tenant_details['tenantPhone'], 'tenantTier': tenant_details['tenantTier'], 'isActive': True } ) except Exception as e: raise Exception('Error creating a new tenant', e) else: return utils.create_success_response("Tenant Created") def get_tenants(event, context): try: response = table_tenant_details.scan() except Exception as e: raise Exception('Error getting all tenants', e) else: return utils.generate_response(response['Items']) @tracer.capture_lambda_handler def update_tenant(event, context): requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_details = json.loads(event['body']) tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response_update = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set tenantName = :tenantName, tenantAddress = :tenantAddress, tenantEmail = :tenantEmail, tenantPhone = :tenantPhone, tenantTier=:tenantTier", ExpressionAttributeValues={ ':tenantName' : tenant_details['tenantName'], ':tenantAddress': tenant_details['tenantAddress'], ':tenantEmail': tenant_details['tenantEmail'], ':tenantPhone': tenant_details['tenantPhone'], ':tenantTier': tenant_details['tenantTier'] }, ReturnValues="UPDATED_NEW" ) logger.log_with_tenant_context(event, response_update) logger.log_with_tenant_context(event, "Request completed to update tenant") return utils.create_success_response("Tenant Updated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_tenant(event, context): requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get tenant details") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): tenant_details = table_tenant_details.get_item( Key={ 'tenantId': tenant_id, }, AttributesToGet=[ 'tenantName', 'tenantAddress', 'tenantEmail', 'tenantPhone' ] ) item = tenant_details['Item'] tenant_info = TenantInfo(item['tenantName'], item['tenantAddress'],item['tenantEmail'], item['tenantPhone']) logger.log_with_tenant_context(event, tenant_info) logger.log_with_tenant_context(event, "Request completed to get tenant details") return utils.create_success_response(tenant_info.__dict__) else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def deactivate_tenant(event, context): url_disable_users = os.environ['DISABLE_USERS_BY_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to deactivate tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': False }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) update_details = {} update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_disable_users(update_details, headers, auth, host, stage_name, url_disable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to deactivate tenant") return utils.create_success_response("Tenant Deactivated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def activate_tenant(event, context): url_enable_users = os.environ['ENABLE_USERS_BY_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to activate tenant") if (auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': True }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) update_details = {} update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_enable_users(update_details, headers, auth, host, stage_name, url_enable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to activate tenant") return utils.create_success_response("Tenant Activated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only system admin can activate tenant!") return utils.create_unauthorized_response() def __invoke_disable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url, '/']) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while disabling users for the tenant') except Exception as e: logger.error('Error occured while disabling users for the tenant') raise Exception('Error occured while disabling users for the tenant', e) else: return "Success invoking disable users" def __invoke_enable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url, '/']) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while enabling users for the tenant') except Exception as e: logger.error('Error occured while enabling users for the tenant') raise Exception('Error occured while enabling users for the tenant', e) else: return "Success invoking enable users" class TenantInfo: def __init__(self, tenant_name, tenant_address, tenant_email, tenant_phone): self.tenant_name = tenant_name self.tenant_address = tenant_address self.tenant_email = tenant_email self.tenant_phone = tenant_phone ================================================ FILE: Solution/Lab4/server/TenantManagementService/tenant-registration.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import utils import uuid import logger import requests import re region = os.environ['AWS_REGION'] create_tenant_admin_user_resource_path = os.environ['CREATE_TENANT_ADMIN_USER_RESOURCE_PATH'] create_tenant_resource_path = os.environ['CREATE_TENANT_RESOURCE_PATH'] lambda_client = boto3.client('lambda') def register_tenant(event, context): try: tenant_id = uuid.uuid1().hex tenant_details = json.loads(event['body']) tenant_details['tenantId'] = tenant_id logger.info(tenant_details) stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) create_user_response = __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name) logger.info (create_user_response) tenant_details['tenantAdminUserName'] = create_user_response['message']['tenantAdminUserName'] create_tenant_response = __create_tenant(tenant_details, headers, auth, host, stage_name) logger.info (create_tenant_response) except Exception as e: logger.error('Error registering a new tenant') raise Exception('Error registering a new tenant', e) else: return utils.create_success_response("You have been registered in our system") def __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_admin_user_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while calling the create tenant admin user service') raise Exception('Error occured while calling the create tenant admin user service', e) else: return response_json def __create_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_resource_path]) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while creating the tenant record in table') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json ================================================ FILE: Solution/Lab4/server/TenantManagementService/user-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import sys import logger import metrics_manager import auth_manager import utils from boto3.dynamodb.conditions import Key from aws_lambda_powertools import Tracer tracer = Tracer() client = boto3.client('cognito-idp') dynamodb = boto3.resource('dynamodb') table_tenant_user_map = dynamodb.Table('ServerlessSaaS-TenantUserMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_id = os.environ['TENANT_USER_POOL_ID'] def create_tenant_admin_user(event, context): app_client_id = os.environ['TENANT_APP_CLIENT_ID'] tenant_details = json.loads(event['body']) tenant_id = tenant_details['tenantId'] logger.info(tenant_details) user_mgmt = UserManagement() #Add tenant admin now based upon user pool tenant_user_group_response = user_mgmt.create_user_group(user_pool_id,tenant_id,"User group for tenant {0}".format(tenant_id)) tenant_admin_user_name = 'tenant-admin-{0}'.format(tenant_details['tenantId']) create_tenant_admin_response = user_mgmt.create_tenant_admin(user_pool_id, tenant_admin_user_name, tenant_details) add_tenant_admin_to_group_response = user_mgmt.add_user_to_group(user_pool_id, tenant_admin_user_name, tenant_user_group_response['Group']['GroupName']) tenant_user_mapping_response = user_mgmt.create_user_tenant_mapping(tenant_admin_user_name,tenant_id) response = {"userPoolId": user_pool_id, "appClientId": app_client_id, "tenantAdminUserName": tenant_admin_user_name} return utils.create_success_response(response) @tracer.capture_lambda_handler #only tenant admin can create users def create_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to create new user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = user_details['tenantId'] else: user_tenant_id = tenant_id if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): metrics_manager.record_metric(event, "UserCreated", "Count", 1) response = client.admin_create_user( Username=user_details['userName'], UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] }, { 'Name': 'custom:tenantId', 'Value': user_tenant_id } ] ) logger.log_with_tenant_context(event, response) user_mgmt = UserManagement() user_mgmt.add_user_to_group(user_pool_id, user_details['userName'], user_tenant_id) response_mapping = user_mgmt.create_user_tenant_mapping(user_details['userName'], user_tenant_id) logger.log_with_tenant_context(event, "Request completed to create new user") return utils.create_success_response("New user created") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can create user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_users(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] users = [] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get users") if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): response = client.list_users( UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) num_of_users = len(response['Users']) metrics_manager.record_metric(event, "Number of users", "Count", num_of_users) if (num_of_users > 0): for user in response['Users']: is_same_tenant_user = False user_info = UserInfo() for attr in user["Attributes"]: if(attr["Name"] == "custom:tenantId" and attr["Value"] == tenant_id): is_same_tenant_user = True user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] if(is_same_tenant_user): user_info.enabled = user["Enabled"] user_info.created = user["UserCreateDate"] user_info.modified = user["UserLastModifiedDate"] user_info.status = user["UserStatus"] user_info.user_name = user["Username"] users.append(user_info) return utils.generate_response(users) else: logger.log_with_tenant_context(event, "Request completed as unauthorized.") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get user") if (auth_manager.isTenantUser(user_role) and user_name != requesting_user_name): logger.log_with_tenant_context(event, "Request completed as unauthorized. User can only get its information.") return utils.create_unauthorized_response() else: user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: logger.log_with_tenant_context(event, "Request completed to get new user") return utils.create_success_response(user_info.__dict__) @tracer.capture_lambda_handler def update_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update user") if (auth_manager.isTenantUser(user_role)): logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update user!") return utils.create_unauthorized_response() else: user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserUpdated", "Count", 1) response = client.admin_update_user_attributes( Username=user_name, UserPoolId=user_pool_id, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] } ] ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to update user") return utils.create_success_response("user updated") @tracer.capture_lambda_handler def disable_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to disable new user") if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserDisabled", "Count", 1) response = client.admin_disable_user( Username=user_name, UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to disable new user") return utils.create_success_response("User disabled") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can disable user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def disable_users_by_tenant(event, context): logger.info("Request received to disable users by tenant") tenantid_to_update = event['tenantId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if ((auth_manager.isTenantAdmin(user_role) and tenantid_to_update == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_disable_user( Username=user['userName'], UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to disable users") return utils.create_success_response("Users disabled") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def enable_users_by_tenant(event, context): logger.info("Request received to enable users by tenant") tenantid_to_update = event['tenantId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if (auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_enable_user( Username=user['userName'], UserPoolId=user_pool_id ) logger.info(response) logger.info("Request completed to enable users") return utils.create_success_response("Users enables") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() def get_user_info(event, user_pool_id, user_name): metrics_manager.record_metric(event, "UserInfoRequested", "Count", 1) response = client.admin_get_user( UserPoolId=user_pool_id, Username=user_name ) logger.log_with_tenant_context(event, response) user_info = UserInfo() user_info.user_name = response["Username"] for attr in response["UserAttributes"]: if(attr["Name"] == "custom:tenantId"): user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] logger.log_with_tenant_context(event, user_info) return user_info class UserManagement: def create_user_group(self, user_pool_id, group_name, group_description): response = client.create_group( GroupName=group_name, UserPoolId=user_pool_id, Description= group_description, Precedence=0 ) return response def create_tenant_admin(self, user_pool_id, tenant_admin_user_name, user_details): response = client.admin_create_user( Username=tenant_admin_user_name, UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['tenantEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': 'TenantAdmin' }, { 'Name': 'custom:tenantId', 'Value': user_details['tenantId'] } ] ) return response def add_user_to_group(self, user_pool_id, user_name, group_name): response = client.admin_add_user_to_group( UserPoolId=user_pool_id, Username=user_name, GroupName=group_name ) return response def create_user_tenant_mapping(self, user_name, tenant_id): response = table_tenant_user_map.put_item( Item={ 'tenantId': tenant_id, 'userName': user_name } ) return response class UserInfo: def __init__(self, user_name=None, tenant_id=None, user_role=None, email=None, status=None, enabled=None, created=None, modified=None): self.user_name = user_name self.tenant_id = tenant_id self.user_role = user_role self.email = email self.status = status self.enabled = enabled self.created = created self.modified = modified ================================================ FILE: Solution/Lab4/server/layers/auth_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils # These are the roles being supported in this reference architecture class UserRoles: SYSTEM_ADMIN = "SystemAdmin" CUSTOMER_SUPPORT = "CustomerSupport" TENANT_ADMIN = "TenantAdmin" TENANT_USER = "TenantUser" def isTenantAdmin(user_role): if (user_role == UserRoles.TENANT_ADMIN): return True else: return False def isSystemAdmin(user_role): if (user_role == UserRoles.SYSTEM_ADMIN): return True else: return False def isSaaSProvider(user_role): if (user_role == UserRoles.SYSTEM_ADMIN or user_role == UserRoles.CUSTOMER_SUPPORT): return True else: return False def isTenantUser(user_role): if (user_role == UserRoles.TENANT_USER): return True else: return False def getPolicyForUser(user_role, service_identifier, tenant_id, region, aws_account_id): """ This method is being used by Authorizer to get appropriate policy by user role Args: user_role (string): UserRoles enum tenant_id (string): region (string): aws_account_id (string): Returns: string: policy that tenant needs to assume """ iam_policy = "" if (isSystemAdmin(user_role)): iam_policy = __getPolicyForSystemAdmin(region, aws_account_id) elif (isTenantAdmin(user_role)): iam_policy = __getPolicyForTenantAdmin(tenant_id, service_identifier, region, aws_account_id) elif (isTenantUser(user_role)): iam_policy = __getPolicyForTenantUser(tenant_id, region, aws_account_id) return iam_policy def __getPolicyForSystemAdmin(region, aws_account_id): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/*".format(region, aws_account_id), ] } ] } return json.dumps(policy) def __getPolicyForTenantAdmin(tenant_id, sevice_identifier, region, aws_account_id): if (sevice_identifier == utils.Service_Identifier.SHARED_SERVICES.value): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantUserMapping".format(region, aws_account_id), "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantDetails".format(region, aws_account_id) ], "Condition": { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [ "{0}".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantStackMapping".format(region, aws_account_id), "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-Settings".format(region, aws_account_id) ] } ] } else: policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Product-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Order-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } } ] } return json.dumps(policy) def __getPolicyForTenantUser(tenant_id, region, aws_account_id): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Product-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Order-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } } ] } return json.dumps(policy) ================================================ FILE: Solution/Lab4/server/layers/logger.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from aws_lambda_powertools import Logger logger = Logger() """Log info messages """ def info(log_message): logger.info (log_message) """Log error messages """ def error(log_message): logger.error (log_message) def log_with_tenant_context(event, log_message): logger.structure_logs(append=True, tenant_id= event['requestContext']['authorizer']['tenantId']) logger.info (log_message) ================================================ FILE: Solution/Lab4/server/layers/metrics_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json from aws_lambda_powertools import Metrics metrics = Metrics() def record_metric(event, metric_name, metric_unit, metric_value): """ Record the metric in Cloudwatch using EMF format Args: event ([type]): [description] metric_name ([type]): [description] metric_unit ([type]): [description] metric_value ([type]): [description] """ metrics.add_dimension(name="tenant_id", value=event['requestContext']['authorizer']['tenantId']) metrics.add_metric(name=metric_name, unit=metric_unit, value=metric_value) metrics_object = metrics.serialize_metric_set() metrics.clear_metrics() print(json.dumps(metrics_object)) ================================================ FILE: Solution/Lab4/server/layers/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] simplejson jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab4/server/layers/utils.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import jsonpickle import simplejson import boto3 from aws_requests_auth.aws_auth import AWSRequestsAuth from enum import Enum class StatusCodes(Enum): SUCCESS = 200 UN_AUTHORIZED = 401 NOT_FOUND = 404 class Service_Identifier(Enum): SHARED_SERVICES = "SharedServices" BUSINESS_SERVICES = "BusinessServices" def create_success_response(message): return { "statusCode": StatusCodes.SUCCESS.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def create_unauthorized_response(): return { "statusCode": StatusCodes.UN_AUTHORIZED.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": "User not authorized to perform this action" }), } def generate_response(inputObject): return { "statusCode": 200, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": encode_to_json_object(inputObject), } def encode_to_json_object(inputObject): jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True) jsonpickle.set_preferred_backend('simplejson') return jsonpickle.encode(inputObject, unpicklable=False, use_decimal=True) def get_auth(host, region): session = boto3.Session() credentials = session.get_credentials() auth = AWSRequestsAuth(aws_access_key=credentials.access_key, aws_secret_access_key=credentials.secret_key, aws_token=credentials.token, aws_host=host, aws_region=region, aws_service='execute-api') return auth def get_headers(event): return event['headers'] ================================================ FILE: Solution/Lab4/server/nested_templates/apigateway.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway, apis, api keys and usage plan as part of bootstrap Parameters: StageName: Type: String RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String Resources: ApiGatewayCloudWatchLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub apigateway-cloudwatch-publish-role-${AWS::Region} Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole ApiGatewayAttachCloudwatchLogArn: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchLogRole.Arn AdminApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/api-gateway/access-logs-serverless-saas-admin-api RetentionInDays: 30 AdminApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: "/*" HttpMethod: "*" Auth: ResourcePolicy: CustomStatements: - Effect: Allow Principal: "*" Action: "execute-api:Invoke" Resource: ["execute-api:/*/*/*"] - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/tenant" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/user/tenant-admin" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref RegisterTenantLambdaExecutionRoleArn - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/disable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/enable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn AccessLogSetting: DestinationArn: !GetAtt AdminApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ["", ["serverless-saas-admin-api-", !Ref "AWS::Region"]] basePath: !Join ["", ["/", !Ref StageName]] schemes: - https paths: /registration: post: summary: Register a new tenant description: Register a new tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref RegisterTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant/activation/{tenantid}: put: security: - api_key: [] - Authorizer: [] summary: Activate an existing tenant description: Activate an existing tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ActivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenants: get: summary: Returns all tenants description: Returns all tenants produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantsFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant: post: summary: Creates a tenant description: Creates a tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/{tenantid}: get: summary: Returns a tenant description: Return a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Disables a tenant description: Disables a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeactivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy put: summary: Updates a tenant description: Updates a tenant produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user/{username}: get: summary: Returns a user description: Return a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUserFunctionArn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Diables a user description: Disable a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUserFunctionArn - /invocations httpMethod: POST type: aws_proxy /user/tenant-admin: post: summary: Creates a tenant admin user description: Creates a tenant admin user produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantAdminUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user: post: summary: Create a user description: Create a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users: get: summary: Get all users by tenantId description: Get all users by tenantId produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUsersFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/disable: put: summary: disable users by tenant id description: disable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/enable: put: summary: enable users by tenant id description: enable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref EnableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: sigv4Reference: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "awsSigv4" Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref AuthorizerFunctionArn - /invocations authorizerResultTtlInSeconds: 60 type: "token" StageName: prod Outputs: AdminApiGatewayApi: Value: !Ref AdminApiGatewayApi ================================================ FILE: Solution/Lab4/server/nested_templates/apigateway_lambdapermissions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway and apis as part of bootstrap Parameters: RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String AdminApiGatewayApi: Type: String Resources: #provide api gateway permissions to call lambda functions RegisterTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref RegisterTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateTenantAdminUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantAdminUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] EnableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref EnableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUsersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUsersFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref AuthorizerFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*" ]] CreateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantsFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeactivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeactivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ActivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ActivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ================================================ FILE: Solution/Lab4/server/nested_templates/cognito.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup cognito as part of bootstrap Parameters: AdminEmailParameter: Type: String Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Description: "Enter the role name for system admin" AdminUserPoolCallbackURLParameter: Type: String Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Description: "Enter Tenant Management userpool call back url" Resources: CognitoUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: PooledTenant-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into tenant UI application at " - "https://" - !Ref TenantUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for tenant UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSClient GenerateSecret: false UserPoolId: !Ref CognitoUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [pooledtenant-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoUserPool CognitoOperationUsersUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: OperationUsers-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into admin UI application at " - "https://" - !Ref AdminUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for admin UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoOperationUsersUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSOperationUsersPoolClient GenerateSecret: false UserPoolId: !Ref CognitoOperationUsersUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoOperationUsersUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [operationsusers-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUserGroup: Type: AWS::Cognito::UserPoolGroup Properties: GroupName: SystemAdmins Description: Admin user group Precedence: 0 UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUser: Type: AWS::Cognito::UserPoolUser Properties: Username: admin DesiredDeliveryMediums: - EMAIL ForceAliasCreation: true UserAttributes: - Name: email Value: !Ref AdminEmailParameter - Name: custom:tenantId Value: system_admins - Name: custom:userRole Value: !Ref SystemAdminRoleNameParameter UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAddUserToGroup: Type: AWS::Cognito::UserPoolUserToGroupAttachment Properties: GroupName: !Ref CognitoAdminUserGroup Username: !Ref CognitoAdminUser UserPoolId: !Ref CognitoOperationUsersUserPool Outputs: CognitoUserPoolId: Value: !Ref CognitoUserPool CognitoUserPoolClientId: Value: !Ref CognitoUserPoolClient CognitoOperationUsersUserPoolId: Value: !Ref CognitoOperationUsersUserPool CognitoOperationUsersUserPoolClientId: Value: !Ref CognitoOperationUsersUserPoolClient CognitoOperationUsersUserPoolProviderURL: Value: !GetAtt CognitoOperationUsersUserPool.ProviderURL ================================================ FILE: Solution/Lab4/server/nested_templates/lambdafunctions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy lambda functions as part of bootstrap Parameters: CognitoOperationUsersUserPoolId: Type: String CognitoOperationUsersUserPoolClientId: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String TenantDetailsTableArn: Type: String TenantUserMappingTableArn: Type: String Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies Description: Utilities for project ContentUri: ../layers/ CompatibleRuntimes: - python3.9 LicenseInfo: "MIT" RetentionPolicy: Retain Metadata: BuildMethod: python3.9 AuthorizerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: authorizer-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: authorizer-execution-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:List* Resource: - !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn AuthorizerAccessRole: Type: AWS::IAM::Role DependsOn: AuthorizerExecutionRole Properties: RoleName: authorizer-access-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: - !GetAtt 'AuthorizerExecutionRole.Arn' Action: - sts:AssumeRole Policies: - PolicyName: authorizer-access-role-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:BatchGetItem - dynamodb:GetItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:UpdateItem - dynamodb:Query - dynamodb:Scan Resource: - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/* SharedServicesAuthorizerFunction: Type: AWS::Serverless::Function DependsOn: AuthorizerAccessRole Properties: CodeUri: ../Resources/ Handler: shared_service_authorizer.lambda_handler Runtime: python3.9 Role: !GetAtt AuthorizerExecutionRole.Arn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !Ref CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !Ref CognitoOperationUsersUserPoolClientId TENANT_USER_POOL: !Ref CognitoUserPoolId TENANT_APP_CLIENT: !Ref CognitoUserPoolClientId #Create user pool for the tenant TenantUserPoolLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-userpool-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-userpool-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn - Effect: Allow Action: - dynamodb:GetItem - dynamodb:Query Resource: - !Ref TenantUserMappingTableArn CreateUserLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub create-user-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-user-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:PutItem Resource: - !Ref TenantUserMappingTableArn - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn CreateTenantAdminUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_tenant_admin_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId TENANT_APP_CLIENT_ID: !Ref CognitoUserPoolClientId POWERTOOLS_SERVICE_NAME: "UserManagement.CreateTenantAdmin" #User management CreateUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.CreateUser" UpdateUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.update_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.UpdateUser" DisableUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUser" DisableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUsersByTenant" EnableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.enable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.EnableUsersByTenant" GetUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.GetUser" GetUsersFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_users Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId POWERTOOLS_SERVICE_NAME: "UserManagement.GetUsers" #Tenant Management TenantManagementLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-management-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-tenant-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:Scan - dynamodb:Query Resource: - !Ref TenantDetailsTableArn - !Join ["", [!Ref TenantDetailsTableArn, '/index/*']] CreateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.create_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.CreateTenant" ActivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.activate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.ActivateTenant" ENABLE_USERS_BY_TENANT: "/users/enable" PROVISION_TENANT: "/provisioning/" GetTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.GetTenant" DeactivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.deactivate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.DeactivateTenant" DEPROVISION_TENANT: "/provisioning/" DISABLE_USERS_BY_TENANT: "/users/disable" UpdateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.update_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.UpdateTenant" GetTenantsFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenants Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers #Tenant Registration RegisterTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-registration-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess RegisterTenantFunction: Type: AWS::Serverless::Function DependsOn: RegisterTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-registration.register_tenant Runtime: python3.9 Role: !GetAtt RegisterTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: # Need to find a better way than hard coding resource paths CREATE_TENANT_ADMIN_USER_RESOURCE_PATH: "/user/tenant-admin" CREATE_TENANT_RESOURCE_PATH: "/tenant" PROVISION_TENANT_RESOURCE_PATH: "/provisioning" POWERTOOLS_SERVICE_NAME: "TenantRegistration.RegisterTenant" Outputs: RegisterTenantLambdaExecutionRoleArn: Value: !GetAtt RegisterTenantLambdaExecutionRole.Arn TenantManagementLambdaExecutionRoleArn: Value: !GetAtt TenantManagementLambdaExecutionRole.Arn RegisterTenantFunctionArn: Value: !GetAtt RegisterTenantFunction.Arn ActivateTenantFunctionArn: Value: !GetAtt ActivateTenantFunction.Arn GetTenantsFunctionArn: Value: !GetAtt GetTenantsFunction.Arn CreateTenantFunctionArn: Value: !GetAtt CreateTenantFunction.Arn GetTenantFunctionArn: Value: !GetAtt GetTenantFunction.Arn DeactivateTenantFunctionArn: Value: !GetAtt DeactivateTenantFunction.Arn UpdateTenantFunctionArn: Value: !GetAtt UpdateTenantFunction.Arn GetUsersFunctionArn: Value: !GetAtt GetUsersFunction.Arn GetUserFunctionArn: Value: !GetAtt GetUserFunction.Arn UpdateUserFunctionArn: Value: !GetAtt UpdateUserFunction.Arn DisableUserFunctionArn: Value: !GetAtt DisableUserFunction.Arn CreateTenantAdminUserFunctionArn: Value: !GetAtt CreateTenantAdminUserFunction.Arn CreateUserFunctionArn: Value: !GetAtt CreateUserFunction.Arn DisableUsersByTenantFunctionArn: Value: !GetAtt DisableUsersByTenantFunction.Arn EnableUsersByTenantFunctionArn: Value: !GetAtt EnableUsersByTenantFunction.Arn SharedServicesAuthorizerFunctionArn: Value: !GetAtt SharedServicesAuthorizerFunction.Arn AuthorizerExecutionRoleArn: Value: !GetAtt AuthorizerExecutionRole.Arn ================================================ FILE: Solution/Lab4/server/nested_templates/tables.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to create dynamodb tables as part of bootstrap Resources: TenantDetailsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: tenantName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: ServerlessSaas-TenantConfig KeySchema: - AttributeName: tenantName KeyType: HASH Projection: NonKeyAttributes: - userPoolId - appClientId - apiGatewayUrl ProjectionType: INCLUDE TableName: ServerlessSaaS-TenantDetails TenantUserMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: userName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH - AttributeName: userName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantUserMapping GlobalSecondaryIndexes: - IndexName: UserName KeySchema: - AttributeName: userName KeyType: HASH - AttributeName: tenantId KeyType: RANGE Projection: ProjectionType: ALL Outputs: TenantDetailsTableArn: Value: !GetAtt TenantDetailsTable.Arn TenantDetailsTableName: Value: !Ref TenantDetailsTable TenantUserMappingTableArn: Value: !GetAtt TenantUserMappingTable.Arn TenantUserMappingTableName: Value: !Ref TenantUserMappingTable ================================================ FILE: Solution/Lab4/server/nested_templates/userinterface.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: IsCloudFrontAndS3PreProvisioned: Type: String Default: false Description: "Tells if cloudfront and s3 buckets are pre-provisioned or not. They get pre-provisioned when the workshop is running as a part of AWS Event through AWS event engine tool." Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref IsCloudFrontAndS3PreProvisioned, true] ] Resources: CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: IsNotRunningInEventEngine Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distributions" AdminAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AdminAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AdminAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AdminAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId AdminAppSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: #Aliases: # - !Sub 'admin.${CustomDomainName}' CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS CachedMethods: - GET - HEAD - OPTIONS Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: adminapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt AdminAppBucket.RegionalDomainName Id: adminapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' LandingAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True LandingAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref LandingAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${LandingAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId LandingApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: landingapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'LandingAppBucket.RegionalDomainName' Id: landingapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' AppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId ApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: tenantapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'AppBucket.RegionalDomainName' Id: tenantapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: AdminBucket: Description: The name of the bucket for uploading the Admin Management site to Value: !Ref AdminAppBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt AdminAppSite.DomainName Condition: IsNotRunningInEventEngine LandingAppBucket: Description: The name of the bucket for uploading the Landing site to Value: !Ref LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt LandingApplicationSite.DomainName Condition: IsNotRunningInEventEngine AppBucket: Description: The name of the bucket for uploading the Tenant Management site to Value: !Ref AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt ApplicationSite.DomainName Condition: IsNotRunningInEventEngine ================================================ FILE: Solution/Lab4/server/shared-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas" s3_bucket = "aws-saas-sam-cli-ujwbuket" s3_prefix = "serverless-saas" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" ================================================ FILE: Solution/Lab4/server/shared-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to Bootstrap the Common Resources Parameters: AdminEmailParameter: Type: String Default: "test@test.com" Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Default: "SystemAdmin" Description: "Enter the role name for system admin" StageName: Type: String Default: "prod" Description: "Stage Name for the api" EventEngineParameter: Type: String Default: false Description: "Tells if this workshop is running as a part of AWS Event through AWS EventEngine or not" AdminUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Tenant Management userpool call back url" Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref EventEngineParameter, true] ] Resources: DynamoDBTables: Type: AWS::Serverless::Application Properties: Location: nested_templates/tables.yaml #Create cloudfront and s3 for UI Cde UserInterface: Type: AWS::Serverless::Application Properties: Location: nested_templates/userinterface.yaml Parameters: IsCloudFrontAndS3PreProvisioned: !Ref EventEngineParameter Cognito: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/cognito.yaml Parameters: AdminEmailParameter: !Ref AdminEmailParameter SystemAdminRoleNameParameter: !Ref SystemAdminRoleNameParameter AdminUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.AdminAppSite, !Ref AdminUserPoolCallbackURLParameter] TenantUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.ApplicationSite, !Ref TenantUserPoolCallbackURLParameter] LambdaFunctions: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/lambdafunctions.yaml Parameters: CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId CognitoOperationUsersUserPoolId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId CognitoOperationUsersUserPoolClientId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId TenantDetailsTableArn: !GetAtt DynamoDBTables.Outputs.TenantDetailsTableArn TenantUserMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantUserMappingTableArn APIs: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway.yaml Parameters: StageName: !Ref StageName RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn APIGatewayLambdaPermissions: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway_lambdapermissions.yaml Parameters: RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn AdminApiGatewayApi: !GetAtt APIs.Outputs.AdminApiGatewayApi Outputs: AdminApi: Description: "API Gateway endpoint URL for Admin API" Value: !Join ["", ["https://", !GetAtt APIs.Outputs.AdminApiGatewayApi, ".execute-api.", !Ref "AWS::Region", ".amazonaws.com/", !Ref StageName]] AdminSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant administration application Value: !GetAtt UserInterface.Outputs.AdminBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt UserInterface.Outputs.AdminAppSite Condition: IsNotRunningInEventEngine LandingApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the landing application Value: !GetAtt UserInterface.Outputs.LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt UserInterface.Outputs.LandingApplicationSite Condition: IsNotRunningInEventEngine ApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant application Value: !GetAtt UserInterface.Outputs.AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt UserInterface.Outputs.ApplicationSite Condition: IsNotRunningInEventEngine CognitoOperationUsersUserPoolProviderURL: Description: The Admin Management userpool provider url Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolProviderURL CognitoOperationUsersUserPoolClientId: Description: The Admin Management userpool client id Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId CognitoTenantUserPoolId: Description: The user pool id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolId Export: Name: "Serverless-SaaS-CognitoTenantUserPoolId" CognitoTenantAppClientId: Description: The app client id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolClientId Export: Name: "Serverless-SaaS-CognitoTenantAppClientId" AuthorizerExecutionRoleArn: Description: The Lambda authorizer execution role Value: !GetAtt LambdaFunctions.Outputs.AuthorizerExecutionRoleArn Export: Name: "Serverless-SaaS-AuthorizerExecutionRoleArn" ================================================ FILE: Solution/Lab4/server/tenant-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "stack-pooled" s3_bucket = "aws-saas-sam-cli-ujwbuket" s3_prefix = "serverless-saas-tenant" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM" ================================================ FILE: Solution/Lab4/server/tenant-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Serverless SaaS Reference Architecture Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Parameters: StageName: Type: String Default: "prod" Description: "Stage Name for the api" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies-pooled Description: Utilities for project ContentUri: layers/ CompatibleRuntimes: - python3.9 LicenseInfo: 'MIT' RetentionPolicy: Retain Metadata: BuildMethod: python3.9 ProductTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: productId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: productId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: Product-pooled OrderTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: orderId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: orderId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: Order-pooled ProductFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: pooled-product-function-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: pooled-product-function-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt ProductTable.Arn GetProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable GetProductsFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_products Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable CreateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.create_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable UpdateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.update_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable DeleteProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.delete_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" PRODUCT_TABLE_NAME: !Ref ProductTable OrderFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: pooled-order-function-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: pooled-order-function-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt OrderTable.Arn GetOrdersFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_orders Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable GetOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable CreateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.create_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable UpdateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.update_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable DeleteOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.delete_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" ORDER_TABLE_NAME: !Ref OrderTable BusinessServicesAuthorizerFunction: Type: AWS::Serverless::Function Properties: CodeUri: Resources/ Handler: tenant_authorizer.lambda_handler Runtime: python3.9 Role: !ImportValue Serverless-SaaS-AuthorizerExecutionRoleArn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL: !ImportValue Serverless-SaaS-CognitoTenantUserPoolId TENANT_APP_CLIENT: !ImportValue Serverless-SaaS-CognitoTenantAppClientId ApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/api-gateway/access-logs-serverless-saas-tenant-api-pooled RetentionInDays: 30 ApiGatewayTenantApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: '/*' HttpMethod: '*' AccessLogSetting: DestinationArn: !GetAtt ApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: 'pooled-serverless-saas-tenant-api' basePath: !Join ['', ['/', !Ref StageName]] x-amazon-apigateway-api-key-source : "AUTHORIZER" schemes: - https paths: /order/{id}: get: summary: Returns a order description: Return a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a order description: Deletes a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /orders: get: summary: Returns all orders description: Returns all orders. produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrdersFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /order: post: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product/{id}: get: summary: Returns a product description: Return a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a product description: Deletes a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /products: get: summary: Returns all products description: Returns all products. produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductsFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product: post: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt BusinessServicesAuthorizerFunction.Arn - /invocations authorizerResultTtlInSeconds: 30 type: "token" StageName: !Ref StageName GetProductsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt GetProductsFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrdersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrdersFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt BusinessServicesAuthorizerFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref ApiGatewayTenantApi, "/*/*" ]] Outputs: TenantApiGatewayId: Description: Id for Tenant API Gateway Value: !Ref ApiGatewayTenantApi TenantAPI: Description: "API Gateway endpoint URL for Tenant API" Value: !Join ['', [!Sub "https://${ApiGatewayTenantApi}.execute-api.${AWS::Region}.amazonaws.com/", !Ref StageName]] ================================================ FILE: Solution/Lab5/client/Admin/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab5/client/Admin/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab5/client/Admin/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab5/client/Admin/README.md ================================================ # Admin This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab5/client/Admin/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "admin": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "admin:build:production" }, "development": { "browserTarget": "admin:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "admin:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab5/client/Admin/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab5/client/Admin/package.json ================================================ { "name": "admin", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/flex-layout": "~14.0.0-beta.40", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.1.3", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab5/client/Admin/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Tenants', url: '/tenants', icon: 'groups', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Solution/Lab5/client/Admin/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: '', component: NavComponent, data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'tenants', loadChildren: () => import('./views/tenants/tenants.module').then((m) => m.TenantsModule), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab5/client/Admin/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab5/client/Admin/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Solution/Lab5/client/Admin/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { HttpInterceptorProviders } from './interceptors'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; @NgModule({ declarations: [AppComponent, NavComponent, AuthComponent], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, HttpClientModule, HttpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab5/client/Admin/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Solution/Lab5/client/Admin/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const HttpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Solution/Lab5/client/Admin/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Solution/Lab5/client/Admin/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Solution/Lab5/client/Admin/src/app/nav/nav.component.css ================================================ .sidenav-container { height: 100%; } .sidenav-content-container { background-color: lightgray; } .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .mat-toolbar.mat-primary { position: sticky; top: 0; z-index: 1; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .spinner-container { height: 80%; display: flex; justify-content: center; align-items: center; box-sizing: border-box; } ================================================ FILE: Solution/Lab5/client/Admin/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ username$ | async }}
================================================ FILE: Solution/Lab5/client/Admin/src/app/nav/nav.component.spec.ts ================================================ import { LayoutModule } from '@angular/cdk/layout'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { NavComponent } from './nav.component'; describe('NavComponent', () => { let component: NavComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [NavComponent], imports: [ NoopAnimationsModule, LayoutModule, MatButtonModule, MatIconModule, MatListModule, MatSidenavModule, MatToolbarModule, ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(NavComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should compile', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: Solution/Lab5/client/Admin/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.css'], }) export class NavComponent implements OnInit { tenantName = ''; loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router ) { this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => err); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map((sesh) => sesh && sesh.isValid()) ); const token$ = session$.pipe(map((sesh) => sesh && sesh.getIdToken())); this.username$ = token$.pipe( map((t) => t && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Solution/Lab5/client/Admin/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/dashboard/dashboard.component.html ================================================

Traffic

December 2020
================================================ FILE: Solution/Lab5/client/Admin/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } .chart-wrapper { padding-right: 20px; position: relative; margin: auto; height: 80vh; width: 80vw; } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/dashboard/dashboard.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ChartConfiguration, ChartType } from 'chart.js'; import { TenantsService } from '../tenants/tenants.service'; interface DataSet { label: string; data: number[]; } interface ChartData { tenantId: string; dataSet: DataSet[]; totalOrders: number; } @Component({ templateUrl: 'dashboard.component.html', styleUrls: ['./dashboard.component.scss'], selector: 'app-dashboard', }) export class DashboardComponent implements OnInit { constructor(private tenantSvc: TenantsService) {} data: ChartData[] = []; // lineChart public lineChartElements = 27; public lineChartData1: Array = []; public lineChartData2: Array = []; public lineChartData3: Array = []; public lineChartData: Array = [ { data: this.lineChartData1, label: 'Current', backgroundColor: 'rgba(148,159,177,0.2)', borderColor: 'rgba(148,159,177,1)', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, { data: this.lineChartData2, label: 'Previous', backgroundColor: 'rgba(77,83,96,0.2)', borderColor: 'rgba(77,83,96,1)', pointBackgroundColor: 'rgba(77,83,96,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(77,83,96,1)', fill: 'origin', }, { data: this.lineChartData3, label: 'BEP', backgroundColor: 'rgba(255,0,0,0.3)', borderColor: 'red', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, ]; public lineChartLegend = false; /* tslint:disable:max-line-length */ public lineChartLabels: Array = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ]; /* tslint:enable:max-line-length */ public lineChartType: ChartType = 'line'; public lineChartOptions: ChartConfiguration['options'] = { maintainAspectRatio: false, elements: { line: { tension: 0.5, }, }, scales: { // We use this empty structure as a placeholder for dynamic theming. x: {}, 'y-axis-0': { position: 'left', }, 'y-axis-1': { position: 'right', grid: { color: 'rgba(80,80,80,0.3)', }, ticks: { color: '#808080', }, }, }, }; public random(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } ngOnInit(): void { for (let i = 0; i <= this.lineChartElements; i++) { this.lineChartData1.push(this.random(50, 200)); this.lineChartData2.push(this.random(80, 100)); this.lineChartData3.push(65); } } } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/tenants/create/create.component.html ================================================
Provision tenant Name Email Phone Address Plan
================================================ FILE: Solution/Lab5/client/Admin/src/app/views/tenants/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/tenants/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { TenantsService } from '../tenants.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { submitting = false; tenantForm: FormGroup = this.fb.group({ tenantName: [null, [Validators.required]], tenantEmail: [null, [Validators.email, Validators.required]], tenantTier: [null, [Validators.required]], tenantPhone: [null], tenantAddress: [null], }); constructor( private fb: FormBuilder, private tenantSvc: TenantsService, private router: Router, private _snackBar: MatSnackBar ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; const user = { ...this.tenantForm.value, }; this.tenantSvc.post(this.tenantForm.value).subscribe({ next: () => { this.submitting = false; this.openErrorMessageSnackBar('Successfully created new tenant!'); this.router.navigate(['tenants']); }, error: (err) => { this.submitting = false; this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); }, }); } } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/tenants/list/list.component.html ================================================
Tenant List
Id {{ element.tenantId }} Tenant Name {{ element.tenantName }} E-Mail {{ element.tenantEmail }} Plan {{ element.tenantTier }} Status {{ element.isActive }}
================================================ FILE: Solution/Lab5/client/Admin/src/app/views/tenants/list/list.component.scss ================================================ .tenant-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/tenants/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Tenant } from '../models/tenant'; import { TenantsService } from '../tenants.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { tenants$ = new Observable(); tenantData: Tenant[] = []; isLoading: boolean = true; displayedColumns = [ 'tenantId', 'tenantName', 'tenantEmail', 'tenantTier', 'isActive', ]; constructor(private tenantSvc: TenantsService) {} ngOnInit(): void { this.tenantSvc.fetch().subscribe((data) => { this.tenantData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/tenants/models/tenant.ts ================================================ export interface Tenant { tenantId: string; tenantName: string; tenantEmail: string; tenantTier: string; isActive: boolean; } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/tenants/tenants-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', component: ListComponent, data: { title: 'Tenant List', }, }, { path: 'create', component: CreateComponent, data: { title: 'Provision new tenant', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class TenantsRoutingModule {} ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/tenants/tenants.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ListComponent } from './list/list.component'; import { TenantsRoutingModule } from './tenants-routing.module'; import { CreateComponent } from './create/create.component'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, TenantsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ReactiveFormsModule, MatSnackBarModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class TenantsModule {} ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/tenants/tenants.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Tenant } from './models/tenant'; @Injectable({ providedIn: 'root', }) export class TenantsService { constructor(private http: HttpClient) {} baseUrl = `${environment.apiUrl}`; tenantsApiUrl = `${this.baseUrl}/tenants`; registrationApiUrl = `${this.baseUrl}/registration`; // TODO strongly-type these anys as tenants once we dial in what the tenant call should return fetch(): Observable { return this.http.get(this.tenantsApiUrl); } post(tenant: Tenant): Observable { return this.http.post(this.registrationApiUrl, tenant); } } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
Tenant Id folder_special
================================================ FILE: Solution/Lab5/client/Admin/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], tenantId: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Solution/Lab5/client/Admin/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Solution/Lab5/client/Admin/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.apiUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } } ================================================ FILE: Solution/Lab5/client/Admin/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab5/client/Admin/src/aws-exports.ts ================================================ const awsmobile = { aws_project_region: 'us-west-2', aws_cognito_region: 'us-west-2', aws_user_pools_id: 'us-west-2_nWhwjijMc', aws_user_pools_web_client_id: '61ipvhmvdrfso0cftpil9irk8k', }; export default awsmobile; ================================================ FILE: Solution/Lab5/client/Admin/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab5/client/Admin/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab5/client/Admin/src/environments/environment.ts ================================================ export const environment = { production: false, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab5/client/Admin/src/index.html ================================================ Dashboard ================================================ FILE: Solution/Lab5/client/Admin/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { Amplify } from 'aws-amplify'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import aws_exports from './aws-exports'; Amplify.configure(aws_exports); if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab5/client/Admin/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab5/client/Admin/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab5/client/Admin/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab5/client/Admin/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; ================================================ FILE: Solution/Lab5/client/Admin/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab5/client/Admin/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab5/client/Admin/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab5/client/Admin/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab5/client/Application/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab5/client/Application/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab5/client/Application/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end #cypress cypress/videos/* cypress.env.json # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab5/client/Application/README.md ================================================ # Application This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab5/client/Application/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "application": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "2mb", "maximumError": "2mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "6kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "application:build:production" }, "development": { "browserTarget": "application:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "application:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab5/client/Application/cypress/README.md ================================================ # Application End-to-End Testing ## Instructions To run End-to-End (e2e) tests against the Sample Application, take the following steps: 1. Make a copy of the example env file (`cypress.env.json.example`): ```bash cp cypress.env.json.example cypress.env.json ``` 2. Edit the new file and replace the sample values with real values. The following should help when deciding what to use in place of the sample values provided: - `host`: The URL where the Sample Application is running. If testing locally, this is usually `"http://localhost:4200"`. - `tenantName`: The name of the tenant used to identify the appropriate Cognito User Pool to use for Authentication. - `tenantUsername`: The username to use when logging in. - `tenantUserPassword`: The password to use when logging in. - `email`: The email address to use for testing. (This should be a valid email address.) 3. Navigate to the root of the Application project (`aws-saas-factory-ref-solution-serverless-saas/clients/Application/`) and run the following: ```bash npx cypress run ``` This will run the tests located in the `cypress/e2e` folder. The documentation [here](https://docs.cypress.io/guides/guides/command-line#cypress-run) has more information on what can be passed in as arguments when running the Cypress tests. For example, running the following will show the Cypress UI and what is happening as each of the tests are run: ```bash npx cypress run --headed ``` ================================================ FILE: Solution/Lab5/client/Application/cypress/e2e/1-getting-started/basic-access.cy.js ================================================ /// describe('check that the app redirects to /unauthorized when tenant is not set', () => { it('redirects to unauthorized when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); }) it('redirects to unauthorized when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); }) it('redirects to unauthorized when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); }) }) describe('check that the app redirects to a page with a sign-in form when tenant is set and user is not logged in', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').should('exist') cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.location('href').should('contain', '/dashboard') }) it('redirects when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) }) ================================================ FILE: Solution/Lab5/client/Application/cypress/e2e/1-getting-started/product-testing.cy.js ================================================ /// describe('check that product, order and user functionality works as expected', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.get('form input[name="username"]').type(Cypress.env('tenantUsername')) cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() cy.wait(1500) cy.get('body').then(body => { if (body.find('form input[name="confirm_password"]').length > 0) { cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form input[name="confirm_password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() } }) }) it('can create new users and display them', () => { const email_username = Cypress.env('email').split('@')[0]; const email_domain = Cypress.env('email').split('@')[1]; const random_suffix = '+test'+Date.now().toString().slice(-3); const myUser = { name: "myUser-"+Date.now(), email: email_username + random_suffix + '@' + email_domain, role: 'userRole'+Date.now().toString().slice(-5) } cy.get("a").contains("Users").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.get("button[color='primary'").contains("Add User").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="userName"]').type(myUser.name) cy.get('input[formcontrolname="userEmail"]').type(myUser.email) cy.get('input[formcontrolname="userRole"]').type(myUser.role) }) cy.intercept({ method: 'POST', url: '**/user', }).as('postUser') cy.intercept({ method: 'GET', url: '**/users', }).as('getUsers') cy.get("button").contains("Create").click() cy.wait('@postUser') cy.go('back') cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.wait('@getUsers') cy.get('table td').contains(myUser.email) }) it('can create a new order with a new product and see them listed', () => { const myProduct = { name: "myProduct-"+Date.now(), price: Date.now().toString().slice(-3), sku: Date.now().toString().slice(-5), category: "category3", } const myOrder = { name: "myOrder-"+Date.now(), } // NOW TESTING PRODUCT CREATION // cy.get("a").contains("Products").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.get("button[color='primary'").contains("Create Product").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="name"]').type(myProduct.name) cy.get('input[formcontrolname="price"]').type(myProduct.price) cy.get('input[formcontrolname="sku"]').type(myProduct.sku) cy.get('mat-select[formcontrolname="category"]').click() }) cy.get('.mat-option-text').contains(myProduct.category).click() cy.get("button").contains("Submit").should('not.be.disabled') cy.intercept({ method: 'POST', url: '**/product', }).as('postProduct') cy.intercept({ method: 'GET', url: '**/products', }).as('getProducts') cy.get("button").contains("Submit").click() cy.wait('@postProduct') cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.wait('@getProducts') cy.get('table td').contains(myProduct.name) cy.get('table td').contains(myProduct.price) cy.get("a").contains("Orders").click() // DONE TESTING PRODUCT CREATION // // NOW TESTING ORDER CREATION // cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.get("button[color='primary']").contains("Create Order").click() cy.location().should((loc) => { expect(loc.href).to.contain('/orders/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="orderName"]').type(myOrder.name) }) cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.intercept({ method: 'POST', url: '**/order', }).as('postOrder') cy.intercept({ method: 'GET', url: '**/orders', }).as('getOrders') cy.get("button").contains("Submit").click() cy.wait('@postOrder') cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.wait('@getOrders') cy.get('table td').contains(myOrder.name) cy.get('table td').contains(new Intl.NumberFormat().format(myProduct.price * 2)) // DONE TESTING ORDER CREATION // }) }) ================================================ FILE: Solution/Lab5/client/Application/cypress.config.ts ================================================ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { supportFile: false, setupNodeEvents(on, config) { // implement node event listeners here }, }, }); ================================================ FILE: Solution/Lab5/client/Application/cypress.env.json.example ================================================ { "host": "http://localhost:4200", "tenantName": "UPDATE_ME!", "tenantUsername": "UPDATE_ME!", "tenantUserPassword": "UPDATE_ME!", "email": "test@example.com" } ================================================ FILE: Solution/Lab5/client/Application/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/application'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab5/client/Application/package.json ================================================ { "name": "application", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "e2e": "npx cypress run" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.2.0", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "cypress": "~10.3.1", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab5/client/Application/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Products', url: '/products', icon: 'sell', }, { name: 'Orders', url: '/orders', icon: 'shopping_cart', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Solution/Lab5/client/Application/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { CognitoGuard } from './cognito.guard'; export const routes: Routes = [ { path: '', redirectTo: 'unauthorized', pathMatch: 'full', }, { path: '', component: NavComponent, canActivate: [CognitoGuard], data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'orders', loadChildren: () => import('./views/orders/orders.module').then((m) => m.OrdersModule), }, { path: 'products', loadChildren: () => import('./views/products/products.module').then( (m) => m.ProductsModule ), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, { path: 'unauthorized', component: UnauthorizedComponent, }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab5/client/Application/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab5/client/Application/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Solution/Lab5/client/Application/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { httpInterceptorProviders } from './interceptors'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; @NgModule({ declarations: [ AppComponent, NavComponent, AuthComponent, UnauthorizedComponent, ], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, MatSnackBarModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatIconModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, httpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab5/client/Application/src/app/cognito.guard.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, } from '@angular/router'; import { Auth } from 'aws-amplify'; import { AuthConfigurationService } from './views/auth/auth-configuration.service'; @Injectable({ providedIn: 'root' }) export class CognitoGuard implements CanActivate { constructor( private router: Router, private authConfigService: AuthConfigurationService ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Promise { if (!this.authConfigService.configureAmplifyAuth()) { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return new Promise((res, rej) => { res(false); }); } return Auth.currentSession() .then((u) => { if (u.isValid()) { return true; } else { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return false; } }) .catch((e) => { if (state.url === '/dashboard') { // if we're going to the dashboard and we're not logged in, // don't stop the flow as the amplify-authenticator will // route requests going to the dashboard to the sign-in page. return true; } console.log('Error getting current session', e); this.router.navigate(['/unauthorized']); return false; }); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const httpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Solution/Lab5/client/Application/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Solution/Lab5/client/Application/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Solution/Lab5/client/Application/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ (companyName$ | async) || "" }} {{ (username$ | async) || user.username}}
================================================ FILE: Solution/Lab5/client/Application/src/app/nav/nav.component.scss ================================================ .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .sidebar-icon-container { height: 64px; background-color: whitesmoke; display: flex; align-items: center; justify-content: center; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .footer { position: fixed; bottom: 0; width: 100%; height: 40px; background-color: whitesmoke; color: #2d3337; // text-align: center; margin: 0px; } .footer-text { display: flex; } .content { position: absolute; width: 100%; height: 90%; max-height: 90%; overflow: auto; background-color: lightgray; } ================================================ FILE: Solution/Lab5/client/Application/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; import { AuthConfigurationService } from './../views/auth/auth-configuration.service'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.scss'], }) export class NavComponent implements OnInit { loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router, private authConfigService: AuthConfigurationService ) { // this.configSvc.loadConfigurations().subscribe((val) => console.log(val)); this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => { console.log('Failed to get current session. Err: ', err); return err; }); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map( (sesh) => sesh && typeof sesh.isValid === 'function' && sesh.isValid() ) ); const token$ = session$.pipe( map( (sesh) => sesh && typeof sesh.getIdToken === 'function' && sesh.getIdToken() ) ); this.username$ = token$.pipe( map((t) => t && t.payload && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload && t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } async logout() { await Auth.signOut({ global: true }) .then((e) => { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); }) .catch((err) => { console.error('Error logging out: ', err); }); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/auth/auth-configuration.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpParams, HttpParamsOptions, } from '@angular/common/http'; import { Injectable, OnInit } from '@angular/core'; import { map, switchMap, catchError } from 'rxjs/operators'; import { throwError } from 'rxjs'; import { environment } from '../../../environments/environment'; import { ConfigParams } from './models/config-params'; import { ActivatedRoute } from '@angular/router'; import Amplify from 'aws-amplify'; import { Auth } from 'aws-amplify'; import { Router } from '@angular/router'; import { from, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AuthConfigurationService { params$: Observable; params: ConfigParams; tenantName: string; constructor( private http: HttpClient, private route: ActivatedRoute, private router: Router ) {} public setTenantConfig(tenantName: string): Promise { const url = `${environment.regApiGatewayUrl}/tenant/init/` + tenantName; this.params$ = this.http.get(url); const setup$ = this.params$.pipe( map((val) => { // remove trailing slash (/) if present val.apiGatewayUrl = val.apiGatewayUrl.replace(/\/$/, ''); localStorage.setItem('userPoolId', val.userPoolId); localStorage.setItem('tenantName', tenantName); localStorage.setItem('appClientId', val.appClientId); localStorage.setItem('apiGatewayUrl', val.apiGatewayUrl); return 'success'; }), catchError((error) => { console.log('Error setting tenant config: ', error); return throwError(error); }) ); return setup$.toPromise(); } configureAmplifyAuth(): boolean { try { const userPoolId = localStorage.getItem('userPoolId'); const appClientId = localStorage.getItem('appClientId'); if (!userPoolId || !appClientId) { return false; } const region = userPoolId?.split('_')[0]; const awsmobile = { aws_project_region: region, aws_cognito_region: region, aws_user_pools_id: userPoolId, aws_user_pools_web_client_id: appClientId, }; Amplify.configure(awsmobile); return true; } catch (err) { console.error('Unable to initialize amplify auth.', err); return false; } } cleanLocalStorage() { localStorage.removeItem('tenantName'); localStorage.removeItem('userPoolId'); localStorage.removeItem('appClientId'); localStorage.removeItem('apiGatewayUrl'); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Solution/Lab5/client/Application/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/auth/models/config-params.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ export interface ConfigParams { appClientId: string; userPoolId: string; apiGatewayUrl: string; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Solution/Lab5/client/Application/src/app/views/dashboard/dashboard.component.html ================================================

Dashboard

{{ card.title }}
================================================ FILE: Solution/Lab5/client/Application/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/dashboard/dashboard.component.ts ================================================ import { Component, ViewChild } from '@angular/core'; import { map } from 'rxjs/operators'; import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js'; import { BaseChartDirective } from 'ng2-charts'; import DataLabelsPlugin from 'chartjs-plugin-datalabels'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'], }) export class DashboardComponent { @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; public barChartOptions: ChartConfiguration['options'] = { responsive: true, maintainAspectRatio: false, // We use these empty structures as placeholders for dynamic theming. scales: { x: {}, y: { min: 10, }, }, plugins: { legend: { display: true, }, datalabels: { anchor: 'end', align: 'end', }, }, }; public barChartType: ChartType = 'bar'; public barChartPlugins = [DataLabelsPlugin]; public barChartData: ChartData<'bar'> = { labels: ['2006', '2007', '2008', '2009', '2010', '2011', '2012'], datasets: [ { data: [65, 59, 80, 81, 56, 55, 40], label: 'Series A' }, { data: [28, 48, 40, 19, 86, 27, 90], label: 'Series B' }, ], }; // events public chartClicked({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public chartHovered({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public randomize(): void { // Only Change 3 values this.barChartData.datasets[0].data = [ Math.round(Math.random() * 100), 59, 80, Math.round(Math.random() * 100), 56, Math.round(Math.random() * 100), 40, ]; this.chart?.update(); } /** Based on the screen size, switch from standard to one column per row */ cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe( map(({ matches }) => { if (matches) { return [ { title: 'Card 1', cols: 1, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 1 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; } return [ { title: 'Card 1', cols: 2, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 2 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; }) ); constructor(private breakpointObserver: BreakpointObserver) {} } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Solution/Lab5/client/Application/src/app/views/error/404.component.html ================================================

404

Oops! You're lost.

The page you are looking for was not found.

================================================ FILE: Solution/Lab5/client/Application/src/app/views/error/404.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '404.component.html', }) export class P404Component { constructor() {} } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/error/500.component.html ================================================

500

Houston, we have a problem!

The page you are looking for is temporarily unavailable.

================================================ FILE: Solution/Lab5/client/Application/src/app/views/error/500.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '500.component.html', }) export class P500Component { constructor() {} } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/error/unauthorized.component.html ================================================
Unauthorized Enter your tenant name and click submit below Tenant Name home
================================================ FILE: Solution/Lab5/client/Application/src/app/views/error/unauthorized.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .center-screen { display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; min-height: 100vh; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/error/unauthorized.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AuthConfigurationService } from './../auth/auth-configuration.service'; import { Observable } from 'rxjs'; import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-unauthorized', templateUrl: './unauthorized.component.html', styleUrls: ['./unauthorized.component.scss'], }) export class UnauthorizedComponent implements OnInit { tenantForm: FormGroup; params$: Observable; error = false; errorMessage: string; tenantNameRequired: boolean = true; constructor( private fb: FormBuilder, private authConfigService: AuthConfigurationService, private _snackBar: MatSnackBar, private router: Router ) { if ( 'userPoolId' in environment && 'appClientId' in environment && 'apiGatewayUrl' in environment ) { // If a tenant's cognito configuration is provided in the // "environment" object, then we take that instead of asking // the visitor to provide the name of their tenant in order // to do a look-up for that tenant's cognito configuration. localStorage.setItem('tenantName', 'PooledTenants'); localStorage.setItem('userPoolId', (environment as any).userPoolId); localStorage.setItem('appClientId', (environment as any).appClientId); localStorage.setItem('apiGatewayUrl', (environment as any).apiGatewayUrl); this.tenantNameRequired = false; } } ngOnInit(): void { this.tenantForm = this.fb.group({ tenantName: [null, [Validators.required]], }); } isFieldInvalid(field: string) { const formField = this.tenantForm.get(field); return ( formField && formField.invalid && (formField.dirty || formField.touched) ); } displayFieldCss(field: string) { return { 'is-invalid': this.isFieldInvalid(field), }; } hasRequiredError(field: string) { return !!this.tenantForm.get(field)?.hasError('required'); } openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } login() { if (!this.tenantNameRequired) { this.router.navigate(['/dashboard']); return true; } let tenantName = this.tenantForm.value.tenantName; if (!tenantName) { this.errorMessage = 'No tenant name provided.'; this.error = true; this.openErrorMessageSnackBar(this.errorMessage); return false; } this.authConfigService .setTenantConfig(tenantName) .then((val) => { this.router.navigate(['/dashboard']); }) .catch((errorResponse) => { this.error = true; this.errorMessage = errorResponse.error.message || 'An unexpected error occurred!'; this.openErrorMessageSnackBar(this.errorMessage); }); return false; } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/create/create.component.html ================================================
Create new order Enter order name Name is required
{{ op.product.name }}
{{ op.product.price | currency }}
{{ op.quantity || 0 }}
================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/create/create.component.scss ================================================ // .card { // margin: 20px; // display: flex; // flex-direction: column; // align-items: flex-start; // } .order-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { Product } from '../../products/models/product.interface'; import { ProductService } from '../../products/product.service'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; interface LineItem { product: Product; quantity?: number; } @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { displayedColumns = []; orderForm: FormGroup; orderProducts: LineItem[] = []; isLoadingProducts: boolean = true; error = ''; constructor( private fb: FormBuilder, private productSvc: ProductService, private orderSvc: OrdersService, private router: Router ) { this.orderForm = this.fb.group({ name: ['', Validators.required], }); } ngOnInit(): void { this.productSvc.fetch().subscribe((products) => { this.orderProducts = products.map((p) => ({ product: p })); this.isLoadingProducts = false; }); this.orderForm = this.fb.group({ orderName: ['', Validators.required], }); } get name() { return this.orderForm.get('name'); } get productQuantity() { return this.orderProducts .filter((p) => !!p.quantity).length > 0; } add(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity ? orderProduct.quantity + 1 : 1, }; } return p; }); } remove(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity && orderProduct.quantity > 1 ? orderProduct.quantity - 1 : undefined, }; } return p; }); } submit() { const val: Order = { ...this.orderForm?.value, orderProducts: this.orderProducts .filter((p) => !!p.quantity) .map((p) => ({ productId: p.product.productId, price: p.product.price, quantity: p.quantity, })), }; this.orderSvc.create(val).subscribe(() => this.router.navigate(['orders'])); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/detail/detail.component.html ================================================

Order Details mostly made up

NO: {{ orderId$ | async }} | Date: {{ today() | date }}
  • {{ tenantName() }}

Services / Products

Item / Details Unit Cost Sum Cost Discount Tax Total
{{ op.productId }}
{{ op.price | currency }}
Before Tax
{{ sum(op) | currency }}
{{ op.quantity }} Units
$0.00
None
{{ tax(op) | currency }}
Sales Tax 8.9%
{{ total(op) | currency }}
Sub Total Discount Total Tax Final
{{ subTotal(order) | currency }} -$0.00 {{ subTotal(order) | currency }} {{ calcTax(order) | currency }} {{ final(order) | currency }}
Comments / Notes
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Odit repudiandae numquam sit facere blanditiis, quasi distinctio ipsam? Libero odit ex expedita, facere sunt, possimus consectetur dolore, nobis iure amet vero.
================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/detail/detail.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .no-bottom-margin { margin-bottom: 0; } .nowrap { white-space: nowrap; } .invoice { font-family: Arial, Helvetica, sans-serif; width: 970px !important; margin: 50px auto; .invoice-header { padding: 25px 25px 15px; h1 { margin: 0; } .media { .media-body { font-size: 0.9em; margin: 0; } } } .invoice-body { border-radius: 10px; padding: 25px; background: #fff; } .invoice-footer { padding: 15px; font-size: 0.9em; text-align: center; color: #999; } } .logo { max-height: 70px; border-radius: 10px; } .dl-horizontal { margin: 0; dt { float: left; width: 80px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } dd { margin-left: 90px; } } .rowamount { padding-top: 15px !important; } .rowtotal { font-size: 1.3em; } .colfix { width: 12%; } .mono { font-family: monospace; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/detail/detail.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Order } from '../models/order.interface'; import { OrderProduct } from '../models/orderproduct.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-detail', templateUrl: './detail.component.html', styleUrls: ['./detail.component.scss'], }) export class DetailComponent implements OnInit { orderId$: Observable | undefined; order$: Observable | undefined; orderProducts$: Observable | undefined; taxRate = 0.0899; constructor(private route: ActivatedRoute, private orderSvc: OrdersService) {} ngOnInit(): void { this.orderId$ = this.route.params.pipe(map((o) => o['orderId'])); this.order$ = this.orderId$.pipe(switchMap((o) => this.orderSvc.get(o))); this.orderProducts$ = this.order$.pipe(map((o) => o.orderProducts)); } today() { return new Date(); } tenantName() { return ''; } sum(op: OrderProduct) { return op.price * op.quantity; } tax(op: OrderProduct) { return this.sum(op) * this.taxRate; } total(op: OrderProduct) { return this.sum(op) + this.tax(op); } subTotal(order: Order) { return order.orderProducts .map((op) => op.price * op.quantity) .reduce((acc, curr) => acc + curr); } calcTax(order: Order) { return this.subTotal(order) * this.taxRate; } final(order: Order) { return this.subTotal(order) + this.calcTax(order); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/list/list.component.html ================================================

Order List

Name {{ element.orderName }} Line Items {{ element.orderProducts?.length }} Total {{ sum(element) | currency }}
================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/list/list.component.scss ================================================ .order-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { displayedColumns: string[] = ['name', 'lineItems', 'total']; orderData: Order[] = []; isLoading: boolean = true; constructor(private orderSvc: OrdersService, private router: Router) {} ngOnInit(): void { this.orderSvc.fetch().subscribe((data) => { this.isLoading = false; this.orderData = data; }); } sum(order: Order): number { return order.orderProducts .map((p) => p.price * p.quantity) .reduce((acc, curr) => acc + curr); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/models/order.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { OrderProduct } from './orderproduct.interface'; export interface Order { key: string; shardId: string; orderId: string; orderName: string; orderProducts: OrderProduct[]; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/models/orderproduct.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface OrderProduct { productId: string; price: number; quantity: number; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/orders-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', data: { title: 'All Orders', }, component: ListComponent, }, { path: 'create', data: { title: 'Create Order', }, component: CreateComponent, }, { path: 'detail/:orderId', data: { title: 'View Order Detail', }, component: DetailComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class OrdersRoutingModule {} ================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/orders.module.ts ================================================ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; import { OrdersRoutingModule } from './orders-routing.module'; @NgModule({ declarations: [CreateComponent, ListComponent, DetailComponent], imports: [ CommonModule, OrdersRoutingModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class OrdersModule {} ================================================ FILE: Solution/Lab5/client/Application/src/app/views/orders/orders.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Order } from './models/order.interface'; @Injectable({ providedIn: 'root', }) export class OrdersService { orders: Order[] = []; baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; constructor(private http: HttpClient) {} fetch(): Observable { return this.http.get(`${this.baseUrl}/orders`); } get(orderId: string): Observable { const url = `${this.baseUrl}/order/${orderId}`; return this.http.get(url); } create(order: Order): Observable { return this.http.post(`${this.baseUrl}/order`, order); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/create/create.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required SKU Category {{category}}
================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ProductService } from '../product.service'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); } get name() { return this.productForm.get('name'); } get price() { return this.productForm.get('price'); } submit() { this.productSvc.post(this.productForm.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err.message); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/edit/edit.component.html ================================================
Edit Product Product ID Enter product name Name is required Enter product price Price is required SKU Category {{ category }}
================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/edit/edit.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/edit/edit.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.scss'], }) export class EditComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; product$: Observable | undefined; productId$: Observable | undefined; productName$: Observable | undefined; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router, private route: ActivatedRoute ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ shardId: [], productId: [], name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); this.productId$ = this.route.params.pipe(map((p) => p['productId'])); this.product$ = this.productId$.pipe( switchMap((p) => this.productSvc.get(p)) ); this.productName$ = this.product$.pipe(map((p) => p?.name)); this.product$.subscribe((val) => { this.productForm?.patchValue({ ...val, }); }); } get name() { return this.productForm?.get('name'); } get price() { return this.productForm?.get('price'); } submit() { this.productSvc.put(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => console.error(err), }); } delete() { this.productSvc.delete(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/list/list.component.html ================================================

Product List

Name {{ element.name }} Price {{ element.price | currency }} SKU {{ element.sku }}
================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/list/list.component.scss ================================================ .product-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { productData: Product[] = []; isLoading: boolean = true; displayedColumns: string[] = ['name', 'price', 'sku']; constructor(private productSvc: ProductService, private router: Router) {} ngOnInit(): void { this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onEdit(product: Product) { this.router.navigate(['products', 'edit', product.productId]); return false; } onRemove(product: Product) { this.productSvc.delete(product); this.isLoading = true; this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onCreate() { this.router.navigate(['products', 'create']); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/models/product.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface Product { key: string; shardId: string; productId: string; name: string; price: number; sku: string; category: string; pictureUrl?: string; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/product.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Product } from './models/product.interface'; @Injectable({ providedIn: 'root', }) export class ProductService { constructor(private http: HttpClient) {} baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; fetch(): Observable { return this.http.get(`${this.baseUrl}/products`); } get(productId: string): Observable { const url = `${this.baseUrl}/product/${productId}`; return this.http.get(url); } delete(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.delete(url); } put(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.put(url, product); } post(product: Product) { return this.http.post(`${this.baseUrl}/product`, product); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/products-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'Product List', }, component: ListComponent, }, { path: 'create', data: { title: 'Create new Product', }, component: CreateComponent, }, { path: 'edit/:productId', data: { title: 'Edit Product', }, component: EditComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class ProductsRoutingModule {} ================================================ FILE: Solution/Lab5/client/Application/src/app/views/products/products.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ProductsRoutingModule } from './products-routing.module'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [CreateComponent, EditComponent, ListComponent], imports: [ CommonModule, ReactiveFormsModule, ProductsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class ProductsModule {} ================================================ FILE: Solution/Lab5/client/Application/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
================================================ FILE: Solution/Lab5/client/Application/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Solution/Lab5/client/Application/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Solution/Lab5/client/Application/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Solution/Lab5/client/Application/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Solution/Lab5/client/Application/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { find, mergeMap, defaultIfEmpty } from 'rxjs/operators'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.regApiGatewayUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } update(email: string, user: User) {} } ================================================ FILE: Solution/Lab5/client/Application/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab5/client/Application/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab5/client/Application/src/environments/environment.prod.ts ================================================ export const environment = { production: false, regApiGatewayUrl: 'https://ulp15c9bv2.execute-api.us-west-2.amazonaws.com/prod/', }; ================================================ FILE: Solution/Lab5/client/Application/src/environments/environment.ts ================================================ export const environment = { production: false, regApiGatewayUrl: 'https://ulp15c9bv2.execute-api.us-west-2.amazonaws.com/prod/', }; ================================================ FILE: Solution/Lab5/client/Application/src/index.html ================================================ Application ================================================ FILE: Solution/Lab5/client/Application/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab5/client/Application/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab5/client/Application/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab5/client/Application/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab5/client/Application/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "~bootstrap/scss/functions"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/maps"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; .chart-container canvas { max-height: 250px; width: auto; } .chart-container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; } ================================================ FILE: Solution/Lab5/client/Application/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab5/client/Application/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab5/client/Application/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "strictPropertyInitialization": false, "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab5/client/Application/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab5/client/Landing/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab5/client/Landing/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab5/client/Landing/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab5/client/Landing/README.md ================================================ # Landing This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab5/client/Landing/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "Landing": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/custom-theme.scss", "src/styles.scss", "node_modules/font-awesome/css/font-awesome.min.css" ] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "10kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "Landing:build:production" }, "development": { "browserTarget": "Landing:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "Landing:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab5/client/Landing/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab5/client/Landing/package.json ================================================ { "name": "landing", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "bootstrap": "~5.1.3", "font-awesome": "~4.7.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab5/client/Landing/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; export const routes: Routes = [ { path: '', redirectTo: 'landing', pathMatch: 'full', }, { path: 'landing', component: LandingComponent, }, { path: 'register', component: RegisterComponent, }, { path: '**', redirectTo: 'landing', pathMatch: 'full', }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab5/client/Landing/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab5/client/Landing/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'landing'; } ================================================ FILE: Solution/Lab5/client/Landing/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; @NgModule({ declarations: [AppComponent, LandingComponent, RegisterComponent], imports: [ AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, MatButtonModule, MatCardModule, MatIconModule, MatProgressSpinnerModule, MatSidenavModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, FormsModule, HttpClientModule, ReactiveFormsModule, MatSnackBarModule, MatInputModule, MatFormFieldModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab5/client/Landing/src/app/views/landing/landing.component.html ================================================

Serverless SaaS Reference Architecture

It's so nice it blows your mind.

Sign up now!

Serverless SaaS Reference Architecture is so awesome.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere temporibus omnis illum, officia. Architecto voluptatibus commodi voluptatem perspiciatis eos possimus, eius at molestias quaerat magnam? Odio qui quos ipsam natus.

Serverless SaaS Reference Architecture so awesome. Makes you awesome - go sign up!

Serverless SaaS Reference Architecture so great. Makes you even greater - go sign up now. Super cheap deal!

Feel lonely? Go sign up and have a friend!

Take Serverless SaaS Reference Architecture with you everywhere you go.

Serverless SaaS Reference Architecture is all you need. Anywhere - ever. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita sapiente hic voluptatum quo sunt totam accusamus distinctio minus aliquid quis!

Love Serverless SaaS Reference Architecture. So nice! So good! Could not live without!

Satisfied Customer

Reasons to sign up this product:

  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!
  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!

Why you still reading?

Sign up now!
================================================ FILE: Solution/Lab5/client/Landing/src/app/views/landing/landing.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // $color-red: #cc615f; $color-grey: #4b4b4b; $color-grey--light: #d3d3d3; $btn-amzn: #ec7211; $color-primary: #232f3e; $bp-s: 65.75em; //700px; $bp-xs: 34.375em; //550px; @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,800,300); *{ box-sizing: border-box; } html{ width: 100%; height:100%; margin: 0; padding: 0; } body{ width: 100%; height:100%; font-family: 'Open Sans','Helvetica Neue',Helvetica, sans-serif; font-size: 100%; line-height: 1.45; color: #141414; } a{ text-decoration: none; &:hover{ text-decoration: none; } } img{ max-width: 100%; } .btn{ display: inline-block; margin: 1rem 0; color: white; font-weight: 500; font-size: 1.3rem; background: $btn-amzn; letter-spacing: .02em; border: none; border-radius: 5px; padding: .8rem 1rem .9rem; text-shadow: 0 1px rgba(black,.3); box-shadow: 0 0 2px rgba(black,.2); @media (max-width: $bp-s){ padding: .5rem .7rem .6rem; font-size: 1rem; } &:hover{ background: lighten($btn-amzn,5%); color: #fff; } &:focus, &:active, &:focus:active{ background: darken($btn-amzn,5%); border-color: darken($btn-amzn,5%); box-shadow: 0 2px 5px 0 rgba(black,.5) inset; } } .container{ margin: 0 auto; width: 90%; max-width: 900px; } header{ color: white; background: #232f3e; padding: 10rem 0; text-align: center; position: relative; z-index: 1; overflow: hidden; @media (max-width: $bp-s){ padding: 2rem 0; } h1{ font-size: 3rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 2rem; } } h2{ font-weight: 300; font-size: 1.5rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } } section{ background: #fff; color: $color-grey; padding: 3.5rem 0; @media (max-width: $bp-s){ padding: 2rem 0; } &.section--primary{ background: $color-primary; color: #fff; } &.section--primary--alt{ background: desaturate(lighten($color-primary,15%),10%); color: #fff; } &.section--primary--light{ background: rgba($color-primary,.1); } &.section--grey{ background: $color-grey; color: #fff; } &.section--grey--light{ background: $color-grey--light; color: #fff; } h3{ text-align: center; font-size: 2rem; font-weight: 300; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } li{ font-size: 1.2rem; font-weight: 300; } p{ font-size: 1.2rem; font-weight: 300; } } .col{ margin: 0 1.5%; display: inline-block; vertical-align: top; } .col-7{ @extend .col; width: 64%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-3{ @extend .col; width: 29%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-5{ @extend .col; width: 30%; @media (max-width: $bp-xs){ width: 60%; margin: 0; } } .details{ text-align: left; h3{ font-size: 2rem; text-align: left; } } .features{ text-align: center; padding: 1rem; &:hover{ background: rgba(white,.1); } @media (max-width: $bp-s){ width: 100%; margin: 0; text-align: left; border-bottom: 1px solid rgba(white,.2); &:last-child{ border: none; } } i{ font-size: 4rem; margin: 0 0 2rem 0; @media (max-width: $bp-s){ font-size: 1.5rem; width: 2rem; text-align: center; margin: 0 0 1rem 0; float: left; } } p{ margin: 0 0 1rem 0; font-size: 1rem; @media (max-width: $bp-s){ margin-left: 3rem; } } } blockquote{ position: relative; margin: 0; padding: 0; text-align: center; &:before{ display: inline-block; color: $color-primary; font-size: 2rem; content: '\201C'; } p{ margin: 0; display: inline; font-size: 1.5rem; @media (max-width: $bp-s){ font-size: 1.2rem; } } cite{ font-size: 1rem; display: block; margin: .5rem 0 0 1.2rem; @media (max-width: $bp-s){ font-size: .8rem; } &:before{ content: '–'; } } } footer{ background: $color-primary; color: #fff; padding: 2rem 0; text-align: center; font-size: .8rem; color: rgba(white,.4); ul{ margin: 0; padding: 0; list-style: none; li{ display: inline-block; a{ display: block; padding: .4rem .7rem; font-size: .9rem; text-decoration: none; color: rgba(white,.7); &:hover{ color: white; } } } } } .text--center{ text-align: center; } .text--left{ text-align: left; } .bg-image{ background: $color-primary; text-align: center; position: relative; z-index: 1; overflow: hidden; //text-shadow: 2px 0 5px black; &:before{ content: ''; display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; z-index: -1; background-size: 100%; background-attachment: fixed; filter: blur(3px); opacity: .8; transform: scale(1.1); } &.bg-image-2:before{ opacity: .6; background-position: center center; } } ================================================ FILE: Solution/Lab5/client/Landing/src/app/views/landing/landing.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-landing', templateUrl: './landing.component.html', styleUrls: ['./landing.component.scss'], }) export class LandingComponent implements OnInit { constructor(private router: Router) {} ngOnInit() {} register() { this.router.navigate(['register']); return false; } } ================================================ FILE: Solution/Lab5/client/Landing/src/app/views/register/register.component.html ================================================
Provision a new Tenant Login details will be sent to the email provided. Name Email Phone Address Plan
================================================ FILE: Solution/Lab5/client/Landing/src/app/views/register/register.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .tenant-form { display: flex; align-items: center; justify-content: center; } .wide-form { min-width: 150px; max-width: 500px; width: 100%; } .full-width { width: 100%; } .mat-form-field { width: 100%; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab5/client/Landing/src/app/views/register/register.component.ts ================================================ import { MatSnackBar } from '@angular/material/snack-bar'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.scss'], }) export class RegisterComponent implements OnInit { submitting: boolean = false; tenantForm: FormGroup = this.fb.group({ tenantName: ['', [Validators.required]], tenantEmail: ['', [Validators.email, Validators.required]], tenantTier: ['', [Validators.required]], tenantPhone: [''], tenantAddress: [''], }); constructor( private fb: FormBuilder, private _snackBar: MatSnackBar, private http: HttpClient ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; this.tenantForm.disable(); const tenant = { ...this.tenantForm.value, }; this.http .post(`${environment.apiGatewayUrl}/registration`, tenant) .subscribe({ next: () => { this.openErrorMessageSnackBar('Successfully created new tenant!'); this.tenantForm.reset(); this.tenantForm.enable(); this.submitting = false; }, error: (err) => { this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); this.tenantForm.enable(); this.submitting = false; }, }); } } ================================================ FILE: Solution/Lab5/client/Landing/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab5/client/Landing/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab5/client/Landing/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab5/client/Landing/src/environments/environment.ts ================================================ export const environment = { production: false, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab5/client/Landing/src/index.html ================================================ Landing ================================================ FILE: Solution/Lab5/client/Landing/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab5/client/Landing/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab5/client/Landing/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab5/client/Landing/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab5/client/Landing/src/styles.scss ================================================ ================================================ FILE: Solution/Lab5/client/Landing/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab5/client/Landing/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab5/client/Landing/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab5/client/Landing/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab5/scripts/deploy-updates.sh ================================================ #!/bin/bash cd ../server || exit # stop execution if cd fails rm -rf .aws-sam/ python3 -m pylint -E -d E0401 $(find . -iname "*.py" -not -path "./.aws-sam/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi #Deploying shared services changes echo "Deploying shared services changes" echo Y | sam sync --stack-name serverless-saas -t shared-template.yaml --code --resource-id LambdaFunctions/CreateTenantAdminUserFunction --resource-id LambdaFunctions/ProvisionTenantFunction -u cd ../scripts || exit ./geturl.sh ================================================ FILE: Solution/Lab5/scripts/deployment.sh ================================================ #!/bin/bash if [[ "$#" -eq 0 ]]; then echo "Invalid parameters" echo "Command to deploy client code: deployment.sh -c" echo "Command to deploy bootstrap server code: deployment.sh -b" echo "Command to deploy CI/CD pipeline code: deployment.sh -p" echo "Command to deploy CI/CD pipeline, bootstrap & tenant server code: deployment.sh -s" echo "Command to deploy server & client code: deployment.sh -s -c" exit 1 fi while [[ "$#" -gt 0 ]]; do case $1 in -s) server=1 ;; -b) bootstrap=1 ;; -p) pipeline=1 ;; -c) client=1 ;; *) echo "Unknown parameter passed: $1"; exit 1 ;; esac shift done # During AWS hosted events using event engine tool # we pre-provision cloudfront and s3 buckets which hosts UI code. # So that it improves this labs total execution time. # Below code checks if cloudfront and s3 buckets are # pre-provisioned or not and then concludes if the workshop # is running in AWS hosted event through event engine tool or not. IS_RUNNING_IN_EVENT_ENGINE=false PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" IS_RUNNING_IN_EVENT_ENGINE=true ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_BUCKET=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSiteBucket'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) fi if [[ $server -eq 1 ]] || [[ $pipeline -eq 1 ]]; then echo "CI/CD pipeline code is getting deployed" #Create CodeCommit repo REGION=$(aws configure get region) REPO=$(aws codecommit get-repository --repository-name aws-serverless-saas-workshop) if [[ $? -ne 0 ]]; then echo "aws-serverless-saas-workshop codecommit repo is not present, will create one now" CREATE_REPO=$(aws codecommit create-repository --repository-name aws-serverless-saas-workshop --repository-description "Serverless SaaS workshop repository") echo $CREATE_REPO REPO_URL="codecommit::${REGION}://aws-serverless-saas-workshop" git remote add cc $REPO_URL if [[ $? -ne 0 ]]; then echo "Setting url to remote cc" git remote set-url cc $REPO_URL fi git push --set-upstream cc main fi #Deploying CI/CD pipeline cd ../server/TenantPipeline/ npm install && npm run build cdk bootstrap cdk deploy --require-approval never cd ../../scripts fi if [[ $server -eq 1 ]] || [[ $bootstrap -eq 1 ]]; then echo "Bootstrap server code is getting deployed" cd ../server REGION=$(aws configure get region) echo "Validating server code using pylint" python3 -m pylint -E -d E0401 $(find . -iname "*.py" -not -path "./.aws-sam/*" -not -path "./TenantPipeline/node_modules/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi sam build -t shared-template.yaml --use-container if [ "$IS_RUNNING_IN_EVENT_ENGINE" = true ]; then sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE AdminUserPoolCallbackURLParameter=$ADMIN_SITE_URL TenantUserPoolCallbackURLParameter=$APP_SITE_URL else sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE fi cd ../scripts fi if [ "$IS_RUNNING_IN_EVENT_ENGINE" = false ]; then ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_BUCKET=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSiteBucket'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi if [[ $client -eq 1 ]]; then echo "Client code is getting deployed" ADMIN_APIGATEWAYURL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminApi'].OutputValue" --output text) # Admin UI and Landing UI are configured in Lab2 echo "Admin UI and Landing UI are configured in Lab2. Only App UI will be reconfigured in this Lab5." # Configuring app UI echo "aws s3 ls s3://$APP_SITE_BUCKET" aws s3 ls s3://$APP_SITE_BUCKET if [ $? -ne 0 ]; then echo "Error! S3 Bucket: $APP_SITE_BUCKET not readable" exit 1 fi cd ../client/Application echo "Configuring environment for App Client" cat << EoF > ./src/environments/environment.prod.ts export const environment = { production: true, regApiGatewayUrl: '$ADMIN_APIGATEWAYURL' }; EoF cat << EoF > ./src/environments/environment.ts export const environment = { production: true, regApiGatewayUrl: '$ADMIN_APIGATEWAYURL' }; EoF npm install --legacy-peer-deps && npm run build echo "aws s3 sync --delete --cache-control no-store dist s3://$APP_SITE_BUCKET" aws s3 sync --delete --cache-control no-store dist s3://$APP_SITE_BUCKET if [[ $? -ne 0 ]]; then exit 1 fi echo "Completed configuring environment for App Client" echo "Successfully completed redeploying Application UI" fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Solution/Lab5/scripts/geturl.sh ================================================ PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) else ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Solution/Lab5/server/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Solution/Lab5/server/OrderService/order_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Order: key='' def __init__(self, shardId, orderId, orderName, orderProducts): self.shardId = shardId self.orderId = orderId self.key = shardId + ':' + orderId self.orderName = orderName self.orderProducts = orderProducts class OrderProduct: def __init__(self, productId, price, quantity): self.productId = productId self.price = price self.quantity = quantity ================================================ FILE: Solution/Lab5/server/OrderService/order_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import order_service_dal from decimal import Decimal from types import SimpleNamespace from aws_lambda_powertools import Tracer tracer = Tracer() @tracer.capture_lambda_handler def get_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a order") params = event['pathParameters'] key = params['id'] logger.log_with_tenant_context(event, params) order = order_service_dal.get_order(event, key) logger.log_with_tenant_context(event, "Request completed to get a order") metrics_manager.record_metric(event, "SingleOrderRequested", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def create_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) order = order_service_dal.create_order(event, payload) logger.log_with_tenant_context(event, "Request completed to create a order") metrics_manager.record_metric(event, "OrderCreated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def update_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] order = order_service_dal.update_order(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a order") metrics_manager.record_metric(event, "OrderUpdated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def delete_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a order") params = event['pathParameters'] key = params['id'] response = order_service_dal.delete_order(event, key) logger.log_with_tenant_context(event, "Request completed to delete a order") metrics_manager.record_metric(event, "OrderDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the order") @tracer.capture_lambda_handler def get_orders(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all orders") response = order_service_dal.get_orders(event, tenantId) metrics_manager.record_metric(event, "OrdersRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all orders") return utils.generate_response(response) ================================================ FILE: Solution/Lab5/server/OrderService/order_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid from order_models import Order import json import utils from types import SimpleNamespace import logger import random import threading from boto3.dynamodb.conditions import Key import metrics_manager is_pooled_deploy = os.environ['IS_POOLED_DEPLOY'] table_name = os.environ['ORDER_TABLE_NAME'] dynamodb = None suffix_start = 1 suffix_end = 10 def get_order(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) response = table.get_item(Key={'shardId': shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a order', e) else: return order def delete_order(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a order', e) else: logger.info("DeleteItem succeeded:") return response def create_order(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] table = __get_dynamodb_table(event, dynamodb) suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) order = Order(shardId, str(uuid.uuid4()), payload.orderName, payload.orderProducts) try: response = table.put_item(Item={ 'shardId':shardId, 'orderId': order.orderId, 'orderName': order.orderName, 'orderProducts': get_order_products_dict(order.orderProducts) }, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a order', e) else: logger.info("PutItem succeeded:") return order def update_order(event, payload, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) order = Order(shardId, orderId,payload.orderName, payload.orderProducts) response = table.update_item(Key={'shardId':order.shardId, 'orderId': order.orderId}, UpdateExpression="set orderName=:orderName, " +"orderProducts=:orderProducts", ExpressionAttributeValues={ ':orderName': order.orderName, ':orderProducts': get_order_products_dict(order.orderProducts) }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a order', e) else: logger.info("UpdateItem succeeded:") return order def get_orders(event, tenantId): table = __get_dynamodb_table(event, dynamodb) get_all_products_response = [] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error("Error getting all orders") raise Exception('Error getting all orders', e) else: logger.info("Get orders succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) get_all_products_response.append(order) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def __get_dynamodb_table(event, dynamodb): """ Determine the table name based upo pooled vs silo model Args: event ([type]): [description] Returns: [type]: [description] """ if (is_pooled_deploy=='true'): accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken ) else: if not dynamodb: dynamodb = boto3.resource('dynamodb') return dynamodb.Table(table_name) def get_order_products_dict(orderProducts): orderProductList = [] for i in range(len(orderProducts)): product = orderProducts[i] orderProductList.append(vars(product)) return orderProductList ================================================ FILE: Solution/Lab5/server/OrderService/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab5/server/ProductService/product_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Product: key ='' def __init__(self, shardId, productId, sku, name, price, category): self.shardId = shardId self.productId = productId self.key = shardId + ':' + productId self.sku = sku self.name = name self.price = price self.category = category class Category: def __init__(self, id, name): self.id = id self.name = name ================================================ FILE: Solution/Lab5/server/ProductService/product_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import product_service_dal from aws_lambda_powertools import Tracer from decimal import Decimal from types import SimpleNamespace tracer = Tracer() @tracer.capture_lambda_handler def get_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a product") params = event['pathParameters'] logger.log_with_tenant_context(event, params) key = params['id'] logger.log_with_tenant_context(event, key) product = product_service_dal.get_product(event, key) logger.log_with_tenant_context(event, "Request completed to get a product") metrics_manager.record_metric(event, "SingleProductRequested", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def create_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) product = product_service_dal.create_product(event, payload) logger.log_with_tenant_context(event, "Request completed to create a product") metrics_manager.record_metric(event, "ProductCreated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def update_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] product = product_service_dal.update_product(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a product") metrics_manager.record_metric(event, "ProductUpdated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def delete_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a product") params = event['pathParameters'] key = params['id'] response = product_service_dal.delete_product(event, key) logger.log_with_tenant_context(event, "Request completed to delete a product") metrics_manager.record_metric(event, "ProductDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the product") @tracer.capture_lambda_handler def get_products(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all products") response = product_service_dal.get_products(event, tenantId) metrics_manager.record_metric(event, "ProductsRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all products") return utils.generate_response(response) ================================================ FILE: Solution/Lab5/server/ProductService/product_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid import json import logger import random import threading import metrics_manager from product_models import Product from types import SimpleNamespace from boto3.dynamodb.conditions import Key is_pooled_deploy = os.environ['IS_POOLED_DEPLOY'] table_name = os.environ['PRODUCT_TABLE_NAME'] dynamodb = None suffix_start = 1 suffix_end = 10 def get_product(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) response = table.get_item(Key={'shardId': shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a product', e) else: logger.info("GetItem succeeded:"+ str(product)) return product def delete_product(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a product', e) else: logger.info("DeleteItem succeeded:") return response def create_product(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] table = __get_dynamodb_table(event, dynamodb) suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) product = Product(shardId, str(uuid.uuid4()), payload.sku,payload.name, payload.price, payload.category) try: response = table.put_item( Item= { 'shardId': shardId, 'productId': product.productId, 'sku': product.sku, 'name': product.name, 'price': product.price, 'category': product.category }, ReturnConsumedCapacity='TOTAL' ) metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: logger.info("PutItem succeeded:") return product def update_product(event, payload, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) product = Product(shardId,productId,payload.sku, payload.name, payload.price, payload.category) response = table.update_item(Key={'shardId':product.shardId, 'productId': product.productId}, UpdateExpression="set sku=:sku, #n=:productName, price=:price, category=:category", ExpressionAttributeNames= {'#n':'name'}, ExpressionAttributeValues={ ':sku': product.sku, ':productName': product.name, ':price': product.price, ':category': product.category }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a product', e) else: logger.info("UpdateItem succeeded:") return product def get_products(event, tenantId): table = __get_dynamodb_table(event, dynamodb) get_all_products_response =[] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting all products', e) else: logger.info("Get products succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) get_all_products_response.append(product) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def __get_dynamodb_table(event, dynamodb): if (is_pooled_deploy=='true'): accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken ) else: if not dynamodb: dynamodb = boto3.resource('dynamodb') return dynamodb.Table(table_name) ================================================ FILE: Solution/Lab5/server/ProductService/requirements.txt ================================================ requests pytest-mock aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab5/server/README.md ================================================ sam build -t shared-template.yaml --use-container sam deploy --config-file shared-samconfig.toml sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml ================================================ FILE: Solution/Lab5/server/Resources/requirements.txt ================================================ python-jose[cryptography] ================================================ FILE: Solution/Lab5/server/Resources/shared_service_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import auth_manager import utils region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) if(auth_manager.isSaaSProvider(unauthorized_claims['custom:userRole'])): userpool_id = user_pool_operation_user appclient_id = app_client_operation_user else: #get tenant user pool and app client to validate jwt token against tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': unauthorized_claims['custom:tenantId'] } ) logger.info(tenant_details) userpool_id = tenant_details['Item']['userPoolId'] appclient_id = tenant_details['Item']['appClientId'] #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] #only tenant admin and system admin can do certain actions like create and disable users if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): policy.allowAllMethods() if (auth_manager.isTenantAdmin(user_role)): policy.denyMethod(HttpVerb.POST, "tenant-activation") policy.denyMethod(HttpVerb.GET, "tenants") else: #if not tenant admin or system admin then only allow to get info and update info policy.allowMethod(HttpVerb.GET, "user/*") policy.allowMethod(HttpVerb.PUT, "user/*") authResponse = policy.build() # Generate STS credentials to be used for FGAC # Important Note: # We are generating STS token inside Authorizer to take advantage of the caching behavior of authorizer # Another option is to generate the STS token inside the lambda function itself, as mentioned in this blog post: https://aws.amazon.com/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/ # Finally, you can also consider creating one Authorizer per microservice in cases where you want the IAM policy specific to that service iam_policy = auth_manager.getPolicyForUser(user_role, utils.Service_Identifier.SHARED_SERVICES.value, tenant_id, region, aws_account_id) logger.info(iam_policy) role_arn = "arn:aws:iam::{}:role/authorizer-access-role".format(aws_account_id) assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName="tenant-aware-session", Policy=iam_policy, ) credentials = assumed_role["Credentials"] #pass sts credentials to lambda context = { 'accesskey': credentials['AccessKeyId'], # $context.authorizer.key -> value 'secretkey' : credentials['SecretAccessKey'], 'sessiontoken' : credentials["SessionToken"], 'userName': user_name, 'tenantId': tenant_id, 'userPoolId': userpool_id, 'userRole': user_role } authResponse['context'] = context return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Solution/Lab5/server/Resources/tenant_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import auth_manager import utils region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) if(auth_manager.isSaaSProvider(unauthorized_claims['custom:userRole'])): userpool_id = user_pool_operation_user appclient_id = app_client_operation_user else: #get tenant user pool and app client to validate jwt token against tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': unauthorized_claims['custom:tenantId'] } ) logger.info(tenant_details) userpool_id = tenant_details['Item']['userPoolId'] appclient_id = tenant_details['Item']['appClientId'] apigateway_url = tenant_details['Item']['apiGatewayUrl'] #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] if (auth_manager.isSaaSProvider(user_role) == False): if (isTenantAuthorizedForThisAPI(apigateway_url, api_gateway_arn_tmp[0]) == False): logger.error('Unauthorized') raise Exception('Unauthorized') #roles are not fine-grained enough to allow selectively policy.allowAllMethods() authResponse = policy.build() # Generate STS credentials to be used for FGAC # Important Note: # We are generating STS token inside Authorizer to take advantage of the caching behavior of authorizer # Another option is to generate the STS token inside the lambda function itself, as mentioned in this blog post: https://aws.amazon.com/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/ # Finally, you can also consider creating one Authorizer per microservice in cases where you want the IAM policy specific to that service iam_policy = auth_manager.getPolicyForUser(user_role, utils.Service_Identifier.BUSINESS_SERVICES.value, tenant_id, region, aws_account_id) logger.info(iam_policy) role_arn = "arn:aws:iam::{}:role/authorizer-access-role".format(aws_account_id) assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName="tenant-aware-session", Policy=iam_policy, ) credentials = assumed_role["Credentials"] #pass sts credentials to lambda context = { 'accesskey': credentials['AccessKeyId'], # $context.authorizer.key -> value 'secretkey' : credentials['SecretAccessKey'], 'sessiontoken' : credentials["SessionToken"], 'userName': user_name, 'tenantId': tenant_id, 'userPoolId': userpool_id, 'userRole': user_role } authResponse['context'] = context return authResponse def isTenantAuthorizedForThisAPI(apigateway_url, current_api_id): if(apigateway_url.split('.')[0] != 'https://' + current_api_id): return False else: return True def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Solution/Lab5/server/TenantManagementService/events/env.json ================================================ { "CreateTenantAdminUserFunction": { "DEFAULT_USER_POOL_ID": "us-west-2_uliP336sh", "LOG_LEVEL": "INFO" }, "RegisterTenantFunction": { "CREATE_TENANT_ADMIN_USER_FUNCTION": "arn:aws:lambda:us-west-2:779954754415:function:serverless-saas-admin-CreateTenantAdminUserFunctio-D5K1EGEMC4QG", "CREATE_TENANT_FUNCTION": "arn:aws:lambda:us-west-2:779954754415:function:serverless-saas-admin-CreateTenantFunction-13GSSCVNKTV71", "PREMIUM_TIER_API_KEY": "yy", "STANDARD_TIER_API_KEY": "xx", "BASIC_TIER_API_KEY": "zz", "LOG_LEVEL": "DEBUG" } } ================================================ FILE: Solution/Lab5/server/TenantManagementService/events/tenant-registration.json ================================================ { "body": "{\"tenantName\": \"First Tenant\", \"tenantAddress\": \"123 St\", \"tenantEmail\": \"a@a.com\", \"tenantPhone\": \"1234567890\", \"tenantTier\": \"Standard\", \"tenantId\": \"62d1f8793b5e11eb876\", \"apiKey\": \"62d2062a3b5e11eb839457148f07121x\"}"} ================================================ FILE: Solution/Lab5/server/TenantManagementService/events/update_users_apikey_by_tenant.json ================================================ { "body": "{\"tenantId\": \"94d1bc6976ef11eb80cb81ceaed21f3f\", \"userPoolId\": \"us-west-2_vvN1hB5pd\", \"apiKey\": \"6db2bdc2-6d96-11eb-a56f-38f9d33cfd0f\"}" } ================================================ FILE: Solution/Lab5/server/TenantManagementService/events/user-management.json ================================================ { "tenantName": "First Tenant", "tenantAddress": "123 St", "tenantEmail": "a@a.com", "tenantPhone": "1234567890", "tenantTier": "Standard", "dedicatedTenancy": "true", "tenantId": "62d1f8793b5e11eb876", "apiKey": "62d2062a3b5e11eb839457148f07121x" } ================================================ FILE: Solution/Lab5/server/TenantManagementService/requirements.txt ================================================ requests aws_requests_auth ================================================ FILE: Solution/Lab5/server/TenantManagementService/tenant-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import json import boto3 from boto3.dynamodb.conditions import Key import urllib.parse import utils from botocore.exceptions import ClientError import logger import metrics_manager import auth_manager import requests from aws_requests_auth.aws_auth import AWSRequestsAuth from aws_lambda_powertools import Tracer tracer = Tracer() region = os.environ['AWS_REGION'] #This method has been locked down to be only def create_tenant(event, context): api_gateway_url = '' tenant_details = json.loads(event['body']) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails')#TODO: read table names from env vars table_system_settings = dynamodb.Table('ServerlessSaaS-Settings') try: # for pooled tenants the apigateway url is saving in settings during stack creation # update from there during tenant creation if(tenant_details['dedicatedTenancy'].lower()!= 'true'): settings_response = table_system_settings.get_item( Key={ 'settingName': 'apiGatewayUrl-Pooled' } ) api_gateway_url = settings_response['Item']['settingValue'] response = table_tenant_details.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'tenantName' : tenant_details['tenantName'], 'tenantAddress': tenant_details['tenantAddress'], 'tenantEmail': tenant_details['tenantEmail'], 'tenantPhone': tenant_details['tenantPhone'], 'tenantTier': tenant_details['tenantTier'], 'userPoolId': tenant_details['userPoolId'], 'appClientId': tenant_details['appClientId'], 'dedicatedTenancy': tenant_details['dedicatedTenancy'], 'isActive': True, 'apiGatewayUrl': api_gateway_url } ) except Exception as e: raise Exception('Error creating a new tenant', e) else: return utils.create_success_response("Tenant Created") def get_tenants(event, context): table_tenant_details = __getTenantManagementTable(event) try: response = table_tenant_details.scan() except Exception as e: raise Exception('Error getting all tenants', e) else: return utils.generate_response(response['Items']) @tracer.capture_lambda_handler def update_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_details = json.loads(event['body']) tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response_update = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set tenantName = :tenantName, tenantAddress = :tenantAddress, tenantEmail = :tenantEmail, tenantPhone = :tenantPhone, tenantTier=:tenantTier", ExpressionAttributeValues={ ':tenantName' : tenant_details['tenantName'], ':tenantAddress': tenant_details['tenantAddress'], ':tenantEmail': tenant_details['tenantEmail'], ':tenantPhone': tenant_details['tenantPhone'], ':tenantTier': tenant_details['tenantTier'] }, ReturnValues="UPDATED_NEW" ) logger.log_with_tenant_context(event, response_update) logger.log_with_tenant_context(event, "Request completed to update tenant") return utils.create_success_response("Tenant Updated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get tenant details") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): tenant_details = table_tenant_details.get_item( Key={ 'tenantId': tenant_id, }, AttributesToGet=[ 'tenantName', 'tenantAddress', 'tenantEmail', 'tenantPhone' ] ) item = tenant_details['Item'] tenant_info = TenantInfo(item['tenantName'], item['tenantAddress'],item['tenantEmail'], item['tenantPhone']) logger.log_with_tenant_context(event, tenant_info) logger.log_with_tenant_context(event, "Request completed to get tenant details") return utils.create_success_response(tenant_info.__dict__) else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def deactivate_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) url_disable_users = os.environ['DISABLE_USERS_BY_TENANT'] url_deprovision_tenant = os.environ['DEPROVISION_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to deactivate tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': False }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) if (response["Attributes"]["dedicatedTenancy"].upper() == "TRUE"): update_details = {} update_details['tenantId'] = tenant_id update_user_response = __invoke_deprovision_tenant(update_details, headers, auth, host, stage_name, url_deprovision_tenant) update_details = {} update_details['userPoolId'] = response["Attributes"]['userPoolId'] update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_disable_users(update_details, headers, auth, host, stage_name, url_disable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to deactivate tenant") return utils.create_success_response("Tenant Deactivated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def activate_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) url_enable_users = os.environ['ENABLE_USERS_BY_TENANT'] url_provision_tenant = os.environ['PROVISION_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to activate tenant") if (auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': True }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) if (response["Attributes"]["dedicatedTenancy"].upper() == "TRUE"): update_details = {} update_details['tenantId'] = tenant_id provision_response = __invoke_provision_tenant(update_details, headers, auth, host, stage_name, url_provision_tenant) logger.log_with_tenant_context(event, provision_response) update_details = {} update_details['userPoolId'] = response["Attributes"]['userPoolId'] update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_enable_users(update_details, headers, auth, host, stage_name, url_enable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to activate tenant") return utils.create_success_response("Tenant Activated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only system admin can activate tenant!") return utils.create_unauthorized_response() def load_tenant_config(event, context): params = event['pathParameters'] tenantName = urllib.parse.unquote(params['tenantname']) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails')#TODO: read table names from env vars try: response = table_tenant_details.query( IndexName="ServerlessSaas-TenantConfig", KeyConditionExpression=Key('tenantName').eq(tenantName), ProjectionExpression="userPoolId, appClientId, apiGatewayUrl" ) except Exception as e: raise Exception('Error getting tenant config', e) else: if (response['Count'] == 0): return utils.create_notfound_response("Tenant not found."+ "Please enter exact tenant name used during tenant registration.") else: return utils.generate_response(response['Items'][0]) def __invoke_disable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url]) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while disabling users for the tenant') except Exception as e: logger.error('Error occured while disabling users for the tenant') raise Exception('Error occured while disabling users for the tenant', e) else: return "Success invoking disable users" def __invoke_deprovision_tenant(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url + update_details['tenantId']]) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while deprovisioning tenant') except Exception as e: logger.error('Error occured while deprovisioning tenant') raise Exception('Error occured while deprovisioning tenant', e) else: return "Success invoking deprovision tenant" def __invoke_enable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url]) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while enabling users for the tenant') except Exception as e: logger.error('Error occured while enabling users for the tenant') raise Exception('Error occured while enabling users for the tenant', e) else: return "Success invoking enable users" def __invoke_provision_tenant(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url]) response = requests.post(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while provisioning tenant') except Exception as e: logger.error('Error occured while provisioning tenant') raise Exception('Error occured while provisioning tenant', e) else: return "Success invoking provision tenant" def __getTenantManagementTable(event): accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken) table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails')#TODO: read table names from env vars return table_tenant_details class TenantInfo: def __init__(self, tenant_name, tenant_address, tenant_email, tenant_phone): self.tenant_name = tenant_name self.tenant_address = tenant_address self.tenant_email = tenant_email self.tenant_phone = tenant_phone ================================================ FILE: Solution/Lab5/server/TenantManagementService/tenant-provisioning.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import utils from botocore.exceptions import ClientError import logger import os from aws_lambda_powertools import Tracer tracer = Tracer() tenant_stack_mapping_table_name = os.environ['TENANT_STACK_MAPPING_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') codepipeline = boto3.client('codepipeline') cloudformation = boto3.client('cloudformation') table_tenant_stack_mapping = dynamodb.Table(tenant_stack_mapping_table_name) stack_name = 'stack-{0}' @tracer.capture_lambda_handler def provision_tenant(event, context): tenant_details = json.loads(event['body']) try: response_ddb = table_tenant_stack_mapping.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'stackName': stack_name.format(tenant_details['tenantId']), 'applyLatestRelease': True, 'codeCommitId': '' } ) logger.info(response_ddb) response_codepipeline = codepipeline.start_pipeline_execution( name='serverless-saas-pipeline' ) logger.info(response_ddb) except Exception as e: raise else: return utils.create_success_response("Tenant Provisioning Started") @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def deprovision_tenant(event, context): logger.info("Request received to deprovision a tenant") tenantid_to_deprovision = event['tenantId'] try: response_ddb = table_tenant_stack_mapping.delete_item( Key={ 'tenantId': tenantid_to_deprovision } ) logger.info(response_ddb) response_cloudformation = cloudformation.delete_stack( StackName=stack_name.format(tenantid_to_deprovision) ) logger.info(response_cloudformation) except Exception as e: raise else: return utils.create_success_response("Tenant Deprovisioning Started") ================================================ FILE: Solution/Lab5/server/TenantManagementService/tenant-registration.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import utils import uuid import logger import requests import re region = os.environ['AWS_REGION'] create_tenant_admin_user_resource_path = os.environ['CREATE_TENANT_ADMIN_USER_RESOURCE_PATH'] create_tenant_resource_path = os.environ['CREATE_TENANT_RESOURCE_PATH'] provision_tenant_resource_path = os.environ['PROVISION_TENANT_RESOURCE_PATH'] lambda_client = boto3.client('lambda') def register_tenant(event, context): try: tenant_id = uuid.uuid1().hex tenant_details = json.loads(event['body']) tenant_details['dedicatedTenancy'] = 'false' if (tenant_details['tenantTier'].upper() == utils.TenantTier.PLATINUM.value.upper()): tenant_details['dedicatedTenancy'] = 'true' tenant_details['tenantId'] = tenant_id logger.info(tenant_details) stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) create_user_response = __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name) logger.info (create_user_response) tenant_details['userPoolId'] = create_user_response['message']['userPoolId'] tenant_details['appClientId'] = create_user_response['message']['appClientId'] tenant_details['tenantAdminUserName'] = create_user_response['message']['tenantAdminUserName'] create_tenant_response = __create_tenant(tenant_details, headers, auth, host, stage_name) logger.info (create_tenant_response) if (tenant_details['dedicatedTenancy'].upper() == 'TRUE'): provision_tenant_response = __provision_tenant(tenant_details, headers, auth, host, stage_name) logger.info(provision_tenant_response) except Exception as e: logger.error('Error registering a new tenant') raise Exception('Error registering a new tenant', e) else: return utils.create_success_response("You have been registered in our system") def __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_admin_user_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while calling the create tenant admin user service') raise Exception('Error occured while calling the create tenant admin user service', e) else: return response_json def __create_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_resource_path]) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while creating the tenant record in table') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json def __provision_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, provision_tenant_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json()['message'] except Exception as e: logger.error('Error occured while provisioning the tenant') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json ================================================ FILE: Solution/Lab5/server/TenantManagementService/user-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import sys import logger import utils import metrics_manager import auth_manager from boto3.dynamodb.conditions import Key from aws_lambda_powertools import Tracer tracer = Tracer() client = boto3.client('cognito-idp') dynamodb = boto3.resource('dynamodb') table_tenant_user_map = dynamodb.Table('ServerlessSaaS-TenantUserMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') def create_tenant_admin_user(event, context): tenant_user_pool_id = os.environ['TENANT_USER_POOL_ID'] tenant_app_client_id = os.environ['TENANT_APP_CLIENT_ID'] tenant_details = json.loads(event['body']) tenant_id = tenant_details['tenantId'] logger.info(tenant_details) user_mgmt = UserManagement() if (tenant_details['dedicatedTenancy'] == 'true'): user_pool_response = user_mgmt.create_user_pool(tenant_id) user_pool_id = user_pool_response['UserPool']['Id'] logger.info (user_pool_id) app_client_response = user_mgmt.create_user_pool_client(user_pool_id) logger.info(app_client_response) app_client_id = app_client_response['UserPoolClient']['ClientId'] user_pool_domain_response = user_mgmt.create_user_pool_domain(user_pool_id, tenant_id) logger.info ("New Tenant Created") else: user_pool_id = tenant_user_pool_id app_client_id = tenant_app_client_id #Add tenant admin now based upon user pool tenant_user_group_response = user_mgmt.create_user_group(user_pool_id,tenant_id,"User group for tenant {0}".format(tenant_id)) tenant_admin_user_name = 'tenant-admin-{0}'.format(tenant_details['tenantId']) create_tenant_admin_response = user_mgmt.create_tenant_admin(user_pool_id, tenant_admin_user_name, tenant_details) add_tenant_admin_to_group_response = user_mgmt.add_user_to_group(user_pool_id, tenant_admin_user_name, tenant_user_group_response['Group']['GroupName']) tenant_user_mapping_response = user_mgmt.create_user_tenant_mapping(tenant_admin_user_name,tenant_id) response = {"userPoolId": user_pool_id, "appClientId": app_client_id, "tenantAdminUserName": tenant_admin_user_name} return utils.create_success_response(response) @tracer.capture_lambda_handler #only tenant admin can create users def create_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to create new user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = user_details['tenantId'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] else: user_tenant_id = tenant_id if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): metrics_manager.record_metric(event, "UserCreated", "Count", 1) response = client.admin_create_user( Username=user_details['userName'], UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] }, { 'Name': 'custom:tenantId', 'Value': user_tenant_id } ] ) logger.log_with_tenant_context(event, response) user_mgmt = UserManagement() user_mgmt.add_user_to_group(user_pool_id, user_details['userName'], user_tenant_id) response_mapping = user_mgmt.create_user_tenant_mapping(user_details['userName'], user_tenant_id) logger.log_with_tenant_context(event, "Request completed to create new user") return utils.create_success_response("New user created") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can create user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_users(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] users = [] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get users") if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): response = client.list_users( UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) num_of_users = len(response['Users']) metrics_manager.record_metric(event, "Number of users", "Count", num_of_users) if (num_of_users > 0): for user in response['Users']: is_same_tenant_user = False user_info = UserInfo() for attr in user["Attributes"]: if(attr["Name"] == "custom:tenantId" and attr["Value"] == tenant_id): is_same_tenant_user = True user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] if(is_same_tenant_user): user_info.enabled = user["Enabled"] user_info.created = user["UserCreateDate"] user_info.modified = user["UserLastModifiedDate"] user_info.status = user["UserStatus"] user_info.user_name = user["Username"] users.append(user_info) return utils.generate_response(users) else: logger.log_with_tenant_context(event, "Request completed as unauthorized.") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = event['queryStringParameters']['tenantid'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] if (auth_manager.isTenantUser(user_role) and user_name != requesting_user_name): logger.log_with_tenant_context(event, "Request completed as unauthorized. User can only get its information.") return utils.create_unauthorized_response() else: user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: logger.log_with_tenant_context(event, "Request completed to get new user") return utils.create_success_response(user_info.__dict__) @tracer.capture_lambda_handler def update_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = user_details['tenantId'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] if (auth_manager.isTenantUser(user_role)): logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update user!") return utils.create_unauthorized_response() else: user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserUpdated", "Count", 1) response = client.admin_update_user_attributes( Username=user_name, UserPoolId=user_pool_id, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] } ] ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to update user") return utils.create_success_response("user updated") @tracer.capture_lambda_handler def disable_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to disable new user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = event['queryStringParameters']['tenantid'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserDisabled", "Count", 1) response = client.admin_disable_user( Username=user_name, UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to disable new user") return utils.create_success_response("User disabled") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can disable user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def disable_users_by_tenant(event, context): logger.info("Request received to disable users by tenant") tenantid_to_update = event['tenantId'] tenant_user_pool_id = event['userPoolId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if ((auth_manager.isTenantAdmin(user_role) and tenantid_to_update == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_disable_user( Username=user['userName'], UserPoolId=tenant_user_pool_id ) logger.info(response) logger.info("Request completed to disable users") return utils.create_success_response("Users disabled") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def enable_users_by_tenant(event, context): logger.info("Request received to enable users by tenant") tenantid_to_update = event['tenantId'] tenant_user_pool_id = event['userPoolId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if (auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_enable_user( Username=user['userName'], UserPoolId=tenant_user_pool_id ) logger.info(response) logger.info("Request completed to enable users") return utils.create_success_response("Users enables") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() def get_user_info(event, user_pool_id, user_name): metrics_manager.record_metric(event, "UserInfoRequested", "Count", 1) response = client.admin_get_user( UserPoolId=user_pool_id, Username=user_name ) logger.log_with_tenant_context(event, response) user_info = UserInfo() user_info.user_name = response["Username"] for attr in response["UserAttributes"]: if(attr["Name"] == "custom:tenantId"): user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] logger.log_with_tenant_context(event, user_info) return user_info class UserManagement: def create_user_pool(self, tenant_id): application_site_url = os.environ['TENANT_USER_POOL_CALLBACK_URL'] email_message = ''.join(["Login into tenant UI application at ", application_site_url, " with username {username} and temporary password {####}"]) email_subject = "Your temporary password for tenant UI application" response = client.create_user_pool( PoolName= tenant_id + '-ServerlessSaaSUserPool', AutoVerifiedAttributes=['email'], AccountRecoverySetting={ 'RecoveryMechanisms': [ { 'Priority': 1, 'Name': 'verified_email' }, ] }, Schema=[ { 'Name': 'email', 'AttributeDataType': 'String', 'Required': True, }, { 'Name': 'tenantId', 'AttributeDataType': 'String', 'Required': False, }, { 'Name': 'userRole', 'AttributeDataType': 'String', 'Required': False, } ], AdminCreateUserConfig={ 'InviteMessageTemplate': { 'EmailMessage': email_message, 'EmailSubject': email_subject } } ) return response def create_user_pool_client(self, user_pool_id): user_pool_callback_url = os.environ['TENANT_USER_POOL_CALLBACK_URL'] response = client.create_user_pool_client( UserPoolId= user_pool_id, ClientName= 'ServerlessSaaSClient', GenerateSecret= False, AllowedOAuthFlowsUserPoolClient= True, AllowedOAuthFlows=[ 'code', 'implicit' ], SupportedIdentityProviders=[ 'COGNITO', ], CallbackURLs=[ user_pool_callback_url, ], LogoutURLs= [ user_pool_callback_url, ], AllowedOAuthScopes=[ 'email', 'openid', 'profile' ], WriteAttributes=[ 'email', 'custom:tenantId' ] ) return response def create_user_pool_domain(self, user_pool_id, tenant_id): response = client.create_user_pool_domain( Domain= tenant_id + '-serverlesssaas', UserPoolId=user_pool_id ) return response def create_user_group(self, user_pool_id, group_name, group_description): response = client.create_group( GroupName=group_name, UserPoolId=user_pool_id, Description= group_description, Precedence=0 ) return response def create_tenant_admin(self, user_pool_id, tenant_admin_user_name, user_details): response = client.admin_create_user( Username=tenant_admin_user_name, UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['tenantEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': 'TenantAdmin' }, { 'Name': 'custom:tenantId', 'Value': user_details['tenantId'] } ] ) return response def add_user_to_group(self, user_pool_id, user_name, group_name): response = client.admin_add_user_to_group( UserPoolId=user_pool_id, Username=user_name, GroupName=group_name ) return response def create_user_tenant_mapping(self, user_name, tenant_id): response = table_tenant_user_map.put_item( Item={ 'tenantId': tenant_id, 'userName': user_name } ) return response class UserInfo: def __init__(self, user_name=None, tenant_id=None, user_role=None, email=None, status=None, enabled=None, created=None, modified=None): self.user_name = user_name self.tenant_id = tenant_id self.user_role = user_role self.email = email self.status = status self.enabled = enabled self.created = created self.modified = modified ================================================ FILE: Solution/Lab5/server/TenantPipeline/.gitignore ================================================ *.js !jest.config.js *.d.ts node_modules # CDK asset staging directory .cdk.staging cdk.out # Parcel default cache directory .parcel-cache ================================================ FILE: Solution/Lab5/server/TenantPipeline/.npmignore ================================================ *.ts !*.d.ts # CDK asset staging directory .cdk.staging cdk.out ================================================ FILE: Solution/Lab5/server/TenantPipeline/README.md ================================================ # Welcome to your CDK TypeScript project! This is a blank project for TypeScript development with CDK. The `cdk.json` file tells the CDK Toolkit how to execute your app. ## Useful commands * `npm run build` compile typescript to js * `npm run watch` watch for changes and compile * `npm run test` perform the jest unit tests * `cdk deploy` deploy this stack to your default AWS account/region * `cdk diff` compare deployed stack with current state * `cdk synth` emits the synthesized CloudFormation template ================================================ FILE: Solution/Lab5/server/TenantPipeline/bin/pipeline.ts ================================================ #!/usr/bin/env node import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; import { ServerlessSaaSStack } from '../lib/serverless-saas-stack'; const app = new cdk.App(); new ServerlessSaaSStack(app, 'serverless-saas-pipeline'); ================================================ FILE: Solution/Lab5/server/TenantPipeline/cdk.json ================================================ { "app": "npx ts-node bin/pipeline.ts", "context": {} } ================================================ FILE: Solution/Lab5/server/TenantPipeline/jest.config.js ================================================ module.exports = { roots: ['/test'], testMatch: ['**/*.test.ts'], transform: { '^.+\\.tsx?$': 'ts-jest' } }; ================================================ FILE: Solution/Lab5/server/TenantPipeline/lib/serverless-saas-stack.ts ================================================ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 import { Construct } from 'constructs'; import * as cdk from 'aws-cdk-lib'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as codecommit from 'aws-cdk-lib/aws-codecommit'; import * as codepipeline from 'aws-cdk-lib/aws-codepipeline'; import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions'; import * as codebuild from 'aws-cdk-lib/aws-codebuild'; import { Function, Runtime, AssetCode } from 'aws-cdk-lib/aws-lambda'; import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Duration } from 'aws-cdk-lib'; export class ServerlessSaaSStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const artifactsBucket = new s3.Bucket(this, "ArtifactsBucket", { encryption: s3.BucketEncryption.S3_MANAGED, }); //Since this lambda is invoking cloudformation which is inturn deploying AWS resources, we are giving overly permissive permissions to this lambda. //You can limit this based upon your use case and AWS Resources you need to deploy. const lambdaPolicy = new PolicyStatement() lambdaPolicy.addActions("*") lambdaPolicy.addResources("*") const lambdaFunction = new Function(this, "deploy-tenant-stack", { handler: "lambda-deploy-tenant-stack.lambda_handler", runtime: Runtime.PYTHON_3_9, code: new AssetCode(`./resources`), memorySize: 512, timeout: Duration.seconds(10), environment: { BUCKET: artifactsBucket.bucketName, }, initialPolicy: [lambdaPolicy], }) // Pipeline creation starts const pipeline = new codepipeline.Pipeline(this, 'Pipeline', { pipelineName: 'serverless-saas-pipeline', artifactBucket: artifactsBucket }); // Import existing CodeCommit sam-app repository const codeRepo = codecommit.Repository.fromRepositoryName( this, 'AppRepository', 'aws-serverless-saas-workshop' ); // Declare source code as an artifact const sourceOutput = new codepipeline.Artifact(); // Add source stage to pipeline pipeline.addStage({ stageName: 'Source', actions: [ new codepipeline_actions.CodeCommitSourceAction({ actionName: 'CodeCommit_Source', repository: codeRepo, branch: 'main', output: sourceOutput, variablesNamespace: 'SourceVariables' }), ], }); // Declare build output as artifacts const buildOutput = new codepipeline.Artifact(); //Declare a new CodeBuild project const buildProject = new codebuild.PipelineProject(this, 'Build', { buildSpec : codebuild.BuildSpec.fromSourceFilename("Lab5/server/tenant-buildspec.yml"), environment: { buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_4 }, environmentVariables: { 'PACKAGE_BUCKET': { value: artifactsBucket.bucketName } } }); // Add the build stage to our pipeline pipeline.addStage({ stageName: 'Build', actions: [ new codepipeline_actions.CodeBuildAction({ actionName: 'Build-Serverless-SaaS', project: buildProject, input: sourceOutput, outputs: [buildOutput], }), ], }); const deployOutput = new codepipeline.Artifact(); //Add the Lambda function that will deploy the tenant stack in a multitenant way pipeline.addStage({ stageName: 'Deploy', actions: [ new codepipeline_actions.LambdaInvokeAction({ actionName: 'DeployTenantStack', lambda: lambdaFunction, inputs: [buildOutput], outputs: [deployOutput], userParameters: { 'artifact': 'Artifact_Build_Build-Serverless-SaaS', 'template_file': 'packaged.yaml', 'commit_id': '#{SourceVariables.CommitId}' } }), ], }); } } ================================================ FILE: Solution/Lab5/server/TenantPipeline/package.json ================================================ { "name": "pipeline", "version": "0.1.0", "bin": { "pipeline": "bin/pipeline.js" }, "scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "cdk": "cdk" }, "devDependencies": { "@aws-cdk/assert": "1.64.1", "@types/jest": "^26.0.10", "@types/node": "10.17.27", "aws-cdk-lib": "^2.0.0", "constructs": "^10.0.0", "jest": "^26.4.2", "node-notifier": "^8.0.1", "ts-jest": "^26.2.0", "ts-node": "^8.1.0", "typescript": "4.9.5", "@types/prettier": "2.6.0" }, "dependencies": { "aws-cdk-lib": "^2.0.0", "constructs": "^10.0.0", "source-map-support": "^0.5.19" } } ================================================ FILE: Solution/Lab5/server/TenantPipeline/resources/lambda-deploy-tenant-stack.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from boto3.session import Session import json import boto3 import zipfile import tempfile import botocore import traceback import time print('Loading function') cf = boto3.client('cloudformation') code_pipeline = boto3.client('codepipeline') dynamodb = boto3.resource('dynamodb') table_tenant_stack_mapping = dynamodb.Table('ServerlessSaaS-TenantStackMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') table_tenant_settings = dynamodb.Table('ServerlessSaaS-Settings') def find_artifact(artifacts, name): """Finds the artifact 'name' among the 'artifacts' Args: artifacts: The list of artifacts available to the function name: The artifact we wish to use Returns: The artifact dictionary found Raises: Exception: If no matching artifact is found """ for artifact in artifacts: if artifact['name'] == name: return artifact raise Exception('Input artifact named "{0}" not found in event'.format(name)) def get_template_url(s3, artifact, file_in_zip): """Gets the template artifact Downloads the artifact from the S3 artifact store to a temporary file then extracts the zip and returns the file containing the CloudFormation template. Args: artifact: The artifact to download file_in_zip: The path to the file within the zip containing the template Returns: The CloudFormation template as a string Raises: Exception: Any exception thrown while downloading the artifact or unzipping it """ tmp_file = tempfile.NamedTemporaryFile() bucket = artifact['location']['s3Location']['bucketName'] print(bucket) key = artifact['location']['s3Location']['objectKey'] print(key) with tempfile.NamedTemporaryFile() as tmp_file: s3.download_file(bucket, key, tmp_file.name) with zipfile.ZipFile(tmp_file.name, 'r') as zip: extracted_file = zip.extract(file_in_zip, '/tmp/') s3.upload_file(extracted_file, bucket, file_in_zip) template_url =''.join(['https://', bucket,'.s3.amazonaws.com/',file_in_zip]) return template_url def update_stack(stack, template_url, params): """Start a CloudFormation stack update Args: stack: The stack to update template_url: The template to apply Returns: True if an update was started, false if there were no changes to the template since the last update. Raises: Exception: Any exception besides "No updates are to be performed." """ try: cf.update_stack(StackName=stack, TemplateURL=template_url, Capabilities=['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], Parameters=params) return True except botocore.exceptions.ClientError as e: if e.response['Error']['Message'] == 'No updates are to be performed.': return False else: raise Exception('Error updating CloudFormation stack "{0}"'.format(stack), e) def stack_exists(stack): """Check if a stack exists or not Args: stack: The stack to check Returns: True or False depending on whether the stack exists Raises: Any exceptions raised .describe_stacks() besides that the stack doesn't exist. """ try: cf.describe_stacks(StackName=stack) return True except botocore.exceptions.ClientError as e: if "does not exist" in e.response['Error']['Message']: return False else: raise e def create_stack(stack, template_url, params): """Starts a new CloudFormation stack creation Args: stack: The stack to be created template_url: The template for the stack to be created with Throws: Exception: Any exception thrown by .create_stack() """ cf.create_stack(StackName=stack, TemplateURL=template_url, Capabilities=['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], Parameters=params) def get_stack_status(stack): """Get the status of an existing CloudFormation stack Args: stack: The name of the stack to check Returns: The CloudFormation status string of the stack such as CREATE_COMPLETE Raises: Exception: Any exception thrown by .describe_stacks() """ stack_description = cf.describe_stacks(StackName=stack) return stack_description['Stacks'][0]['StackStatus'] def put_job_success(job, message): """Notify CodePipeline of a successful job Args: job: The CodePipeline job ID message: A message to be logged relating to the job status Raises: Exception: Any exception thrown by .put_job_success_result() """ print('Putting job success') print(message) code_pipeline.put_job_success_result(jobId=job) def put_job_failure(job, message): """Notify CodePipeline of a failed job Args: job: The CodePipeline job ID message: A message to be logged relating to the job status Raises: Exception: Any exception thrown by .put_job_failure_result() """ print('Putting job failure') print(message) code_pipeline.put_job_failure_result(jobId=job, failureDetails={'message': message, 'type': 'JobFailed'}) def continue_job_later(job, message): """Notify CodePipeline of a continuing job This will cause CodePipeline to invoke the function again with the supplied continuation token. Args: job: The JobID message: A message to be logged relating to the job status continuation_token: The continuation token Raises: Exception: Any exception thrown by .put_job_success_result() """ # Use the continuation token to keep track of any job execution state # This data will be available when a new job is scheduled to continue the current execution continuation_token = json.dumps({'previous_job_id': job}) print('Putting job continuation') print(message) code_pipeline.put_job_success_result(jobId=job, continuationToken=continuation_token) def start_update_or_create(job_id, stack, template_url, params): """Starts the stack update or create process If the stack exists then update, otherwise create. Args: job_id: The ID of the CodePipeline job stack: The stack to create or update template_url: The template to create/update the stack with """ if stack_exists(stack): status = get_stack_status(stack) if status not in ['CREATE_COMPLETE', 'ROLLBACK_COMPLETE', 'UPDATE_COMPLETE']: # If the CloudFormation stack is not in a state where # it can be updated again then fail the job right away. put_job_failure(job_id, 'Stack cannot be updated when status is: ' + status) return were_updates = update_stack(stack, template_url, params) if were_updates: # If there were updates then continue the job so it can monitor # the progress of the update. continue_job_later(job_id, 'Stack update started') else: # If there were no updates then succeed the job immediately put_job_success(job_id, 'There were no stack updates') else: # If the stack doesn't already exist then create it instead # of updating it. create_stack(stack, template_url, params) # Continue the job so the pipeline will wait for the CloudFormation # stack to be created. continue_job_later(job_id, 'Stack create started') def check_stack_update_status(job_id, stack): """Monitor an already-running CloudFormation update/create Succeeds, fails or continues the job depending on the stack status. Args: job_id: The CodePipeline job ID stack: The stack to monitor """ status = get_stack_status(stack) if status in ['UPDATE_COMPLETE', 'CREATE_COMPLETE']: # If the update/create finished successfully then # succeed the job and don't continue. put_job_success(job_id, 'Stack update complete') elif status in ['UPDATE_IN_PROGRESS', 'UPDATE_ROLLBACK_IN_PROGRESS', 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', 'CREATE_IN_PROGRESS', 'ROLLBACK_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS']: # If the job isn't finished yet then continue it continue_job_later(job_id, 'Stack update still in progress') else: # If the Stack is a state which isn't "in progress" or "complete" # then the stack update/create has failed so end the job with # a failed result. put_job_failure(job_id, 'Update failed: ' + status) def get_user_params(job_data): """Decodes the JSON user parameters and validates the required properties. Args: job_data: The job data structure containing the UserParameters string which should be a valid JSON structure Returns: The JSON parameters decoded as a dictionary. Raises: Exception: The JSON can't be decoded or a property is missing. """ try: # Get the user parameters which contain the stack, artifact and file settings user_parameters = job_data['actionConfiguration']['configuration']['UserParameters'] decoded_parameters = json.loads(user_parameters) except Exception: # We're expecting the user parameters to be encoded as JSON # so we can pass multiple values. If the JSON can't be decoded # then fail the job with a helpful message. raise Exception('UserParameters could not be decoded as JSON') if 'artifact' not in decoded_parameters: # Validate that the artifact name is provided, otherwise fail the job # with a helpful message. raise Exception('Your UserParameters JSON must include the artifact name') if 'template_file' not in decoded_parameters: # Validate that the template file is provided, otherwise fail the job # with a helpful message. raise Exception('Your UserParameters JSON must include the template file name') return decoded_parameters def setup_s3_client(job_data): """Creates an S3 client Uses the credentials passed in the event by CodePipeline. These credentials can be used to access the artifact bucket. Args: job_data: The job data structure Returns: An S3 client with the appropriate credentials """ # Could not use the artifact credentials to put object to artifacts s3 bucket. # We are running into issue as described in https://github.com/aws/aws-cdk/issues/3274 # key_id = job_data['artifactCredentials']['accessKeyId'] # key_secret = job_data['artifactCredentials']['secretAccessKey'] # session_token = job_data['artifactCredentials']['sessionToken'] # session = Session(aws_access_key_id=key_id, # aws_secret_access_key=key_secret, # aws_session_token=session_token) # return session.client('s3') return boto3.client('s3') def get_tenant_params(tenantId): """Get tenant details to be supplied to Cloud formation Args: tenantId (str): tenantId for which details are needed Returns: params from tenant management table """ params = [] param_tenantid = {} param_tenantid['ParameterKey'] = 'TenantIdParameter' param_tenantid['ParameterValue'] = tenantId params.append(param_tenantid) return params def add_parameter(params, parameter_key, parameter_value): parameter = {} parameter['ParameterKey'] = parameter_key parameter['ParameterValue'] = parameter_value params.append(parameter) def update_tenantstackmapping(tenantId, commit_id): """Update the tenant stack mapping table with the code pipeline job id Args: tenantId ([string]): tenant id for which data needs to be updated job_id ([type]): current code pipeline job id Returns: [type]: [description] """ response = table_tenant_stack_mapping.update_item( Key={'tenantId': tenantId}, UpdateExpression="set codeCommitId=:codeCommitId", ExpressionAttributeValues={ ':codeCommitId': commit_id }, ReturnValues="NONE") return response def lambda_handler(event, context): """The Lambda function handler If a continuing job then checks the CloudFormation stack status and updates the job accordingly. If a new job then kick of an update or creation of the target CloudFormation stack. Args: event: The event passed by Lambda context: The context passed by Lambda """ try: # Extract the Job ID job_id = event['CodePipeline.job']['id'] # Extract the Job Data job_data = event['CodePipeline.job']['data'] # Extract the params params = get_user_params(job_data) # Get the list of artifacts passed to the function artifacts = job_data['inputArtifacts'] artifact = params['artifact'] template_file = params['template_file'] commit_id = params['commit_id'] # Get all the stacks for each tenant to be updated/created from tenant stack mapping table mappings = table_tenant_stack_mapping.scan() print (mappings) #Update/Create stacks for all tenants for mapping in mappings['Items']: stack = mapping['stackName'] tenantId = mapping['tenantId'] applyLatestRelease = mapping['applyLatestRelease'] if (applyLatestRelease): # Get the parameters to be passed to the Cloudformation from tenant table params = get_tenant_params(tenantId) if 'continuationToken' in job_data: # If we're continuing then the create/update has already been triggered # we just need to check if it has finished. check_stack_update_status(job_id, stack) else: # Get the artifact details artifact_data = find_artifact(artifacts, artifact) # Get S3 client to access artifact with s3 = setup_s3_client(job_data) # Get the JSON template file out of the artifact template_url = get_template_url(s3, artifact_data, template_file) # Kick off a stack update or create start_update_or_create(job_id, stack, template_url, params) # If we are applying the release, update tenant stack mapping with the pipe line id update_tenantstackmapping(tenantId, commit_id) except Exception as e: # If any other exceptions which we didn't expect are raised # then fail the job and log the exception message. print('Function failed due to exception.') print(e) traceback.print_exc() put_job_failure(job_id, 'Function exception: ' + str(e)) #put_job_success(job_id, "Changeset executed successfully") print('Function complete.') return "Complete." ================================================ FILE: Solution/Lab5/server/TenantPipeline/test/pipeline.test.ts ================================================ // import { SynthUtils } from '@aws-cdk/assert'; // import { Stack, App } from 'aws-cdk-lib'; // import { Template } from 'aws-cdk-lib/assertions'; // import * as Pipeline from '../lib/serverless-saas-stack'; // test('synthesized cloudformation template should match original template', () => { // const app = new App(); // const stack = new Pipeline.ServerlessSaaSStack(app, 'MyTestStack'); // const template = Template.fromStack(stack); // expect(template).toMatchSnapshot(); // }); ================================================ FILE: Solution/Lab5/server/TenantPipeline/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2018", "module": "commonjs", "lib": ["es2018"], "declaration": true, "strict": true, "noImplicitAny": true, "strictNullChecks": true, "noImplicitThis": true, "alwaysStrict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": false, "inlineSourceMap": true, "inlineSources": true, "experimentalDecorators": true, "strictPropertyInitialization": false, "typeRoots": ["./node_modules/@types"] }, "exclude": ["cdk.out"] } ================================================ FILE: Solution/Lab5/server/custom_resources/requirements.txt ================================================ requests crhelper ================================================ FILE: Solution/Lab5/server/custom_resources/update_settings_table.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import logger from crhelper import CfnResource helper = CfnResource() try: dynamodb = boto3.resource('dynamodb') except Exception as e: helper.init_failure(e) @helper.create @helper.update def do_action(event, _): """ Called as part of bootstrap template. Inserts/Updates Settings table based upon the resources deployed inside bootstrap template We use these settings inside tenant template Args: event ([type]): [description] _ ([type]): [description] """ logger.info("Updating settings") settings_table_name = event['ResourceProperties']['SettingsTableName'] cognitoUserPoolId = event['ResourceProperties']['cognitoUserPoolId'] cognitoUserPoolClientId = event['ResourceProperties']['cognitoUserPoolClientId'] table_system_settings = dynamodb.Table(settings_table_name) response = table_system_settings.put_item( Item={ 'settingName': 'userPoolId-pooled', 'settingValue' : cognitoUserPoolId } ) response = table_system_settings.put_item( Item={ 'settingName': 'appClientId-pooled', 'settingValue' : cognitoUserPoolClientId } ) @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Solution/Lab5/server/custom_resources/update_tenant_apigatewayurl.py ================================================ import json import boto3 import logger from boto3.dynamodb.conditions import Key from crhelper import CfnResource helper = CfnResource() try: client = boto3.client('dynamodb') dynamodb = boto3.resource('dynamodb') except Exception as e: helper.init_failure(e) @helper.create @helper.update def do_action(event, _): """ The URL for Tenant APIs(Product/Order) can differ by tenant. For Pooled tenants it is shared and for Silo (Platinum tier tenants) it is unique to them. This method keeps the URL for Pooled tenants inside Settings Table, since it is shared across multiple tenants, And for Silo tenants inside the tenant management table along with other tenant settings, for that tenant Args: event ([type]): [description] _ ([type]): [description] """ logger.info("Updating Tenant Details table") tenant_details_table_name = event['ResourceProperties']['TenantDetailsTableName'] settings_table_name = event['ResourceProperties']['SettingsTableName'] tenant_id = event['ResourceProperties']['TenantId'] tenant_api_gateway_url = event['ResourceProperties']['TenantApiGatewayUrl'] if(tenant_id.lower() =='pooled'): # Note: Tenant management service will use below setting to update apiGatewayUrl for pooled tenants in TenantDetails table settings_table = dynamodb.Table(settings_table_name) settings_table.put_item(Item={ 'settingName': 'apiGatewayUrl-Pooled', 'settingValue' : tenant_api_gateway_url }) else: tenant_details = dynamodb.Table(tenant_details_table_name) response = tenant_details.update_item( Key={'tenantId': tenant_id}, UpdateExpression="set apiGatewayUrl=:apiGatewayUrl", ExpressionAttributeValues={ ':apiGatewayUrl': tenant_api_gateway_url }, ReturnValues="NONE") @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Solution/Lab5/server/custom_resources/update_tenantstackmap_table.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import logger from crhelper import CfnResource helper = CfnResource() try: dynamodb = boto3.resource('dynamodb') except Exception as e: helper.init_failure(e) @helper.create @helper.update def do_action(event, _): """ One time entry for pooled tenants inside tenant stack mapping table. This ensures that when code pipeline for tenant template is kicked off, it always create a default stack for pooled tenants. Args: event ([type]): [description] _ ([type]): [description] """ logger.info("Updating Tenant Stack Map") tenantstackmap_table_name = event['ResourceProperties']['TenantStackMappingTableName'] table_stack_mapping = dynamodb.Table(tenantstackmap_table_name) response = table_stack_mapping.put_item( Item={ 'tenantId': 'pooled', 'stackName' : 'stack-pooled', 'applyLatestRelease': True, 'codeCommitId': '' } ) @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Solution/Lab5/server/layers/auth_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils # These are the roles being supported in this reference architecture class UserRoles: SYSTEM_ADMIN = "SystemAdmin" CUSTOMER_SUPPORT = "CustomerSupport" TENANT_ADMIN = "TenantAdmin" TENANT_USER = "TenantUser" def isTenantAdmin(user_role): if (user_role == UserRoles.TENANT_ADMIN): return True else: return False def isSystemAdmin(user_role): if (user_role == UserRoles.SYSTEM_ADMIN): return True else: return False def isSaaSProvider(user_role): if (user_role == UserRoles.SYSTEM_ADMIN or user_role == UserRoles.CUSTOMER_SUPPORT): return True else: return False def isTenantUser(user_role): if (user_role == UserRoles.TENANT_USER): return True else: return False def getPolicyForUser(user_role, service_identifier, tenant_id, region, aws_account_id): """ This method is being used by Authorizer to get appropriate policy by user role Args: user_role (string): UserRoles enum tenant_id (string): region (string): aws_account_id (string): Returns: string: policy that tenant needs to assume """ iam_policy = "" if (isSystemAdmin(user_role)): iam_policy = __getPolicyForSystemAdmin(region, aws_account_id) elif (isTenantAdmin(user_role)): iam_policy = __getPolicyForTenantAdmin(tenant_id, service_identifier, region, aws_account_id) elif (isTenantUser(user_role)): iam_policy = __getPolicyForTenantUser(tenant_id, region, aws_account_id) return iam_policy def __getPolicyForSystemAdmin(region, aws_account_id): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/*".format(region, aws_account_id), ] } ] } return json.dumps(policy) def __getPolicyForTenantAdmin(tenant_id, sevice_identifier, region, aws_account_id): if (sevice_identifier == utils.Service_Identifier.SHARED_SERVICES.value): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantUserMapping".format(region, aws_account_id), "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantDetails".format(region, aws_account_id) ], "Condition": { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [ "{0}".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantStackMapping".format(region, aws_account_id), "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-Settings".format(region, aws_account_id) ] } ] } else: policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Product-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Order-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } } ] } return json.dumps(policy) def __getPolicyForTenantUser(tenant_id, region, aws_account_id): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Product-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Order-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } } ] } return json.dumps(policy) ================================================ FILE: Solution/Lab5/server/layers/logger.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from aws_lambda_powertools import Logger logger = Logger() """Log info messages """ def info(log_message): #logger.structure_logs(append=True, tenant_id=tenant_id) logger.info (log_message) """Log error messages """ def error(log_message): #logger.structure_logs(append=True, tenant_id=tenant_id) logger.error (log_message) """Log with tenant context. Extracts tenant context from the lambda events """ def log_with_tenant_context(event, log_message): logger.structure_logs(append=True, tenant_id= event['requestContext']['authorizer']['tenantId']) logger.info (log_message) ================================================ FILE: Solution/Lab5/server/layers/metrics_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json from aws_lambda_powertools import Metrics metrics = Metrics() def record_metric(event, metric_name, metric_unit, metric_value): """ Record the metric in Cloudwatch using EMF format Args: event ([type]): [description] metric_name ([type]): [description] metric_unit ([type]): [description] metric_value ([type]): [description] """ metrics.add_dimension(name="tenant_id", value=event['requestContext']['authorizer']['tenantId']) metrics.add_metric(name=metric_name, unit=metric_unit, value=metric_value) metrics_object = metrics.serialize_metric_set() metrics.clear_metrics() print(json.dumps(metrics_object)) ================================================ FILE: Solution/Lab5/server/layers/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] simplejson jsonpickle aws_requests_auth python-jose[cryptography] aws_requests_auth ================================================ FILE: Solution/Lab5/server/layers/utils.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import jsonpickle import simplejson import boto3 from aws_requests_auth.aws_auth import AWSRequestsAuth from enum import Enum class TenantTier(Enum): PLATINUM = "Platinum" PREMIUM = "Premium" STANDARD = "Standard" BASIC = "Basic" class StatusCodes(Enum): SUCCESS = 200 UN_AUTHORIZED = 401 NOT_FOUND = 404 class Service_Identifier(Enum): SHARED_SERVICES = "SharedServices" BUSINESS_SERVICES = "BusinessServices" def create_success_response(message): return { "statusCode": StatusCodes.SUCCESS.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def create_unauthorized_response(): return { "statusCode": StatusCodes.UN_AUTHORIZED.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": "User not authorized to perform this action" }), } def create_notfound_response(message): return { "statusCode": StatusCodes.NOT_FOUND.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def get_auth(host, region): session = boto3.Session() credentials = session.get_credentials() auth = AWSRequestsAuth(aws_access_key=credentials.access_key, aws_secret_access_key=credentials.secret_key, aws_token=credentials.token, aws_host=host, aws_region=region, aws_service='execute-api') return auth def get_headers(event): return event['headers'] def generate_response(inputObject): return { "statusCode": 200, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": encode_to_json_object(inputObject), } def encode_to_json_object(inputObject): jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True) jsonpickle.set_preferred_backend('simplejson') return jsonpickle.encode(inputObject, unpicklable=False, use_decimal=True) ================================================ FILE: Solution/Lab5/server/nested_templates/apigateway.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway, apis, api keys and usage plan as part of bootstrap Parameters: StageName: Type: String RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ProvisionTenantFunctionArn: Type: String DeProvisionTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetTenantConfigFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String Resources: ApiGatewayCloudWatchLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub apigateway-cloudwatch-publish-role-${AWS::Region} Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole ApiGatewayAttachCloudwatchLogArn: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchLogRole.Arn AdminApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/api-gateway/access-logs-serverless-saas-admin-api RetentionInDays: 30 AdminApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: "/*" HttpMethod: "*" Auth: ResourcePolicy: CustomStatements: - Effect: Allow Principal: "*" Action: "execute-api:Invoke" Resource: ["execute-api:/*/*/*"] - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/tenant" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/user/tenant-admin" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/provisioning" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref RegisterTenantLambdaExecutionRoleArn - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/disable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/enable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/provisioning/{tenantid}" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn AccessLogSetting: DestinationArn: !GetAtt AdminApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ["", ["serverless-saas-admin-api-", !Ref "AWS::Region"]] basePath: !Join ["", ["/", !Ref StageName]] schemes: - https paths: /registration: post: summary: Register a new tenant description: Register a new tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref RegisterTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /provisioning: post: summary: provisions resource for new tenant description: provisions resource for new tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ProvisionTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /provisioning/{tenantid}: put: summary: deprovision by tenant description: deprovision by tenant produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeProvisionTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/activation/{tenantid}: put: security: - Authorizer: [] summary: Activate an existing tenant description: Activate an existing tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ActivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenants: get: summary: Returns all tenants description: Returns all tenants produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantsFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant: post: summary: Creates a tenant description: Creates a tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/init/{tenantname}: get: summary: Returns a tenant config description: Return a tenant config by a tenant name produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantConfigFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/{tenantid}: get: summary: Returns a tenant description: Return a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Disables a tenant description: Disables a tenant by a tenant id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeactivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy put: summary: Updates a tenant description: Updates a tenant produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user/{username}: get: summary: Returns a user description: Return a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUserFunctionArn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Diables a user description: Disable a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUserFunctionArn - /invocations httpMethod: POST type: aws_proxy /user/tenant-admin: post: summary: Creates a tenant admin user description: Creates a tenant admin user produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantAdminUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user: post: summary: Create a user description: Create a user by a user id produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users: get: summary: Get all users by tenantId description: Get all users by tenantId produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUsersFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/disable: put: summary: disable users by tenant id description: disable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/enable: put: summary: enable users by tenant id description: enable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref EnableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: api_key: type: "apiKey" name: "x-api-key" in: "header" sigv4Reference: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "awsSigv4" Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref AuthorizerFunctionArn - /invocations authorizerResultTtlInSeconds: 60 type: "token" StageName: prod Outputs: AdminApiGatewayApi: Value: !Ref AdminApiGatewayApi ================================================ FILE: Solution/Lab5/server/nested_templates/apigateway_lambdapermissions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway and apis as part of bootstrap Parameters: RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ProvisionTenantFunctionArn: Type: String DeProvisionTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetTenantConfigFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String AdminApiGatewayApi: Type: String Resources: #provide api gateway permissions to call lambda functions RegisterTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref RegisterTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateTenantAdminUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantAdminUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ProvisionTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ProvisionTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeProvisionTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeProvisionTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] EnableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref EnableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUsersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUsersFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref AuthorizerFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*" ]] CreateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantsFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeactivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeactivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ActivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ActivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantConfigLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantConfigFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ================================================ FILE: Solution/Lab5/server/nested_templates/cognito.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup cognito as part of bootstrap Parameters: AdminEmailParameter: Type: String Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Description: "Enter the role name for system admin" AdminUserPoolCallbackURLParameter: Type: String Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Description: "Enter Tenant Management userpool call back url" Resources: CognitoUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: PooledTenant-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into tenant UI application at " - "https://" - !Ref TenantUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for tenant UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSClient GenerateSecret: false UserPoolId: !Ref CognitoUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [pooledtenant-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoUserPool CognitoOperationUsersUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: OperationUsers-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into admin UI application at " - "https://" - !Ref AdminUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for admin UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoOperationUsersUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSOperationUsersPoolClient GenerateSecret: false UserPoolId: !Ref CognitoOperationUsersUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoOperationUsersUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [operationsusers-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUserGroup: Type: AWS::Cognito::UserPoolGroup Properties: GroupName: SystemAdmins Description: Admin user group Precedence: 0 UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUser: Type: AWS::Cognito::UserPoolUser Properties: Username: admin DesiredDeliveryMediums: - EMAIL ForceAliasCreation: true UserAttributes: - Name: email Value: !Ref AdminEmailParameter - Name: custom:tenantId Value: system_admins - Name: custom:userRole Value: !Ref SystemAdminRoleNameParameter UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAddUserToGroup: Type: AWS::Cognito::UserPoolUserToGroupAttachment Properties: GroupName: !Ref CognitoAdminUserGroup Username: !Ref CognitoAdminUser UserPoolId: !Ref CognitoOperationUsersUserPool Outputs: CognitoUserPoolId: Value: !Ref CognitoUserPool CognitoUserPoolClientId: Value: !Ref CognitoUserPoolClient CognitoOperationUsersUserPoolId: Value: !Ref CognitoOperationUsersUserPool CognitoOperationUsersUserPoolClientId: Value: !Ref CognitoOperationUsersUserPoolClient CognitoOperationUsersUserPoolProviderURL: Value: !GetAtt CognitoOperationUsersUserPool.ProviderURL ================================================ FILE: Solution/Lab5/server/nested_templates/custom_resources.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: ServerlessSaaSSettingsTableArn: Type: String ServerlessSaaSSettingsTableName: Type: String TenantStackMappingTableArn: Type: String TenantStackMappingTableName: Type: String UpdateSettingsTableFunctionArn: Type: String UpdateTenantStackMapTableFunctionArn: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String Resources: #Custom resources UpdateSettingsTable: Type: Custom::UpdateSettingsTable Properties: ServiceToken: !Ref UpdateSettingsTableFunctionArn SettingsTableName: !Ref ServerlessSaaSSettingsTableName cognitoUserPoolId: !Ref CognitoUserPoolId cognitoUserPoolClientId: !Ref CognitoUserPoolClientId UpdateTenantStackMap: Type: Custom::UpdateTenantStackMap Properties: ServiceToken: !Ref UpdateTenantStackMapTableFunctionArn TenantStackMappingTableName: !Ref TenantStackMappingTableName ================================================ FILE: Solution/Lab5/server/nested_templates/lambdafunctions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy lambda functions as part of bootstrap Parameters: CognitoOperationUsersUserPoolId: Type: String CognitoOperationUsersUserPoolClientId: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String TenantDetailsTableArn: Type: String ServerlessSaaSSettingsTableArn: Type: String TenantStackMappingTableArn: Type: String TenantUserMappingTableArn: Type: String TenantStackMappingTableName: Type: String TenantUserPoolCallbackURLParameter: Type: String Description: "Enter Tenant Management userpool call back url" Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies Description: Utilities for project ContentUri: ../layers/ CompatibleRuntimes: - python3.9 LicenseInfo: "MIT" RetentionPolicy: Retain Metadata: BuildMethod: python3.9 #Tenant Authorizer AuthorizerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: authorizer-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: authorizer-execution-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:List* Resource: - !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn AuthorizerAccessRole: Type: AWS::IAM::Role DependsOn: AuthorizerExecutionRole Properties: RoleName: authorizer-access-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: - !GetAtt 'AuthorizerExecutionRole.Arn' Action: - sts:AssumeRole Policies: - PolicyName: authorizer-access-role-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:BatchGetItem - dynamodb:GetItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:UpdateItem - dynamodb:Query - dynamodb:Scan Resource: - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/* SharedServicesAuthorizerFunction: Type: AWS::Serverless::Function DependsOn: AuthorizerAccessRole Properties: CodeUri: ../Resources/ Handler: shared_service_authorizer.lambda_handler Runtime: python3.9 Role: !GetAtt AuthorizerExecutionRole.Arn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !Ref CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !Ref CognitoOperationUsersUserPoolClientId #Create user pool for the tenant TenantUserPoolLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-userpool-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-userpool-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn - Effect: Allow Action: - dynamodb:GetItem - dynamodb:Query Resource: - !Ref TenantUserMappingTableArn CreateUserLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub create-user-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-user-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:PutItem Resource: - !Ref TenantUserMappingTableArn - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn CreateTenantAdminUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_tenant_admin_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId TENANT_APP_CLIENT_ID: !Ref CognitoUserPoolClientId TENANT_USER_POOL_CALLBACK_URL: !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] POWERTOOLS_SERVICE_NAME: "UserManagement.CreateTenantAdmin" #User management CreateUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.CreateUser" UpdateUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.update_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.UpdateUser" DisableUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUser" DisableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUsersByTenant" EnableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.enable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.EnableUsersByTenant" GetUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.GetUser" GetUsersFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_users Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.GetUsers" #Tenant Management TenantManagementLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-management-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-tenant-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:Scan - dynamodb:Query Resource: - !Ref TenantDetailsTableArn - !Join ["", [!Ref TenantDetailsTableArn, '/index/*']] - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref ServerlessSaaSSettingsTableArn CreateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.create_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.CreateTenant" ActivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.activate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.ActivateTenant" ENABLE_USERS_BY_TENANT: "/users/enable" PROVISION_TENANT: "/provisioning/" GetTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.GetTenant" DeactivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.deactivate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.DeactivateTenant" DEPROVISION_TENANT: "/provisioning/" DISABLE_USERS_BY_TENANT: "/users/disable" UpdateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.update_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.UpdateTenant" GetTenantsFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenants Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers GetTenantConfigFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.load_tenant_config Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers #Tenant Registration RegisterTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-registration-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess RegisterTenantFunction: Type: AWS::Serverless::Function DependsOn: RegisterTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-registration.register_tenant Runtime: python3.9 Role: !GetAtt RegisterTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: # Need to find a better way than hard coding resource paths CREATE_TENANT_ADMIN_USER_RESOURCE_PATH: "/user/tenant-admin" CREATE_TENANT_RESOURCE_PATH: "/tenant" PROVISION_TENANT_RESOURCE_PATH: "/provisioning" POWERTOOLS_SERVICE_NAME: "TenantRegistration.RegisterTenant" #Tenant Provisioning ProvisionTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-provisioning-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-provisioning-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:DeleteItem Resource: - !Ref TenantStackMappingTableArn - Effect: Allow Action: - codepipeline:StartPipelineExecution Resource: - !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:serverless-saas-pipeline - Effect: Allow Action: - cloudformation:DeleteStack Resource: "*" ProvisionTenantFunction: Type: AWS::Serverless::Function DependsOn: ProvisionTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-provisioning.provision_tenant Runtime: python3.9 Role: !GetAtt ProvisionTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_STACK_MAPPING_TABLE_NAME: !Ref TenantStackMappingTableName DeProvisionTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-deprovisioning-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-deprovisioning-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 #Since this lambda is invoking cloudformation which is inturn removing AWS resources, we are giving overly permissive permissions to this lambda. #You can limit this based upon your use case and AWS Resources you need to remove. Statement: - Effect: Allow Action: "*" Resource: "*" DeProvisionTenantFunction: Type: AWS::Serverless::Function DependsOn: DeProvisionTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-provisioning.deprovision_tenant Runtime: python3.9 Role: !GetAtt DeProvisionTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_STACK_MAPPING_TABLE_NAME: !Ref TenantStackMappingTableName UpdateSettingsTableLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub update-settingstable-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub update-settingstable-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: !Ref ServerlessSaaSSettingsTableArn UpdateSettingsTableFunction: Type: AWS::Serverless::Function DependsOn: UpdateSettingsTableLambdaExecutionRole Properties: CodeUri: ../custom_resources/ Handler: update_settings_table.handler Runtime: python3.9 Role: !GetAtt UpdateSettingsTableLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers UpdateTenantStackMapTableLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub update-tenantstackmap-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub update-tenantstackmap-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: !Ref TenantStackMappingTableArn UpdateTenantStackMapTableFunction: Type: AWS::Serverless::Function DependsOn: UpdateTenantStackMapTableLambdaExecutionRole Properties: CodeUri: ../custom_resources/ Handler: update_tenantstackmap_table.handler Runtime: python3.9 Role: !GetAtt UpdateTenantStackMapTableLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Outputs: RegisterTenantLambdaExecutionRoleArn: Value: !GetAtt RegisterTenantLambdaExecutionRole.Arn TenantManagementLambdaExecutionRoleArn: Value: !GetAtt TenantManagementLambdaExecutionRole.Arn RegisterTenantFunctionArn: Value: !GetAtt RegisterTenantFunction.Arn ProvisionTenantFunctionArn: Value: !GetAtt ProvisionTenantFunction.Arn DeProvisionTenantFunctionArn: Value: !GetAtt DeProvisionTenantFunction.Arn ActivateTenantFunctionArn: Value: !GetAtt ActivateTenantFunction.Arn GetTenantConfigFunctionArn: Value: !GetAtt GetTenantConfigFunction.Arn GetTenantsFunctionArn: Value: !GetAtt GetTenantsFunction.Arn CreateTenantFunctionArn: Value: !GetAtt CreateTenantFunction.Arn GetTenantFunctionArn: Value: !GetAtt GetTenantFunction.Arn DeactivateTenantFunctionArn: Value: !GetAtt DeactivateTenantFunction.Arn UpdateTenantFunctionArn: Value: !GetAtt UpdateTenantFunction.Arn GetUsersFunctionArn: Value: !GetAtt GetUsersFunction.Arn GetUserFunctionArn: Value: !GetAtt GetUserFunction.Arn UpdateUserFunctionArn: Value: !GetAtt UpdateUserFunction.Arn DisableUserFunctionArn: Value: !GetAtt DisableUserFunction.Arn CreateTenantAdminUserFunctionArn: Value: !GetAtt CreateTenantAdminUserFunction.Arn CreateUserFunctionArn: Value: !GetAtt CreateUserFunction.Arn DisableUsersByTenantFunctionArn: Value: !GetAtt DisableUsersByTenantFunction.Arn EnableUsersByTenantFunctionArn: Value: !GetAtt EnableUsersByTenantFunction.Arn SharedServicesAuthorizerFunctionArn: Value: !GetAtt SharedServicesAuthorizerFunction.Arn AuthorizerExecutionRoleArn: Value: !GetAtt AuthorizerExecutionRole.Arn UpdateSettingsTableFunctionArn: Value: !GetAtt UpdateSettingsTableFunction.Arn UpdateTenantStackMapTableFunctionArn: Value: !GetAtt UpdateTenantStackMapTableFunction.Arn ================================================ FILE: Solution/Lab5/server/nested_templates/tables.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to create dynamodb tables as part of bootstrap Resources: ServerlessSaaSSettingsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: settingName AttributeType: S KeySchema: - AttributeName: settingName KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-Settings TenantStackMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantStackMapping TenantDetailsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: tenantName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: ServerlessSaas-TenantConfig KeySchema: - AttributeName: tenantName KeyType: HASH Projection: NonKeyAttributes: - userPoolId - appClientId - apiGatewayUrl ProjectionType: INCLUDE TableName: ServerlessSaaS-TenantDetails TenantUserMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: userName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH - AttributeName: userName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantUserMapping GlobalSecondaryIndexes: - IndexName: UserName KeySchema: - AttributeName: userName KeyType: HASH - AttributeName: tenantId KeyType: RANGE Projection: ProjectionType: ALL Outputs: ServerlessSaaSSettingsTableArn: Value: !GetAtt ServerlessSaaSSettingsTable.Arn ServerlessSaaSSettingsTableName: Value: !Ref ServerlessSaaSSettingsTable TenantStackMappingTableArn: Value: !GetAtt TenantStackMappingTable.Arn TenantStackMappingTableName: Value: !Ref TenantStackMappingTable TenantDetailsTableArn: Value: !GetAtt TenantDetailsTable.Arn TenantDetailsTableName: Value: !Ref TenantDetailsTable TenantUserMappingTableArn: Value: !GetAtt TenantUserMappingTable.Arn TenantUserMappingTableName: Value: !Ref TenantUserMappingTable ================================================ FILE: Solution/Lab5/server/nested_templates/userinterface.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: IsCloudFrontAndS3PreProvisioned: Type: String Default: false Description: "Tells if cloudfront and s3 buckets are pre-provisioned or not. They get pre-provisioned when the workshop is running as a part of AWS Event through AWS event engine tool." Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref IsCloudFrontAndS3PreProvisioned, true] ] Resources: CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: IsNotRunningInEventEngine Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distributions" AdminAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AdminAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AdminAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AdminAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId AdminAppSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: #Aliases: # - !Sub 'admin.${CustomDomainName}' CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS CachedMethods: - GET - HEAD - OPTIONS Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: adminapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt AdminAppBucket.RegionalDomainName Id: adminapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' AppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId ApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: tenantapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'AppBucket.RegionalDomainName' Id: tenantapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' LandingAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True LandingAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref LandingAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${LandingAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId LandingApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: landingapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'LandingAppBucket.RegionalDomainName' Id: landingapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: AdminBucket: Description: The name of the bucket for uploading the Admin Management site to Value: !Ref AdminAppBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt AdminAppSite.DomainName Condition: IsNotRunningInEventEngine AppBucket: Description: The name of the bucket for uploading the Tenant Management site to Value: !Ref AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt ApplicationSite.DomainName Condition: IsNotRunningInEventEngine LandingAppBucket: Description: The name of the bucket for uploading the Landing site to Value: !Ref LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt LandingApplicationSite.DomainName Condition: IsNotRunningInEventEngine ================================================ FILE: Solution/Lab5/server/shared-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas" s3_bucket = "aws-saas-sam-cli-ujwbuket" s3_prefix = "serverless-saas" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" ================================================ FILE: Solution/Lab5/server/shared-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to Bootstrap the Common Resources Parameters: AdminEmailParameter: Type: String Default: "test@test.com" Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Default: "SystemAdmin" Description: "Enter the role name for system admin" StageName: Type: String Default: "prod" Description: "Stage Name for the api" EventEngineParameter: Type: String Default: false Description: "Tells if this workshop is running as a part of AWS Event through AWS EventEngine or not" AdminUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Tenant Management userpool call back url" Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref EventEngineParameter, true] ] Resources: DynamoDBTables: Type: AWS::Serverless::Application Properties: Location: nested_templates/tables.yaml #Create cloudfront and s3 for UI Cde UserInterface: Type: AWS::Serverless::Application Properties: Location: nested_templates/userinterface.yaml Parameters: IsCloudFrontAndS3PreProvisioned: !Ref EventEngineParameter Cognito: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/cognito.yaml Parameters: AdminEmailParameter: !Ref AdminEmailParameter SystemAdminRoleNameParameter: !Ref SystemAdminRoleNameParameter AdminUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.AdminAppSite, !Ref AdminUserPoolCallbackURLParameter] TenantUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.ApplicationSite, !Ref TenantUserPoolCallbackURLParameter] LambdaFunctions: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/lambdafunctions.yaml Parameters: CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId CognitoOperationUsersUserPoolId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId CognitoOperationUsersUserPoolClientId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId TenantDetailsTableArn: !GetAtt DynamoDBTables.Outputs.TenantDetailsTableArn ServerlessSaaSSettingsTableArn: !GetAtt DynamoDBTables.Outputs.ServerlessSaaSSettingsTableArn TenantStackMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableArn TenantUserMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantUserMappingTableArn TenantStackMappingTableName: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableName TenantUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.ApplicationSite, !Ref TenantUserPoolCallbackURLParameter] APIs: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway.yaml Parameters: StageName: !Ref StageName RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ProvisionTenantFunctionArn DeProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeProvisionTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn GetTenantConfigFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantConfigFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn APIGatewayLambdaPermissions: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway_lambdapermissions.yaml Parameters: RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ProvisionTenantFunctionArn DeProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeProvisionTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantConfigFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantConfigFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn AdminApiGatewayApi: !GetAtt APIs.Outputs.AdminApiGatewayApi #setup custom resources CustomResources: Type: AWS::Serverless::Application DependsOn: APIs Properties: Location: nested_templates/custom_resources.yaml Parameters: ServerlessSaaSSettingsTableArn: !GetAtt DynamoDBTables.Outputs.ServerlessSaaSSettingsTableArn ServerlessSaaSSettingsTableName: !GetAtt DynamoDBTables.Outputs.ServerlessSaaSSettingsTableName TenantStackMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableArn TenantStackMappingTableName: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableName UpdateSettingsTableFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateSettingsTableFunctionArn UpdateTenantStackMapTableFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantStackMapTableFunctionArn CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId Outputs: AdminApi: Description: "API Gateway endpoint URL for Admin API" Value: !Join ["", ["https://", !GetAtt APIs.Outputs.AdminApiGatewayApi, ".execute-api.", !Ref "AWS::Region", ".amazonaws.com/", !Ref StageName]] AdminSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant administration application Value: !GetAtt UserInterface.Outputs.AdminBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt UserInterface.Outputs.AdminAppSite Condition: IsNotRunningInEventEngine LandingApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the landing application Value: !GetAtt UserInterface.Outputs.LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt UserInterface.Outputs.LandingApplicationSite Condition: IsNotRunningInEventEngine ApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant application Value: !GetAtt UserInterface.Outputs.AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt UserInterface.Outputs.ApplicationSite Condition: IsNotRunningInEventEngine CognitoOperationUsersUserPoolId: Description: The user pool id of Admin Management userpool Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId Export: Name: "Serverless-SaaS-CognitoOperationUsersUserPoolId" CognitoOperationUsersUserPoolProviderURL: Description: The Admin Management userpool provider url Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolProviderURL CognitoOperationUsersUserPoolClientId: Description: The Admin Management userpool client id Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId Export: Name: "Serverless-SaaS-CognitoOperationUsersUserPoolClientId" CognitoTenantUserPoolId: Description: The user pool id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolId Export: Name: "Serverless-SaaS-CognitoTenantUserPoolId" CognitoTenantAppClientId: Description: The app client id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolClientId Export: Name: "Serverless-SaaS-CognitoTenantAppClientId" AuthorizerExecutionRoleArn: Description: The Lambda authorizer execution role Value: !GetAtt LambdaFunctions.Outputs.AuthorizerExecutionRoleArn Export: Name: "Serverless-SaaS-AuthorizerExecutionRoleArn" ================================================ FILE: Solution/Lab5/server/tenant-buildspec.yml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 version: 0.2 phases: install: runtime-versions: python: 3.9 commands: # Install packages or any pre-reqs in this phase. # Upgrading SAM CLI to 1.33.0 version - python -m pip install aws-sam-cli==1.33.0 - sam --version # Installing project dependencies - cd Lab5/server/ProductService - python -m pip install -r requirements.txt - cd ../OrderService - python -m pip install -r requirements.txt pre_build: commands: # Run tests, lint scripts or any other pre-build checks. - cd .. - export PYTHONPATH=./ProductService/ # unit tests needs to be fixed. Commenting for now #- python -m pytest tests/unit/ProductService-test_handler.py build: commands: # Use Build phase to build your artifacts (compile, etc.) - sam build -t tenant-template.yaml post_build: commands: # Use Post-Build for notifications, git tags, upload artifacts to S3 - sam package --s3-bucket $PACKAGE_BUCKET --output-template-file packaged.yaml artifacts: discard-paths: yes files: # List of local artifacts that will be passed down the pipeline - Lab5/server/packaged.yaml ================================================ FILE: Solution/Lab5/server/tenant-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "stack-pooled" s3_bucket = "aws-saas-sam-cli-ujwbuket" s3_prefix = "serverless-saas-tenant" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM" ================================================ FILE: Solution/Lab5/server/tenant-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Serverless SaaS Reference Architecture Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Parameters: TenantIdParameter: Type: String Default: pooled Description: Tenant ID for the stack StageName: Type: String Default: "prod" Description: "Stage Name for the api" Conditions: IsPooledDeploy: !Equals [ !Ref TenantIdParameter, pooled] IsSiloDeploy: !Not [!Equals [ !Ref TenantIdParameter, pooled]] Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: !Join ['-', [serverless-saas-dependencies, !Ref TenantIdParameter]] Description: Utilities for project ContentUri: layers/ CompatibleRuntimes: - python3.9 LicenseInfo: 'MIT' RetentionPolicy: Retain Metadata: BuildMethod: python3.9 ProductTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: productId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: productId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: !Join ['-', [Product, !Ref TenantIdParameter]] Tags: - Key: "TenantId" Value: !Ref TenantIdParameter OrderTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: orderId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: orderId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: !Join ['-', [Order, !Ref TenantIdParameter]] Tags: - Key: "TenantId" Value: !Ref TenantIdParameter ProductFunctionExecutionRolePolicy: Condition: IsSiloDeploy Type: AWS::IAM::Policy Properties: PolicyName: !Join ['-', [!Ref TenantIdParameter, product-function-policy]] Roles: - !Ref ProductFunctionExecutionRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt ProductTable.Arn ProductFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, product-function-execution-role]] Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess GetProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter GetProductsFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_products Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter CreateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.create_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter UpdateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.update_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter DeleteProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.delete_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter OrderFunctionExecutionRolePolicy: Condition: IsSiloDeploy Type: AWS::IAM::Policy Properties: PolicyName: !Join ['-', [!Ref TenantIdParameter, order-function-policy]] Roles: - !Ref OrderFunctionExecutionRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt OrderTable.Arn OrderFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, order-function-execution-role]] Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess GetOrdersFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_orders Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter GetOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter CreateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.create_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter UpdateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.update_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter DeleteOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.delete_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter BusinessServicesAuthorizerFunction: Type: AWS::Serverless::Function Properties: CodeUri: Resources/ Handler: tenant_authorizer.lambda_handler Runtime: python3.9 Role: !ImportValue Serverless-SaaS-AuthorizerExecutionRoleArn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !ImportValue Serverless-SaaS-CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !ImportValue Serverless-SaaS-CognitoOperationUsersUserPoolClientId ApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Join ['-', [/aws/api-gateway/access-logs-serverless-saas-tenant-api-, !Ref TenantIdParameter]] RetentionInDays: 30 ApiGatewayTenantApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: '/*' HttpMethod: '*' AccessLogSetting: DestinationArn: !GetAtt ApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ['-', [!Ref TenantIdParameter, 'serverless-saas-tenant-api']] basePath: !Join ['', ['/', !Ref StageName]] x-amazon-apigateway-api-key-source : "AUTHORIZER" schemes: - https paths: /order/{id}: get: summary: Returns a order description: Return a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a order description: Deletes a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /orders: get: summary: Returns all orders description: Returns all orders. produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrdersFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /order: post: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product/{id}: get: summary: Returns a product description: Return a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a product description: Deletes a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /products: get: summary: Returns all products description: Returns all products. produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductsFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product: post: produces: - application/json responses: {} security: - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt BusinessServicesAuthorizerFunction.Arn - /invocations authorizerResultTtlInSeconds: 30 type: "token" StageName: !Ref StageName GetProductsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt GetProductsFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrdersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrdersFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt BusinessServicesAuthorizerFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref ApiGatewayTenantApi, "/*/*" ]] UpdateTenantApiGatewayUrlLambdaExecutionRole: Type: AWS::IAM::Role DependsOn: ApiGatewayTenantApi Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, apigwurl-lambda-exec-role]] Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Join ['-', [!Ref TenantIdParameter, apigwurl-lambda-exe-policy ]] PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/ServerlessSaaS-Settings - Effect: Allow Action: - dynamodb:UpdateItem Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/ServerlessSaaS-TenantDetails UpdateTenantApiGatewayUrlFunction: Type: AWS::Serverless::Function DependsOn: UpdateTenantApiGatewayUrlLambdaExecutionRole Properties: CodeUri: custom_resources/ Handler: update_tenant_apigatewayurl.handler Runtime: python3.9 Role: !GetAtt UpdateTenantApiGatewayUrlLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers UpdateTenantApiGatewayUrl: Type: Custom::UpdateTenantApiGatewayUrl DependsOn: UpdateTenantApiGatewayUrlFunction Properties: ServiceToken: !GetAtt UpdateTenantApiGatewayUrlFunction.Arn TenantDetailsTableName: ServerlessSaaS-TenantDetails SettingsTableName: ServerlessSaaS-Settings TenantId: !Ref TenantIdParameter TenantApiGatewayUrl: !Sub "https://${ApiGatewayTenantApi}.execute-api.${AWS::Region}.amazonaws.com/prod/" Outputs: TenantApiGatewayId: Description: Id for Tenant API Gateway Value: !Ref ApiGatewayTenantApi TenantAPI: Description: "API Gateway endpoint URL for Tenant API" Value: !Join ['', [!Sub "https://${ApiGatewayTenantApi}.execute-api.${AWS::Region}.amazonaws.com/", !Ref StageName]] ================================================ FILE: Solution/Lab6/client/Admin/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab6/client/Admin/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab6/client/Admin/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab6/client/Admin/README.md ================================================ # Admin This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab6/client/Admin/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "admin": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "admin:build:production" }, "development": { "browserTarget": "admin:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "admin:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab6/client/Admin/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab6/client/Admin/package.json ================================================ { "name": "admin", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/flex-layout": "~14.0.0-beta.40", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.1.3", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab6/client/Admin/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Tenants', url: '/tenants', icon: 'groups', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Solution/Lab6/client/Admin/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full', }, { path: '', component: NavComponent, data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'tenants', loadChildren: () => import('./views/tenants/tenants.module').then((m) => m.TenantsModule), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab6/client/Admin/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab6/client/Admin/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Solution/Lab6/client/Admin/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { HttpInterceptorProviders } from './interceptors'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; @NgModule({ declarations: [AppComponent, NavComponent, AuthComponent], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, HttpClientModule, HttpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab6/client/Admin/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Solution/Lab6/client/Admin/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const HttpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Solution/Lab6/client/Admin/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Solution/Lab6/client/Admin/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Solution/Lab6/client/Admin/src/app/nav/nav.component.css ================================================ .sidenav-container { height: 100%; } .sidenav-content-container { background-color: lightgray; } .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .mat-toolbar.mat-primary { position: sticky; top: 0; z-index: 1; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .spinner-container { height: 80%; display: flex; justify-content: center; align-items: center; box-sizing: border-box; } ================================================ FILE: Solution/Lab6/client/Admin/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ username$ | async }}
================================================ FILE: Solution/Lab6/client/Admin/src/app/nav/nav.component.spec.ts ================================================ import { LayoutModule } from '@angular/cdk/layout'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { NavComponent } from './nav.component'; describe('NavComponent', () => { let component: NavComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [NavComponent], imports: [ NoopAnimationsModule, LayoutModule, MatButtonModule, MatIconModule, MatListModule, MatSidenavModule, MatToolbarModule, ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(NavComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should compile', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: Solution/Lab6/client/Admin/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.css'], }) export class NavComponent implements OnInit { tenantName = ''; loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router ) { this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => err); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map((sesh) => sesh && sesh.isValid()) ); const token$ = session$.pipe(map((sesh) => sesh && sesh.getIdToken())); this.username$ = token$.pipe( map((t) => t && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Solution/Lab6/client/Admin/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/dashboard/dashboard.component.html ================================================

Traffic

December 2020
================================================ FILE: Solution/Lab6/client/Admin/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } .chart-wrapper { padding-right: 20px; position: relative; margin: auto; height: 80vh; width: 80vw; } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/dashboard/dashboard.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ChartConfiguration, ChartType } from 'chart.js'; import { TenantsService } from '../tenants/tenants.service'; interface DataSet { label: string; data: number[]; } interface ChartData { tenantId: string; dataSet: DataSet[]; totalOrders: number; } @Component({ templateUrl: 'dashboard.component.html', styleUrls: ['./dashboard.component.scss'], selector: 'app-dashboard', }) export class DashboardComponent implements OnInit { constructor(private tenantSvc: TenantsService) {} data: ChartData[] = []; // lineChart public lineChartElements = 27; public lineChartData1: Array = []; public lineChartData2: Array = []; public lineChartData3: Array = []; public lineChartData: Array = [ { data: this.lineChartData1, label: 'Current', backgroundColor: 'rgba(148,159,177,0.2)', borderColor: 'rgba(148,159,177,1)', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, { data: this.lineChartData2, label: 'Previous', backgroundColor: 'rgba(77,83,96,0.2)', borderColor: 'rgba(77,83,96,1)', pointBackgroundColor: 'rgba(77,83,96,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(77,83,96,1)', fill: 'origin', }, { data: this.lineChartData3, label: 'BEP', backgroundColor: 'rgba(255,0,0,0.3)', borderColor: 'red', pointBackgroundColor: 'rgba(148,159,177,1)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgba(148,159,177,0.8)', fill: 'origin', }, ]; public lineChartLegend = false; /* tslint:disable:max-line-length */ public lineChartLabels: Array = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Monday', 'Thursday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ]; /* tslint:enable:max-line-length */ public lineChartType: ChartType = 'line'; public lineChartOptions: ChartConfiguration['options'] = { maintainAspectRatio: false, elements: { line: { tension: 0.5, }, }, scales: { // We use this empty structure as a placeholder for dynamic theming. x: {}, 'y-axis-0': { position: 'left', }, 'y-axis-1': { position: 'right', grid: { color: 'rgba(80,80,80,0.3)', }, ticks: { color: '#808080', }, }, }, }; public random(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } ngOnInit(): void { for (let i = 0; i <= this.lineChartElements; i++) { this.lineChartData1.push(this.random(50, 200)); this.lineChartData2.push(this.random(80, 100)); this.lineChartData3.push(65); } } } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/tenants/create/create.component.html ================================================
Provision tenant Name Email Phone Address Plan
================================================ FILE: Solution/Lab6/client/Admin/src/app/views/tenants/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/tenants/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { TenantsService } from '../tenants.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { submitting = false; tenantForm: FormGroup = this.fb.group({ tenantName: [null, [Validators.required]], tenantEmail: [null, [Validators.email, Validators.required]], tenantTier: [null, [Validators.required]], tenantPhone: [null], tenantAddress: [null], }); constructor( private fb: FormBuilder, private tenantSvc: TenantsService, private router: Router, private _snackBar: MatSnackBar ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; const user = { ...this.tenantForm.value, }; this.tenantSvc.post(this.tenantForm.value).subscribe({ next: () => { this.submitting = false; this.openErrorMessageSnackBar('Successfully created new tenant!'); this.router.navigate(['tenants']); }, error: (err) => { this.submitting = false; this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); }, }); } } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/tenants/list/list.component.html ================================================
Tenant List
Id {{ element.tenantId }} Tenant Name {{ element.tenantName }} E-Mail {{ element.tenantEmail }} Plan {{ element.tenantTier }} Status {{ element.isActive }}
================================================ FILE: Solution/Lab6/client/Admin/src/app/views/tenants/list/list.component.scss ================================================ .tenant-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/tenants/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Tenant } from '../models/tenant'; import { TenantsService } from '../tenants.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { tenants$ = new Observable(); tenantData: Tenant[] = []; isLoading: boolean = true; displayedColumns = [ 'tenantId', 'tenantName', 'tenantEmail', 'tenantTier', 'isActive', ]; constructor(private tenantSvc: TenantsService) {} ngOnInit(): void { this.tenantSvc.fetch().subscribe((data) => { this.tenantData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/tenants/models/tenant.ts ================================================ export interface Tenant { tenantId: string; tenantName: string; tenantEmail: string; tenantTier: string; isActive: boolean; } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/tenants/tenants-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', component: ListComponent, data: { title: 'Tenant List', }, }, { path: 'create', component: CreateComponent, data: { title: 'Provision new tenant', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class TenantsRoutingModule {} ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/tenants/tenants.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ListComponent } from './list/list.component'; import { TenantsRoutingModule } from './tenants-routing.module'; import { CreateComponent } from './create/create.component'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, TenantsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ReactiveFormsModule, MatSnackBarModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class TenantsModule {} ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/tenants/tenants.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { Tenant } from './models/tenant'; @Injectable({ providedIn: 'root', }) export class TenantsService { constructor(private http: HttpClient) {} baseUrl = `${environment.apiUrl}`; tenantsApiUrl = `${this.baseUrl}/tenants`; registrationApiUrl = `${this.baseUrl}/registration`; // TODO strongly-type these anys as tenants once we dial in what the tenant call should return fetch(): Observable { return this.http.get(this.tenantsApiUrl); } post(tenant: Tenant): Observable { return this.http.post(this.registrationApiUrl, tenant); } } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
Tenant Id folder_special
================================================ FILE: Solution/Lab6/client/Admin/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], tenantId: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Solution/Lab6/client/Admin/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Solution/Lab6/client/Admin/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.apiUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } } ================================================ FILE: Solution/Lab6/client/Admin/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab6/client/Admin/src/aws-exports.ts ================================================ const awsmobile = { aws_project_region: 'us-west-2', aws_cognito_region: 'us-west-2', aws_user_pools_id: 'us-west-2_nWhwjijMc', aws_user_pools_web_client_id: '61ipvhmvdrfso0cftpil9irk8k', }; export default awsmobile; ================================================ FILE: Solution/Lab6/client/Admin/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab6/client/Admin/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab6/client/Admin/src/environments/environment.ts ================================================ export const environment = { production: false, apiUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab6/client/Admin/src/index.html ================================================ Dashboard ================================================ FILE: Solution/Lab6/client/Admin/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { Amplify } from 'aws-amplify'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import aws_exports from './aws-exports'; Amplify.configure(aws_exports); if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab6/client/Admin/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab6/client/Admin/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab6/client/Admin/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab6/client/Admin/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; ================================================ FILE: Solution/Lab6/client/Admin/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab6/client/Admin/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab6/client/Admin/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab6/client/Admin/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab6/client/Application/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab6/client/Application/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab6/client/Application/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end #cypress cypress/videos/* cypress.env.json # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab6/client/Application/README.md ================================================ # Application This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab6/client/Application/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "application": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/custom-theme.scss", "src/styles.scss" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "2mb", "maximumError": "2mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "6kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "application:build:production" }, "development": { "browserTarget": "application:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "application:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab6/client/Application/cypress/README.md ================================================ # Application End-to-End Testing ## Instructions To run End-to-End (e2e) tests against the Sample Application, take the following steps: 1. Make a copy of the example env file (`cypress.env.json.example`): ```bash cp cypress.env.json.example cypress.env.json ``` 2. Edit the new file and replace the sample values with real values. The following should help when deciding what to use in place of the sample values provided: - `host`: The URL where the Sample Application is running. If testing locally, this is usually `"http://localhost:4200"`. - `tenantName`: The name of the tenant used to identify the appropriate Cognito User Pool to use for Authentication. - `tenantUsername`: The username to use when logging in. - `tenantUserPassword`: The password to use when logging in. - `email`: The email address to use for testing. (This should be a valid email address.) 3. Navigate to the root of the Application project (`aws-saas-factory-ref-solution-serverless-saas/clients/Application/`) and run the following: ```bash npx cypress run ``` This will run the tests located in the `cypress/e2e` folder. The documentation [here](https://docs.cypress.io/guides/guides/command-line#cypress-run) has more information on what can be passed in as arguments when running the Cypress tests. For example, running the following will show the Cypress UI and what is happening as each of the tests are run: ```bash npx cypress run --headed ``` ================================================ FILE: Solution/Lab6/client/Application/cypress/e2e/1-getting-started/basic-access.cy.js ================================================ /// describe('check that the app redirects to /unauthorized when tenant is not set', () => { it('redirects to unauthorized when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); }) it('redirects to unauthorized when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); }) it('redirects to unauthorized when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); }) }) describe('check that the app redirects to a page with a sign-in form when tenant is set and user is not logged in', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').should('exist') cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.location('href').should('contain', '/dashboard') }) it('redirects when visiting orders page', () => { cy.visit(Cypress.env('host') + '/orders') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/orders') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting products page', () => { cy.visit(Cypress.env('host') + '/products') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/products') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) it('redirects when visiting a random page', () => { cy.visit(Cypress.env('host') + '/random') cy.location().should((loc) => { expect(loc.href).to.not.contain('/unauthorized') expect(loc.href).to.not.contain('/random') }); cy.get('form').within(() => { cy.get('input[name="username"]').should('exist'); cy.get('input[name="password"]').should('exist'); }) }) }) ================================================ FILE: Solution/Lab6/client/Application/cypress/e2e/1-getting-started/product-testing.cy.js ================================================ /// describe('check that product, order and user functionality works as expected', () => { beforeEach(() => { cy.visit(Cypress.env('host')) cy.get('#tenantname').type(Cypress.env('tenantName')) cy.intercept({ method: 'GET', url: '**/tenant/init/*', }).as('getTenantInfo') cy.contains('Submit').click() cy.wait('@getTenantInfo') cy.get('form input[name="username"]').type(Cypress.env('tenantUsername')) cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() cy.wait(1500) cy.get('body').then(body => { if (body.find('form input[name="confirm_password"]').length > 0) { cy.get('form input[name="password"]').type(Cypress.env('tenantUserPassword')) cy.get('form input[name="confirm_password"]').type(Cypress.env('tenantUserPassword')) cy.get('form button[type="submit"]').click() } }) }) it('can create new users and display them', () => { const email_username = Cypress.env('email').split('@')[0]; const email_domain = Cypress.env('email').split('@')[1]; const random_suffix = '+test'+Date.now().toString().slice(-3); const myUser = { name: "myUser-"+Date.now(), email: email_username + random_suffix + '@' + email_domain, role: 'userRole'+Date.now().toString().slice(-5) } cy.get("a").contains("Users").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.get("button[color='primary'").contains("Add User").click() cy.location().should((loc) => { expect(loc.href).to.contain('/users/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="userName"]').type(myUser.name) cy.get('input[formcontrolname="userEmail"]').type(myUser.email) cy.get('input[formcontrolname="userRole"]').type(myUser.role) }) cy.intercept({ method: 'POST', url: '**/user', }).as('postUser') cy.intercept({ method: 'GET', url: '**/users', }).as('getUsers') cy.get("button").contains("Create").click() cy.wait('@postUser') cy.go('back') cy.location().should((loc) => { expect(loc.href).to.contain('/users/list') }); cy.wait('@getUsers') cy.get('table td').contains(myUser.email) }) it('can create a new order with a new product and see them listed', () => { const myProduct = { name: "myProduct-"+Date.now(), price: Date.now().toString().slice(-3), sku: Date.now().toString().slice(-5), category: "category3", } const myOrder = { name: "myOrder-"+Date.now(), } // NOW TESTING PRODUCT CREATION // cy.get("a").contains("Products").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.get("button[color='primary'").contains("Create Product").click() cy.location().should((loc) => { expect(loc.href).to.contain('/products/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="name"]').type(myProduct.name) cy.get('input[formcontrolname="price"]').type(myProduct.price) cy.get('input[formcontrolname="sku"]').type(myProduct.sku) cy.get('mat-select[formcontrolname="category"]').click() }) cy.get('.mat-option-text').contains(myProduct.category).click() cy.get("button").contains("Submit").should('not.be.disabled') cy.intercept({ method: 'POST', url: '**/product', }).as('postProduct') cy.intercept({ method: 'GET', url: '**/products', }).as('getProducts') cy.get("button").contains("Submit").click() cy.wait('@postProduct') cy.location().should((loc) => { expect(loc.href).to.contain('/products/list') }); cy.wait('@getProducts') cy.get('table td').contains(myProduct.name) cy.get('table td').contains(myProduct.price) cy.get("a").contains("Orders").click() // DONE TESTING PRODUCT CREATION // // NOW TESTING ORDER CREATION // cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.get("button[color='primary']").contains("Create Order").click() cy.location().should((loc) => { expect(loc.href).to.contain('/orders/create') }); cy.get('form').within(() => { cy.get('input[formcontrolname="orderName"]').type(myOrder.name) }) cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.get('div .row').contains(myProduct.name).parent().find("button[color='primary']").click() cy.intercept({ method: 'POST', url: '**/order', }).as('postOrder') cy.intercept({ method: 'GET', url: '**/orders', }).as('getOrders') cy.get("button").contains("Submit").click() cy.wait('@postOrder') cy.location().should((loc) => { expect(loc.href).to.contain('/orders/list') }); cy.wait('@getOrders') cy.get('table td').contains(myOrder.name) cy.get('table td').contains(new Intl.NumberFormat().format(myProduct.price * 2)) // DONE TESTING ORDER CREATION // }) }) ================================================ FILE: Solution/Lab6/client/Application/cypress.config.ts ================================================ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { supportFile: false, setupNodeEvents(on, config) { // implement node event listeners here }, }, }); ================================================ FILE: Solution/Lab6/client/Application/cypress.env.json.example ================================================ { "host": "http://localhost:4200", "tenantName": "UPDATE_ME!", "tenantUsername": "UPDATE_ME!", "tenantUserPassword": "UPDATE_ME!", "email": "test@example.com" } ================================================ FILE: Solution/Lab6/client/Application/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/application'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab6/client/Application/package.json ================================================ { "name": "application", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "e2e": "npx cypress run" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "@aws-amplify/ui-angular": "~2.4.14", "aws-amplify": "~4.3.27", "bootstrap": "~5.2.0", "chart.js": "~3.8.0", "chartjs-plugin-datalabels": "~2.0.0", "ng2-charts": "~4.0.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "cypress": "~10.3.1", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab6/client/Application/src/app/_nav.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { INavData } from './models'; export const navItems: INavData[] = [ { name: 'Dashboard', url: '/dashboard', icon: 'insights', }, { name: 'Products', url: '/products', icon: 'sell', }, { name: 'Orders', url: '/orders', icon: 'shopping_cart', }, { name: 'Users', url: '/users', icon: 'supervisor_account', }, ]; ================================================ FILE: Solution/Lab6/client/Application/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { CognitoGuard } from './cognito.guard'; export const routes: Routes = [ { path: '', redirectTo: 'unauthorized', pathMatch: 'full', }, { path: '', component: NavComponent, canActivate: [CognitoGuard], data: { title: 'Home', }, children: [ { path: 'auth/info', component: AuthComponent, }, { path: 'dashboard', loadChildren: () => import('./views/dashboard/dashboard.module').then( (m) => m.DashboardModule ), }, { path: 'orders', loadChildren: () => import('./views/orders/orders.module').then((m) => m.OrdersModule), }, { path: 'products', loadChildren: () => import('./views/products/products.module').then( (m) => m.ProductsModule ), }, { path: 'users', loadChildren: () => import('./views/users/users.module').then((m) => m.UsersModule), }, ], }, { path: 'unauthorized', component: UnauthorizedComponent, }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab6/client/Application/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab6/client/Application/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'dashboard'; } ================================================ FILE: Solution/Lab6/client/Application/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { LayoutModule } from '@angular/cdk/layout'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { AmplifyAuthenticatorModule } from '@aws-amplify/ui-angular'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatNativeDateModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { UnauthorizedComponent } from './views/error/unauthorized.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NavComponent } from './nav/nav.component'; import { AuthComponent } from './views/auth/auth.component'; import { httpInterceptorProviders } from './interceptors'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; @NgModule({ declarations: [ AppComponent, NavComponent, AuthComponent, UnauthorizedComponent, ], imports: [ AmplifyAuthenticatorModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, LayoutModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, MatNativeDateModule, MatProgressSpinnerModule, MatSidenavModule, MatToolbarModule, MatSnackBarModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatIconModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, httpInterceptorProviders, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab6/client/Application/src/app/cognito.guard.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, } from '@angular/router'; import { Auth } from 'aws-amplify'; import { AuthConfigurationService } from './views/auth/auth-configuration.service'; @Injectable({ providedIn: 'root' }) export class CognitoGuard implements CanActivate { constructor( private router: Router, private authConfigService: AuthConfigurationService ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Promise { if (!this.authConfigService.configureAmplifyAuth()) { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return new Promise((res, rej) => { res(false); }); } return Auth.currentSession() .then((u) => { if (u.isValid()) { return true; } else { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); return false; } }) .catch((e) => { if (state.url === '/dashboard') { // if we're going to the dashboard and we're not logged in, // don't stop the flow as the amplify-authenticator will // route requests going to the dashboard to the sign-in page. return true; } console.log('Error getting current session', e); this.router.navigate(['/unauthorized']); return false; }); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/interceptors/auth.interceptor.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, } from '@angular/common/http'; import { from, Observable } from 'rxjs'; import { Auth } from 'aws-amplify'; import { filter, map, switchMap } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor() {} idToken = ''; intercept( req: HttpRequest, next: HttpHandler ): Observable> { if (req.url.includes('tenant/init')) { return next.handle(req); } const s = Auth.currentSession().catch((err) => console.log(err)); const session$ = from(s); return session$.pipe( filter((sesh) => !!sesh), map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), switchMap((tok) => { req = req.clone({ headers: req.headers.set('Authorization', 'Bearer ' + tok), }); return next.handle(req); }) ); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/interceptors/index.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './auth.interceptor'; /** Http interceptor providers in outside-in order */ export const httpInterceptorProviders = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, ]; ================================================ FILE: Solution/Lab6/client/Application/src/app/models/index.ts ================================================ export * from './interfaces'; ================================================ FILE: Solution/Lab6/client/Application/src/app/models/interfaces.ts ================================================ export interface INavData { name?: string; url?: string | any[]; href?: string; icon?: string; title?: boolean; children?: INavData[]; variant?: string; divider?: boolean; class?: string; } ================================================ FILE: Solution/Lab6/client/Application/src/app/nav/nav.component.html ================================================ {{ navItem.icon }} {{ navItem.name }} {{ (companyName$ | async) || "" }} {{ (username$ | async) || user.username}}
================================================ FILE: Solution/Lab6/client/Application/src/app/nav/nav.component.scss ================================================ .sidenav { width: 200px; background-color: #2f353a; } .mat-list-item { color: whitesmoke; } .sidebar-icon-container { height: 64px; background-color: whitesmoke; display: flex; align-items: center; justify-content: center; } .spacer { flex: 1 1 auto; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; } .logo { width: 100px; height: auto; } .nav-icon { color: #20a8d8; } .footer { position: fixed; bottom: 0; width: 100%; height: 40px; background-color: whitesmoke; color: #2d3337; // text-align: center; margin: 0px; } .footer-text { display: flex; } .content { position: absolute; width: 100%; height: 90%; max-height: 90%; overflow: auto; background-color: lightgray; } ================================================ FILE: Solution/Lab6/client/Application/src/app/nav/nav.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { from, Observable, of } from 'rxjs'; import { filter, map, shareReplay } from 'rxjs/operators'; import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, } from '@angular/router'; import { AuthenticatorService } from '@aws-amplify/ui-angular'; import { Auth } from 'aws-amplify'; import { navItems } from '../_nav'; import { AuthConfigurationService } from './../views/auth/auth-configuration.service'; @Component({ selector: 'app-nav', templateUrl: './nav.component.html', styleUrls: ['./nav.component.scss'], }) export class NavComponent implements OnInit { loading$: Observable = of(false); isAuthenticated$: Observable | undefined; username$: Observable | undefined; companyName$: Observable | undefined; public navItems = navItems; isHandset$: Observable = this.breakpointObserver .observe(Breakpoints.Handset) .pipe( map((result) => result.matches), shareReplay() ); constructor( private breakpointObserver: BreakpointObserver, private router: Router, private authConfigService: AuthConfigurationService ) { // this.configSvc.loadConfigurations().subscribe((val) => console.log(val)); this.loading$ = this.router.events.pipe( filter( (e) => e instanceof NavigationStart || e instanceof NavigationEnd || e instanceof NavigationCancel || e instanceof NavigationError ), map((e) => e instanceof NavigationStart) ); } ngOnInit(): void { try { const s = Auth.currentSession().catch((err) => { console.log('Failed to get current session. Err: ', err); return err; }); const session$ = from(s); this.isAuthenticated$ = session$.pipe( filter((sesh) => !!sesh), map( (sesh) => sesh && typeof sesh.isValid === 'function' && sesh.isValid() ) ); const token$ = session$.pipe( map( (sesh) => sesh && typeof sesh.getIdToken === 'function' && sesh.getIdToken() ) ); this.username$ = token$.pipe( map((t) => t && t.payload && t.payload['cognito:username']) ); this.companyName$ = token$.pipe( map((t) => t.payload && t.payload['custom:company-name']) ); } catch (err) { console.error('Unable to get current session.'); } } async logout() { await Auth.signOut({ global: true }) .then((e) => { this.authConfigService.cleanLocalStorage(); this.router.navigate(['/unauthorized']); }) .catch((err) => { console.error('Error logging out: ', err); }); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/auth/auth-configuration.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpParams, HttpParamsOptions, } from '@angular/common/http'; import { Injectable, OnInit } from '@angular/core'; import { map, switchMap, catchError } from 'rxjs/operators'; import { throwError } from 'rxjs'; import { environment } from '../../../environments/environment'; import { ConfigParams } from './models/config-params'; import { ActivatedRoute } from '@angular/router'; import Amplify from 'aws-amplify'; import { Auth } from 'aws-amplify'; import { Router } from '@angular/router'; import { from, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AuthConfigurationService { params$: Observable; params: ConfigParams; tenantName: string; constructor( private http: HttpClient, private route: ActivatedRoute, private router: Router ) {} public setTenantConfig(tenantName: string): Promise { const url = `${environment.regApiGatewayUrl}/tenant/init/` + tenantName; this.params$ = this.http.get(url); const setup$ = this.params$.pipe( map((val) => { // remove trailing slash (/) if present val.apiGatewayUrl = val.apiGatewayUrl.replace(/\/$/, ''); localStorage.setItem('userPoolId', val.userPoolId); localStorage.setItem('tenantName', tenantName); localStorage.setItem('appClientId', val.appClientId); localStorage.setItem('apiGatewayUrl', val.apiGatewayUrl); return 'success'; }), catchError((error) => { console.log('Error setting tenant config: ', error); return throwError(error); }) ); return setup$.toPromise(); } configureAmplifyAuth(): boolean { try { const userPoolId = localStorage.getItem('userPoolId'); const appClientId = localStorage.getItem('appClientId'); if (!userPoolId || !appClientId) { return false; } const region = userPoolId?.split('_')[0]; const awsmobile = { aws_project_region: region, aws_cognito_region: region, aws_user_pools_id: userPoolId, aws_user_pools_web_client_id: appClientId, }; Amplify.configure(awsmobile); return true; } catch (err) { console.error('Unable to initialize amplify auth.', err); return false; } } cleanLocalStorage() { localStorage.removeItem('tenantName'); localStorage.removeItem('userPoolId'); localStorage.removeItem('appClientId'); localStorage.removeItem('apiGatewayUrl'); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/auth/auth.component.html ================================================
Access Token
              
                {{accessToken$ | async}}
              
            
ID Token
              
                {{idToken$ | async}}
              
            



User Data

Is Authenticated: {{ isAuthenticated$ | async }}
{{ session$ | async | json }}

================================================ FILE: Solution/Lab6/client/Application/src/app/views/auth/auth.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ pre, code { font-family: monospace, monospace; } pre { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ overflow: auto; } .card { margin: 20px; } .card-header { justify-content: center; font-size: larger; font-weight: bold; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/auth/auth.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { from, Observable, pipe } from 'rxjs'; import { Auth } from 'aws-amplify'; import { CognitoUserSession } from 'amazon-cognito-identity-js'; import { map } from 'rxjs/operators'; @Component({ templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'], }) export class AuthComponent implements OnInit { session$: Observable | undefined; userData$: Observable | undefined; isAuthenticated$: Observable | undefined; checkSessionChanged$: Observable | undefined; idToken$: Observable | undefined; accessToken$: Observable | undefined; checkSessionChanged: any; constructor() {} ngOnInit(): void { this.session$ = from(Auth.currentSession()); this.accessToken$ = this.session$.pipe( map((sesh) => sesh.getAccessToken().getJwtToken()) ); this.idToken$ = this.session$.pipe( map((sesh) => sesh.getIdToken().getJwtToken()) ); this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); } async logout() { await Auth.signOut({ global: true }); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/auth/models/config-params.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ export interface ConfigParams { appClientId: string; userPoolId: string; apiGatewayUrl: string; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/dashboard/dashboard-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; const routes: Routes = [ { path: '', component: DashboardComponent, data: { title: 'Dashboard', }, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class DashboardRoutingModule {} ================================================ FILE: Solution/Lab6/client/Application/src/app/views/dashboard/dashboard.component.html ================================================

Dashboard

{{ card.title }}
================================================ FILE: Solution/Lab6/client/Application/src/app/views/dashboard/dashboard.component.scss ================================================ .grid-container { margin: 20px; } .dashboard-card { position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; } .more-button { position: absolute; top: 5px; right: 10px; border: none; } .dashboard-card-content { text-align: center; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/dashboard/dashboard.component.ts ================================================ import { Component, ViewChild } from '@angular/core'; import { map } from 'rxjs/operators'; import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js'; import { BaseChartDirective } from 'ng2-charts'; import DataLabelsPlugin from 'chartjs-plugin-datalabels'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'], }) export class DashboardComponent { @ViewChild(BaseChartDirective) chart: BaseChartDirective | undefined; public barChartOptions: ChartConfiguration['options'] = { responsive: true, maintainAspectRatio: false, // We use these empty structures as placeholders for dynamic theming. scales: { x: {}, y: { min: 10, }, }, plugins: { legend: { display: true, }, datalabels: { anchor: 'end', align: 'end', }, }, }; public barChartType: ChartType = 'bar'; public barChartPlugins = [DataLabelsPlugin]; public barChartData: ChartData<'bar'> = { labels: ['2006', '2007', '2008', '2009', '2010', '2011', '2012'], datasets: [ { data: [65, 59, 80, 81, 56, 55, 40], label: 'Series A' }, { data: [28, 48, 40, 19, 86, 27, 90], label: 'Series B' }, ], }; // events public chartClicked({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public chartHovered({ event, active, }: { event?: ChartEvent; active?: {}[]; }): void {} public randomize(): void { // Only Change 3 values this.barChartData.datasets[0].data = [ Math.round(Math.random() * 100), 59, 80, Math.round(Math.random() * 100), 56, Math.round(Math.random() * 100), 40, ]; this.chart?.update(); } /** Based on the screen size, switch from standard to one column per row */ cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe( map(({ matches }) => { if (matches) { return [ { title: 'Card 1', cols: 1, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 1 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; } return [ { title: 'Card 1', cols: 2, rows: 1 }, { title: 'Card 2', cols: 1, rows: 1 }, { title: 'Card 3', cols: 1, rows: 2 }, { title: 'Card 4', cols: 1, rows: 1 }, ]; }) ); constructor(private breakpointObserver: BreakpointObserver) {} } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/dashboard/dashboard.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { NgChartsModule } from 'ng2-charts'; import { DashboardRoutingModule } from './dashboard-routing.module'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, DashboardRoutingModule, MatButtonModule, MatCardModule, MatGridListModule, MatIconModule, MatListModule, MatMenuModule, NgChartsModule, ], }) export class DashboardModule {} ================================================ FILE: Solution/Lab6/client/Application/src/app/views/error/404.component.html ================================================

404

Oops! You're lost.

The page you are looking for was not found.

================================================ FILE: Solution/Lab6/client/Application/src/app/views/error/404.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '404.component.html', }) export class P404Component { constructor() {} } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/error/500.component.html ================================================

500

Houston, we have a problem!

The page you are looking for is temporarily unavailable.

================================================ FILE: Solution/Lab6/client/Application/src/app/views/error/500.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component } from '@angular/core'; @Component({ templateUrl: '500.component.html', }) export class P500Component { constructor() {} } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/error/unauthorized.component.html ================================================
Unauthorized Enter your tenant name and click submit below Tenant Name home
================================================ FILE: Solution/Lab6/client/Application/src/app/views/error/unauthorized.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .center-screen { display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; min-height: 100vh; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/error/unauthorized.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AuthConfigurationService } from './../auth/auth-configuration.service'; import { Observable } from 'rxjs'; import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-unauthorized', templateUrl: './unauthorized.component.html', styleUrls: ['./unauthorized.component.scss'], }) export class UnauthorizedComponent implements OnInit { tenantForm: FormGroup; params$: Observable; error = false; errorMessage: string; constructor( private fb: FormBuilder, private authConfigService: AuthConfigurationService, private _snackBar: MatSnackBar, private router: Router ) {} ngOnInit(): void { this.tenantForm = this.fb.group({ tenantName: [null, [Validators.required]], }); if (localStorage.getItem('tenantName')) { this.router.navigate(['/dashboard']); } } isFieldInvalid(field: string) { const formField = this.tenantForm.get(field); return ( formField && formField.invalid && (formField.dirty || formField.touched) ); } displayFieldCss(field: string) { return { 'is-invalid': this.isFieldInvalid(field), }; } hasRequiredError(field: string) { return !!this.tenantForm.get(field)?.hasError('required'); } openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } login() { let tenantName = this.tenantForm.value.tenantName; if (!tenantName) { this.errorMessage = 'No tenant name provided.'; this.error = true; this.openErrorMessageSnackBar(this.errorMessage); return false; } this.authConfigService .setTenantConfig(tenantName) .then((val) => { this.router.navigate(['/dashboard']); }) .catch((errorResponse) => { this.error = true; this.errorMessage = errorResponse.error.message || 'An unexpected error occurred!'; this.openErrorMessageSnackBar(this.errorMessage); }); return false; } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/create/create.component.html ================================================
Create new order Enter order name Name is required
{{ op.product.name }}
{{ op.product.price | currency }}
{{ op.quantity || 0 }}
================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/create/create.component.scss ================================================ // .card { // margin: 20px; // display: flex; // flex-direction: column; // align-items: flex-start; // } .order-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { Product } from '../../products/models/product.interface'; import { ProductService } from '../../products/product.service'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; interface LineItem { product: Product; quantity?: number; } @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { displayedColumns = []; orderForm: FormGroup; orderProducts: LineItem[] = []; isLoadingProducts: boolean = true; error = ''; constructor( private fb: FormBuilder, private productSvc: ProductService, private orderSvc: OrdersService, private router: Router ) { this.orderForm = this.fb.group({ name: ['', Validators.required], }); } ngOnInit(): void { this.productSvc.fetch().subscribe((products) => { this.orderProducts = products.map((p) => ({ product: p })); this.isLoadingProducts = false; }); this.orderForm = this.fb.group({ orderName: ['', Validators.required], }); } get name() { return this.orderForm.get('name'); } get productQuantity() { return this.orderProducts .filter((p) => !!p.quantity).length > 0; } add(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity ? orderProduct.quantity + 1 : 1, }; } return p; }); } remove(op: LineItem) { const orderProduct = this.orderProducts.find( (p) => p?.product.productId === op.product.productId ); this.orderProducts = this.orderProducts.map((p) => { if (p.product?.productId === orderProduct?.product?.productId) { p = { ...orderProduct, quantity: orderProduct.quantity && orderProduct.quantity > 1 ? orderProduct.quantity - 1 : undefined, }; } return p; }); } submit() { const val: Order = { ...this.orderForm?.value, orderProducts: this.orderProducts .filter((p) => !!p.quantity) .map((p) => ({ productId: p.product.productId, price: p.product.price, quantity: p.quantity, })), }; this.orderSvc.create(val).subscribe(() => this.router.navigate(['orders'])); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/detail/detail.component.html ================================================

Order Details mostly made up

NO: {{ orderId$ | async }} | Date: {{ today() | date }}
  • {{ tenantName() }}

Services / Products

Item / Details Unit Cost Sum Cost Discount Tax Total
{{ op.productId }}
{{ op.price | currency }}
Before Tax
{{ sum(op) | currency }}
{{ op.quantity }} Units
$0.00
None
{{ tax(op) | currency }}
Sales Tax 8.9%
{{ total(op) | currency }}
Sub Total Discount Total Tax Final
{{ subTotal(order) | currency }} -$0.00 {{ subTotal(order) | currency }} {{ calcTax(order) | currency }} {{ final(order) | currency }}
Comments / Notes
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Odit repudiandae numquam sit facere blanditiis, quasi distinctio ipsam? Libero odit ex expedita, facere sunt, possimus consectetur dolore, nobis iure amet vero.
================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/detail/detail.component.scss ================================================ /* * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .no-bottom-margin { margin-bottom: 0; } .nowrap { white-space: nowrap; } .invoice { font-family: Arial, Helvetica, sans-serif; width: 970px !important; margin: 50px auto; .invoice-header { padding: 25px 25px 15px; h1 { margin: 0; } .media { .media-body { font-size: 0.9em; margin: 0; } } } .invoice-body { border-radius: 10px; padding: 25px; background: #fff; } .invoice-footer { padding: 15px; font-size: 0.9em; text-align: center; color: #999; } } .logo { max-height: 70px; border-radius: 10px; } .dl-horizontal { margin: 0; dt { float: left; width: 80px; overflow: hidden; clear: left; text-align: right; text-overflow: ellipsis; white-space: nowrap; } dd { margin-left: 90px; } } .rowamount { padding-top: 15px !important; } .rowtotal { font-size: 1.3em; } .colfix { width: 12%; } .mono { font-family: monospace; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/detail/detail.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Order } from '../models/order.interface'; import { OrderProduct } from '../models/orderproduct.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-detail', templateUrl: './detail.component.html', styleUrls: ['./detail.component.scss'], }) export class DetailComponent implements OnInit { orderId$: Observable | undefined; order$: Observable | undefined; orderProducts$: Observable | undefined; taxRate = 0.0899; constructor(private route: ActivatedRoute, private orderSvc: OrdersService) {} ngOnInit(): void { this.orderId$ = this.route.params.pipe(map((o) => o['orderId'])); this.order$ = this.orderId$.pipe(switchMap((o) => this.orderSvc.get(o))); this.orderProducts$ = this.order$.pipe(map((o) => o.orderProducts)); } today() { return new Date(); } tenantName() { return ''; } sum(op: OrderProduct) { return op.price * op.quantity; } tax(op: OrderProduct) { return this.sum(op) * this.taxRate; } total(op: OrderProduct) { return this.sum(op) + this.tax(op); } subTotal(order: Order) { return order.orderProducts .map((op) => op.price * op.quantity) .reduce((acc, curr) => acc + curr); } calcTax(order: Order) { return this.subTotal(order) * this.taxRate; } final(order: Order) { return this.subTotal(order) + this.calcTax(order); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/list/list.component.html ================================================

Order List

Name {{ element.orderName }} Line Items {{ element.orderProducts?.length }} Total {{ sum(element) | currency }}
================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/list/list.component.scss ================================================ .order-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Order } from '../models/order.interface'; import { OrdersService } from '../orders.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { displayedColumns: string[] = ['name', 'lineItems', 'total']; orderData: Order[] = []; isLoading: boolean = true; constructor(private orderSvc: OrdersService, private router: Router) {} ngOnInit(): void { this.orderSvc.fetch().subscribe((data) => { this.isLoading = false; this.orderData = data; }); } sum(order: Order): number { return order.orderProducts .map((p) => p.price * p.quantity) .reduce((acc, curr) => acc + curr); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/models/order.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { OrderProduct } from './orderproduct.interface'; export interface Order { key: string; shardId: string; orderId: string; orderName: string; orderProducts: OrderProduct[]; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/models/orderproduct.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface OrderProduct { productId: string; price: number; quantity: number; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/orders-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'full', }, { path: 'list', data: { title: 'All Orders', }, component: ListComponent, }, { path: 'create', data: { title: 'Create Order', }, component: CreateComponent, }, { path: 'detail/:orderId', data: { title: 'View Order Detail', }, component: DetailComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class OrdersRoutingModule {} ================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/orders.module.ts ================================================ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { CreateComponent } from './create/create.component'; import { DetailComponent } from './detail/detail.component'; import { ListComponent } from './list/list.component'; import { OrdersRoutingModule } from './orders-routing.module'; @NgModule({ declarations: [CreateComponent, ListComponent, DetailComponent], imports: [ CommonModule, OrdersRoutingModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatIconModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class OrdersModule {} ================================================ FILE: Solution/Lab6/client/Application/src/app/views/orders/orders.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Order } from './models/order.interface'; @Injectable({ providedIn: 'root', }) export class OrdersService { orders: Order[] = []; baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; constructor(private http: HttpClient) {} fetch(): Observable { return this.http.get(`${this.baseUrl}/orders`); } get(orderId: string): Observable { const url = `${this.baseUrl}/order/${orderId}`; return this.http.get(url); } create(order: Order): Observable { return this.http.post(`${this.baseUrl}/order`, order); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/create/create.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required SKU Category {{category}}
================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/create/create.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; } .product-form { display: flex; flex-direction: column; align-items: flex-start; } .mat-form-field { display: flex; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/create/create.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ProductService } from '../product.service'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { productForm: FormGroup; categories: string[] = ['category1', 'category2', 'category3', 'category4']; constructor( private fb: FormBuilder, private productSvc: ProductService, private router: Router ) { this.productForm = this.fb.group({}); } ngOnInit(): void { this.productForm = this.fb.group({ name: ['', Validators.required], price: ['', Validators.required], sku: '', category: '', }); } get name() { return this.productForm.get('name'); } get price() { return this.productForm.get('price'); } submit() { this.productSvc.post(this.productForm.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err.message); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/edit/edit.component.html ================================================
Create new Product Enter product name Name is required Enter product price Price is required Description
================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/edit/edit.component.scss ================================================ ================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/edit/edit.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.scss'], }) export class EditComponent implements OnInit { productForm: FormGroup | undefined; product$: Observable | undefined; productId$: Observable | undefined; productName$: Observable | undefined; constructor( private route: ActivatedRoute, private router: Router, private productSvc: ProductService, private fb: FormBuilder ) {} ngOnInit(): void { this.productId$ = this.route.params.pipe(map((p) => p['productId'])); this.product$ = this.productId$.pipe( switchMap((p) => this.productSvc.get(p)) ); this.productName$ = this.product$.pipe(map((p) => p?.name)); this.productForm = this.fb.group({ productId: [''], name: [''], price: [''], description: [''], }); this.product$.subscribe((val) => { this.productForm?.patchValue({ ...val, }); }); } get name() { return this.productForm?.get('name'); } get price() { return this.productForm?.get('price'); } submit() { this.productSvc.put(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => console.error(err), }); } delete() { this.productSvc.delete(this.productForm?.value).subscribe({ next: () => this.router.navigate(['products']), error: (err) => { alert(err); console.error(err); }, }); } cancel() { this.router.navigate(['products']); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/list/list.component.html ================================================

Product List

Name {{ element.name }} Price {{ element.price | currency }} SKU {{ element.sku }}
================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/list/list.component.scss ================================================ .product-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/list/list.component.ts ================================================ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Product } from '../models/product.interface'; import { ProductService } from '../product.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { productData: Product[] = []; isLoading: boolean = true; displayedColumns: string[] = ['name', 'price', 'sku']; constructor(private productSvc: ProductService, private router: Router) {} ngOnInit(): void { this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onEdit(product: Product) { this.router.navigate(['products', 'edit', product.productId]); return false; } onRemove(product: Product) { this.productSvc.delete(product); this.isLoading = true; this.productSvc.fetch().subscribe((data) => { this.productData = data; this.isLoading = false; }); } onCreate() { this.router.navigate(['products', 'create']); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/models/product.interface.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface Product { key: string; shardId: string; productId: string; name: string; price: number; sku: string; category: string; pictureUrl?: string; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/product.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Product } from './models/product.interface'; @Injectable({ providedIn: 'root', }) export class ProductService { constructor(private http: HttpClient) {} baseUrl = `${localStorage.getItem('apiGatewayUrl')}`; fetch(): Observable { return this.http.get(`${this.baseUrl}/products`); } get(productId: string): Observable { const url = `${this.baseUrl}/product/${productId}`; return this.http.get(url); } delete(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.delete(url); } put(product: Product) { const url = `${this.baseUrl}/product/${product.shardId}:${product.productId}`; return this.http.put(url, product); } post(product: Product) { return this.http.post(`${this.baseUrl}/product`, product); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/products-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'Product List', }, component: ListComponent, }, { path: 'create', data: { title: 'Create new Product', }, component: CreateComponent, }, { path: 'edit/:productId', data: { title: 'Edit Product', }, component: EditComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class ProductsRoutingModule {} ================================================ FILE: Solution/Lab6/client/Application/src/app/views/products/products.module.ts ================================================ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ProductsRoutingModule } from './products-routing.module'; import { CreateComponent } from './create/create.component'; import { EditComponent } from './edit/edit.component'; import { ListComponent } from './list/list.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [CreateComponent, EditComponent, ListComponent], imports: [ CommonModule, ReactiveFormsModule, ProductsRoutingModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatProgressSpinnerModule, ], providers: [ { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, }, ], }) export class ProductsModule {} ================================================ FILE: Solution/Lab6/client/Application/src/app/views/users/create/create.component.html ================================================
Create User Upon submission, a new user account will be created and we will send an email to the provided address with login instructions. Username person
Email email A valid email must be provided.
User Role vpn_key
================================================ FILE: Solution/Lab6/client/Application/src/app/views/users/create/create.component.scss ================================================ .user-form { margin: 20px; display: flex; flex-direction: column; } .mat-card-title { display: flex; justify-content: center; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } .row { width: 100%; align-items: center; } button { margin: 4px; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/users/create/create.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UsersService } from '../users.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.scss'], }) export class CreateComponent implements OnInit { userForm: FormGroup; error: boolean = false; success: boolean = false; constructor( private fb: FormBuilder, private userSvc: UsersService, private _snackBar: MatSnackBar ) { this.userForm = this.fb.group({ userName: [null, [Validators.required]], userEmail: [null, [Validators.email, Validators.required]], userRole: [null, [Validators.required]], }); } ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } onSubmit() { const user = this.userForm.value; this.userSvc.create(user).subscribe( () => { this.success = true; this.openErrorMessageSnackBar('Successfully created new user!'); }, (err) => { this.error = true; this.openErrorMessageSnackBar('An unexpected error occurred!'); } ); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/users/list/list.component.html ================================================
User List
Email {{ element.email }} Created Date {{ element.created | date }} Modified Date {{ element.modified | date }} Status {{ element.status }} Enabled {{ element.enabled }}
================================================ FILE: Solution/Lab6/client/Application/src/app/views/users/list/list.component.scss ================================================ .user-list { margin: 20px; } table { width: 100%; } .button-panel { margin-top: 20px; } .mat-cell { margin: 20px 5px; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/users/list/list.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { Component, OnInit } from '@angular/core'; import { User } from '../models/user'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], }) export class ListComponent implements OnInit { userData: User[] = []; isLoading: boolean = true; displayedColumns: string[] = [ 'email', 'created', 'modified', 'status', 'enabled', ]; constructor(private userSvc: UsersService) {} ngOnInit(): void { this.userSvc.fetch().subscribe((data) => { this.userData = data; this.isLoading = false; }); } } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/users/models/user.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ export interface User { email: string; created?: string; modified?: string; enabled?: boolean; status?: string; verified?: boolean; } ================================================ FILE: Solution/Lab6/client/Application/src/app/views/users/users-routing.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CreateComponent } from './create/create.component'; import { ListComponent } from './list/list.component'; const routes: Routes = [ { path: '', redirectTo: 'list', pathMatch: 'prefix', }, { path: 'list', data: { title: 'All Users', }, component: ListComponent, }, { path: 'create', data: { title: 'Create User', }, component: CreateComponent, }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) export class UsersRoutingModule {} ================================================ FILE: Solution/Lab6/client/Application/src/app/views/users/users.module.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 */ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { UsersRoutingModule } from './users-routing.module'; import { ListComponent } from './list/list.component'; import { CreateComponent } from './create/create.component'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS, } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatRadioModule } from '@angular/material/radio'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ListComponent, CreateComponent], imports: [ CommonModule, UsersRoutingModule, FormsModule, ReactiveFormsModule, MatButtonModule, MatCardModule, MatDatepickerModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatRadioModule, MatSelectModule, MatTableModule, MatIconModule, MatProgressSpinnerModule, MatSnackBarModule, ], }) export class UsersModule {} ================================================ FILE: Solution/Lab6/client/Application/src/app/views/users/users.service.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { find, mergeMap, defaultIfEmpty } from 'rxjs/operators'; import { User } from './models/user'; import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UsersService { apiUrl: string; constructor(private http: HttpClient) { this.apiUrl = environment.regApiGatewayUrl; } fetch(): Observable { return this.http.get(this.apiUrl + '/users'); } create(user: User): Observable { return this.http.post(this.apiUrl + '/user', user); } update(email: string, user: User) {} } ================================================ FILE: Solution/Lab6/client/Application/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab6/client/Application/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab6/client/Application/src/environments/environment.prod.ts ================================================ export const environment = { production: true, regApiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab6/client/Application/src/environments/environment.ts ================================================ export const environment = { production: false, regApiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab6/client/Application/src/index.html ================================================ Application ================================================ FILE: Solution/Lab6/client/Application/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab6/client/Application/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab6/client/Application/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab6/client/Application/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab6/client/Application/src/styles.scss ================================================ /* You can add global styles to this file, and also import other style files */ @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); @import "~@aws-amplify/ui-angular/theme.css"; @import "~bootstrap/scss/functions"; @import "styles/variables"; // Import functions, variables, and mixins needed by other Bootstrap files @import "~bootstrap/scss/variables"; @import "~bootstrap/scss/maps"; @import "~bootstrap/scss/mixins"; // Import Bootstrap Reboot @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files @import "~bootstrap/scss/reboot"; @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes @import "~bootstrap/scss/grid"; // Add the grid system @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes @import "styles/reset"; .chart-container canvas { max-height: 250px; width: auto; } .chart-container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; } ================================================ FILE: Solution/Lab6/client/Application/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab6/client/Application/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab6/client/Application/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "strictPropertyInitialization": false, "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab6/client/Application/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab6/client/Landing/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: Solution/Lab6/client/Landing/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: Solution/Lab6/client/Landing/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db #amplify-do-not-edit-begin amplify/\#current-cloud-backend amplify/.config/local-* amplify/logs amplify/mock-data amplify/backend/amplify-meta.json amplify/backend/.temp build/ dist/ node_modules/ aws-exports.js awsconfiguration.json amplifyconfiguration.json amplifyconfiguration.dart amplify-build-config.json amplify-gradle-config.json amplifytools.xcconfig .secret-* **.sample #amplify-do-not-edit-end # ignore yarn.lock and package-lock.json files for now yarn.lock package-lock.json ================================================ FILE: Solution/Lab6/client/Landing/README.md ================================================ # Landing This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: Solution/Lab6/client/Landing/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "yarn", "analytics": false }, "newProjectRoot": "projects", "projects": { "Landing": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/custom-theme.scss", "src/styles.scss", "node_modules/font-awesome/css/font-awesome.min.css" ] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "10mb", "maximumError": "10mb" }, { "type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "10kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "Landing:build:production" }, "development": { "browserTarget": "Landing:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "Landing:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } } } ================================================ FILE: Solution/Lab6/client/Landing/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/dashboard'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: Solution/Lab6/client/Landing/package.json ================================================ { "name": "landing", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "~14.0.0", "@angular/cdk": "~14.0.4", "@angular/common": "~14.0.0", "@angular/compiler": "~14.0.0", "@angular/core": "~14.0.0", "@angular/forms": "~14.0.0", "@angular/material": "~14.0.4", "@angular/platform-browser": "~14.0.0", "@angular/platform-browser-dynamic": "~14.0.0", "@angular/router": "~14.0.0", "bootstrap": "~5.1.3", "font-awesome": "~4.7.0", "rxjs": "~7.5.0", "tslib": "~2.3.0", "zone.js": "~0.11.4" }, "devDependencies": { "@angular-devkit/build-angular": "~14.1.0", "@angular/cli": "~14.0.5", "@angular/compiler-cli": "~14.0.0", "@types/jasmine": "~4.0.0", "jasmine-core": "~4.1.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.0.0", "karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.7.2" } } ================================================ FILE: Solution/Lab6/client/Landing/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; export const routes: Routes = [ { path: '', redirectTo: 'landing', pathMatch: 'full', }, { path: 'landing', component: LandingComponent, }, { path: 'register', component: RegisterComponent, }, { path: '**', redirectTo: 'landing', pathMatch: 'full', }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ================================================ FILE: Solution/Lab6/client/Landing/src/app/app.component.scss ================================================ ================================================ FILE: Solution/Lab6/client/Landing/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', template: ` `, styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { this.matIconRegistry.addSvgIcon( 'saas-commerce', this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') ); } title = 'landing'; } ================================================ FILE: Solution/Lab6/client/Landing/src/app/app.module.ts ================================================ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { HashLocationStrategy, LocationStrategy } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { RegisterComponent } from './views/register/register.component'; import { LandingComponent } from './views/landing/landing.component'; @NgModule({ declarations: [AppComponent, LandingComponent, RegisterComponent], imports: [ AppRoutingModule, BrowserAnimationsModule, BrowserModule, HttpClientModule, MatButtonModule, MatCardModule, MatIconModule, MatProgressSpinnerModule, MatSidenavModule, AppRoutingModule, BrowserAnimationsModule, BrowserModule, FormsModule, HttpClientModule, ReactiveFormsModule, MatSnackBarModule, MatInputModule, MatFormFieldModule, ], providers: [ HttpClientModule, { provide: LocationStrategy, useClass: HashLocationStrategy, }, ], bootstrap: [AppComponent], }) export class AppModule {} ================================================ FILE: Solution/Lab6/client/Landing/src/app/views/landing/landing.component.html ================================================

Serverless SaaS Reference Architecture

It's so nice it blows your mind.

Sign up now!

Serverless SaaS Reference Architecture is so awesome.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere temporibus omnis illum, officia. Architecto voluptatibus commodi voluptatem perspiciatis eos possimus, eius at molestias quaerat magnam? Odio qui quos ipsam natus.

Serverless SaaS Reference Architecture so awesome. Makes you awesome - go sign up!

Serverless SaaS Reference Architecture so great. Makes you even greater - go sign up now. Super cheap deal!

Feel lonely? Go sign up and have a friend!

Take Serverless SaaS Reference Architecture with you everywhere you go.

Serverless SaaS Reference Architecture is all you need. Anywhere - ever. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita sapiente hic voluptatum quo sunt totam accusamus distinctio minus aliquid quis!

Love Serverless SaaS Reference Architecture. So nice! So good! Could not live without!

Satisfied Customer

Reasons to sign up this product:

  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!
  • Its the best
  • Its awesome
  • It makes you happy
  • It brings world peace
  • Its free!

Why you still reading?

Sign up now!
================================================ FILE: Solution/Lab6/client/Landing/src/app/views/landing/landing.component.scss ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // $color-red: #cc615f; $color-grey: #4b4b4b; $color-grey--light: #d3d3d3; $btn-amzn: #ec7211; $color-primary: #232f3e; $bp-s: 65.75em; //700px; $bp-xs: 34.375em; //550px; @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,800,300); *{ box-sizing: border-box; } html{ width: 100%; height:100%; margin: 0; padding: 0; } body{ width: 100%; height:100%; font-family: 'Open Sans','Helvetica Neue',Helvetica, sans-serif; font-size: 100%; line-height: 1.45; color: #141414; } a{ text-decoration: none; &:hover{ text-decoration: none; } } img{ max-width: 100%; } .btn{ display: inline-block; margin: 1rem 0; color: white; font-weight: 500; font-size: 1.3rem; background: $btn-amzn; letter-spacing: .02em; border: none; border-radius: 5px; padding: .8rem 1rem .9rem; text-shadow: 0 1px rgba(black,.3); box-shadow: 0 0 2px rgba(black,.2); @media (max-width: $bp-s){ padding: .5rem .7rem .6rem; font-size: 1rem; } &:hover{ background: lighten($btn-amzn,5%); color: #fff; } &:focus, &:active, &:focus:active{ background: darken($btn-amzn,5%); border-color: darken($btn-amzn,5%); box-shadow: 0 2px 5px 0 rgba(black,.5) inset; } } .container{ margin: 0 auto; width: 90%; max-width: 900px; } header{ color: white; background: #232f3e; padding: 10rem 0; text-align: center; position: relative; z-index: 1; overflow: hidden; @media (max-width: $bp-s){ padding: 2rem 0; } h1{ font-size: 3rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 2rem; } } h2{ font-weight: 300; font-size: 1.5rem; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } } section{ background: #fff; color: $color-grey; padding: 3.5rem 0; @media (max-width: $bp-s){ padding: 2rem 0; } &.section--primary{ background: $color-primary; color: #fff; } &.section--primary--alt{ background: desaturate(lighten($color-primary,15%),10%); color: #fff; } &.section--primary--light{ background: rgba($color-primary,.1); } &.section--grey{ background: $color-grey; color: #fff; } &.section--grey--light{ background: $color-grey--light; color: #fff; } h3{ text-align: center; font-size: 2rem; font-weight: 300; margin: 0 0 1rem; @media (max-width: $bp-s){ font-size: 1.5rem; } } li{ font-size: 1.2rem; font-weight: 300; } p{ font-size: 1.2rem; font-weight: 300; } } .col{ margin: 0 1.5%; display: inline-block; vertical-align: top; } .col-7{ @extend .col; width: 64%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-3{ @extend .col; width: 29%; @media (max-width: $bp-s){ width: 100%; margin: 0; } } .col-5{ @extend .col; width: 30%; @media (max-width: $bp-xs){ width: 60%; margin: 0; } } .details{ text-align: left; h3{ font-size: 2rem; text-align: left; } } .features{ text-align: center; padding: 1rem; &:hover{ background: rgba(white,.1); } @media (max-width: $bp-s){ width: 100%; margin: 0; text-align: left; border-bottom: 1px solid rgba(white,.2); &:last-child{ border: none; } } i{ font-size: 4rem; margin: 0 0 2rem 0; @media (max-width: $bp-s){ font-size: 1.5rem; width: 2rem; text-align: center; margin: 0 0 1rem 0; float: left; } } p{ margin: 0 0 1rem 0; font-size: 1rem; @media (max-width: $bp-s){ margin-left: 3rem; } } } blockquote{ position: relative; margin: 0; padding: 0; text-align: center; &:before{ display: inline-block; color: $color-primary; font-size: 2rem; content: '\201C'; } p{ margin: 0; display: inline; font-size: 1.5rem; @media (max-width: $bp-s){ font-size: 1.2rem; } } cite{ font-size: 1rem; display: block; margin: .5rem 0 0 1.2rem; @media (max-width: $bp-s){ font-size: .8rem; } &:before{ content: '–'; } } } footer{ background: $color-primary; color: #fff; padding: 2rem 0; text-align: center; font-size: .8rem; color: rgba(white,.4); ul{ margin: 0; padding: 0; list-style: none; li{ display: inline-block; a{ display: block; padding: .4rem .7rem; font-size: .9rem; text-decoration: none; color: rgba(white,.7); &:hover{ color: white; } } } } } .text--center{ text-align: center; } .text--left{ text-align: left; } .bg-image{ background: $color-primary; text-align: center; position: relative; z-index: 1; overflow: hidden; //text-shadow: 2px 0 5px black; &:before{ content: ''; display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; z-index: -1; background-size: 100%; background-attachment: fixed; filter: blur(3px); opacity: .8; transform: scale(1.1); } &.bg-image-2:before{ opacity: .6; background-position: center center; } } ================================================ FILE: Solution/Lab6/client/Landing/src/app/views/landing/landing.component.ts ================================================ /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-landing', templateUrl: './landing.component.html', styleUrls: ['./landing.component.scss'], }) export class LandingComponent implements OnInit { constructor(private router: Router) {} ngOnInit() {} register() { this.router.navigate(['register']); return false; } } ================================================ FILE: Solution/Lab6/client/Landing/src/app/views/register/register.component.html ================================================
Provision a new Tenant Login details will be sent to the email provided. Name Email Phone Address Plan
================================================ FILE: Solution/Lab6/client/Landing/src/app/views/register/register.component.scss ================================================ .card { margin: 20px; display: flex; flex-direction: column; align-items: flex-start; width: fit-content; } .tenant-form { display: flex; align-items: center; justify-content: center; } .wide-form { min-width: 150px; max-width: 500px; width: 100%; } .full-width { width: 100%; } .mat-form-field { width: 100%; } .button-panel { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; } button { margin: 4px; } ================================================ FILE: Solution/Lab6/client/Landing/src/app/views/register/register.component.ts ================================================ import { MatSnackBar } from '@angular/material/snack-bar'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-register', templateUrl: './register.component.html', styleUrls: ['./register.component.scss'], }) export class RegisterComponent implements OnInit { submitting: boolean = false; tenantForm: FormGroup = this.fb.group({ tenantName: ['', [Validators.required]], tenantEmail: ['', [Validators.email, Validators.required]], tenantTier: ['', [Validators.required]], tenantPhone: [''], tenantAddress: [''], }); constructor( private fb: FormBuilder, private _snackBar: MatSnackBar, private http: HttpClient ) {} ngOnInit(): void {} openErrorMessageSnackBar(errorMessage: string) { this._snackBar.open(errorMessage, 'Dismiss', { duration: 4 * 1000, // seconds }); } submit() { this.submitting = true; this.tenantForm.disable(); const tenant = { ...this.tenantForm.value, }; this.http .post(`${environment.apiGatewayUrl}/registration`, tenant) .subscribe({ next: () => { this.openErrorMessageSnackBar('Successfully created new tenant!'); this.tenantForm.reset(); this.tenantForm.enable(); this.submitting = false; }, error: (err) => { this.openErrorMessageSnackBar('An unexpected error occurred!'); console.error(err); this.tenantForm.enable(); this.submitting = false; }, }); } } ================================================ FILE: Solution/Lab6/client/Landing/src/assets/.gitkeep ================================================ ================================================ FILE: Solution/Lab6/client/Landing/src/custom-theme.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $dashboard-primary: mat.define-palette(mat.$indigo-palette); $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $dashboard-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $dashboard-theme: mat.define-light-theme(( color: ( primary: $dashboard-primary, accent: $dashboard-accent, warn: $dashboard-warn, ) )); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($dashboard-theme); html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } ================================================ FILE: Solution/Lab6/client/Landing/src/environments/environment.prod.ts ================================================ export const environment = { production: true, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab6/client/Landing/src/environments/environment.ts ================================================ export const environment = { production: false, apiGatewayUrl: 'https://duobieudcl.execute-api.us-west-2.amazonaws.com/prod', }; ================================================ FILE: Solution/Lab6/client/Landing/src/index.html ================================================ Landing ================================================ FILE: Solution/Lab6/client/Landing/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: Solution/Lab6/client/Landing/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: Solution/Lab6/client/Landing/src/styles/_variables.scss ================================================ $link-color: #673ab7; // 1 $label-margin-bottom: 0; // 2 $grid-breakpoints: ( xs: 0, // handset portrait (small, medium, large) | handset landscape (small) sm: 600px, // handset landscape (medium, large) | tablet portrait (small, large) md: 960px, // tablet landscape (small, large) lg: 1280px, // laptops and desktops xl: 1600px // large desktops, ); $container-max-widths: ( sm: 600px, md: 960px, lg: 1280px, xl: 1600px, ); ================================================ FILE: Solution/Lab6/client/Landing/src/styles/reset.scss ================================================ a { &.mat-button, &.mat-raised-button, &.mat-fab, &.mat-mini-fab, &.mat-list-item { &:hover { color: currentColor; } } } ================================================ FILE: Solution/Lab6/client/Landing/src/styles.scss ================================================ ================================================ FILE: Solution/Lab6/client/Landing/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; keys(): string[]; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); ================================================ FILE: Solution/Lab6/client/Landing/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab6/client/Landing/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2020", "module": "es2020", "lib": ["es2020", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "skipLibCheck": true } } ================================================ FILE: Solution/Lab6/client/Landing/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: Solution/Lab6/scripts/deployment.sh ================================================ # During AWS hosted events using event engine tool # we pre-provision cloudfront and s3 buckets which hosts UI code. # So that it improves this labs total execution time. # Below code checks if cloudfront and s3 buckets are # pre-provisioned or not and then concludes if the workshop # is running in AWS hosted event through event engine tool or not. IS_RUNNING_IN_EVENT_ENGINE=false PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" IS_RUNNING_IN_EVENT_ENGINE=true ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) fi echo "server code is getting deployed" cd ../server REGION=$(aws configure get region) echo "Validating server code using pylint" python3 -m pylint -E -d E0401 $(find . -iname "*.py" -not -path "./.aws-sam/*" -not -path "./TenantPipeline/node_modules/*") if [[ $? -ne 0 ]]; then echo "****ERROR: Please fix above code errors and then rerun script!!****" exit 1 fi sam build -t shared-template.yaml --use-container if [ "$IS_RUNNING_IN_EVENT_ENGINE" = true ]; then sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE AdminUserPoolCallbackURLParameter=$ADMIN_SITE_URL TenantUserPoolCallbackURLParameter=$APP_SITE_URL else sam deploy --config-file shared-samconfig.toml --region=$REGION --parameter-overrides EventEngineParameter=$IS_RUNNING_IN_EVENT_ENGINE fi echo "Pooled tenant server code is getting deployed" REGION=$(aws configure get region) sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml --region=$REGION cd ../scripts if [ "$IS_RUNNING_IN_EVENT_ENGINE" = false ]; then ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Solution/Lab6/scripts/geturl.sh ================================================ PREPROVISIONED_ADMIN_SITE=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) if [ ! -z "$PREPROVISIONED_ADMIN_SITE" ]; then echo "Workshop is running in WorkshopStudio" ADMIN_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-AdminAppSite'].Value" --output text) LANDING_APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-LandingApplicationSite'].Value" --output text) APP_SITE_URL=$(aws cloudformation list-exports --query "Exports[?Name=='Serverless-SaaS-ApplicationSite'].Value" --output text) else ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='AdminAppSite'].OutputValue" --output text) LANDING_APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='LandingApplicationSite'].OutputValue" --output text) APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name serverless-saas --query "Stacks[0].Outputs[?OutputKey=='ApplicationSite'].OutputValue" --output text) fi echo "Admin site URL: https://$ADMIN_SITE_URL" echo "Landing site URL: https://$LANDING_APP_SITE_URL" echo "App site URL: https://$APP_SITE_URL" ================================================ FILE: Solution/Lab6/scripts/test-basic-tier-throttling.sh ================================================ #!/bin/bash APP_APIGATEWAYURL=$(aws cloudformation describe-stacks --stack-name stack-pooled --query "Stacks[0].Outputs[?OutputKey=='TenantAPI'].OutputValue" --output text) get_product() { curl -X GET -H "Authorization: Bearer $1" -H "Content-Type: application/json" $APP_APIGATEWAYURL/products echo "\n $2" } for i in $(seq 1 550) do get_product $1 $i & done wait echo "All done" ================================================ FILE: Solution/Lab6/server/.gitignore ================================================ .aws-sam/ .pytest_cache/ .DS_Store .vscode/ ================================================ FILE: Solution/Lab6/server/OrderService/order_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Order: key='' def __init__(self, shardId, orderId, orderName, orderProducts): self.shardId = shardId self.orderId = orderId self.key = shardId + ':' + orderId self.orderName = orderName self.orderProducts = orderProducts class OrderProduct: def __init__(self, productId, price, quantity): self.productId = productId self.price = price self.quantity = quantity ================================================ FILE: Solution/Lab6/server/OrderService/order_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import order_service_dal from decimal import Decimal from types import SimpleNamespace from aws_lambda_powertools import Tracer tracer = Tracer() @tracer.capture_lambda_handler def get_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a order") params = event['pathParameters'] key = params['id'] logger.log_with_tenant_context(event, params) order = order_service_dal.get_order(event, key) logger.log_with_tenant_context(event, "Request completed to get a order") metrics_manager.record_metric(event, "SingleOrderRequested", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def create_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) order = order_service_dal.create_order(event, payload) logger.log_with_tenant_context(event, "Request completed to create a order") metrics_manager.record_metric(event, "OrderCreated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def update_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a order") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] order = order_service_dal.update_order(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a order") metrics_manager.record_metric(event, "OrderUpdated", "Count", 1) return utils.generate_response(order) @tracer.capture_lambda_handler def delete_order(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a order") params = event['pathParameters'] key = params['id'] response = order_service_dal.delete_order(event, key) logger.log_with_tenant_context(event, "Request completed to delete a order") metrics_manager.record_metric(event, "OrderDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the order") @tracer.capture_lambda_handler def get_orders(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all orders") response = order_service_dal.get_orders(event, tenantId) metrics_manager.record_metric(event, "OrdersRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all orders") return utils.generate_response(response) ================================================ FILE: Solution/Lab6/server/OrderService/order_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid from order_models import Order import json import utils from types import SimpleNamespace import logger import random import threading from boto3.dynamodb.conditions import Key import metrics_manager is_pooled_deploy = os.environ['IS_POOLED_DEPLOY'] table_name = os.environ['ORDER_TABLE_NAME'] dynamodb = None suffix_start = 1 suffix_end = 10 def get_order(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) response = table.get_item(Key={'shardId': shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a order', e) else: return order def delete_order(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'orderId': orderId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a order', e) else: logger.info("DeleteItem succeeded:") return response def create_order(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] table = __get_dynamodb_table(event, dynamodb) suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) order = Order(shardId, str(uuid.uuid4()), payload.orderName, payload.orderProducts) try: response = table.put_item(Item={ 'shardId':shardId, 'orderId': order.orderId, 'orderName': order.orderName, 'orderProducts': get_order_products_dict(order.orderProducts) }, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a order', e) else: logger.info("PutItem succeeded:") return order def update_order(event, payload, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] orderId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, orderId) order = Order(shardId, orderId,payload.orderName, payload.orderProducts) response = table.update_item(Key={'shardId':order.shardId, 'orderId': order.orderId}, UpdateExpression="set orderName=:orderName, " +"orderProducts=:orderProducts", ExpressionAttributeValues={ ':orderName': order.orderName, ':orderProducts': get_order_products_dict(order.orderProducts) }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a order', e) else: logger.info("UpdateItem succeeded:") return order def get_orders(event, tenantId): table = __get_dynamodb_table(event, dynamodb) get_all_products_response = [] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error("Error getting all orders") raise Exception('Error getting all orders', e) else: logger.info("Get orders succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: order = Order(item['shardId'], item['orderId'], item['orderName'], item['orderProducts']) get_all_products_response.append(order) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def __get_dynamodb_table(event, dynamodb): """ Determine the table name based upo pooled vs silo model Args: event ([type]): [description] Returns: [type]: [description] """ if (is_pooled_deploy=='true'): accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken ) else: if not dynamodb: dynamodb = boto3.resource('dynamodb') return dynamodb.Table(table_name) def get_order_products_dict(orderProducts): orderProductList = [] for i in range(len(orderProducts)): product = orderProducts[i] orderProductList.append(vars(product)) return orderProductList ================================================ FILE: Solution/Lab6/server/OrderService/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab6/server/ProductService/product_models.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 class Product: key ='' def __init__(self, shardId, productId, sku, name, price, category): self.shardId = shardId self.productId = productId self.key = shardId + ':' + productId self.sku = sku self.name = name self.price = price self.category = category class Category: def __init__(self, id, name): self.id = id self.name = name ================================================ FILE: Solution/Lab6/server/ProductService/product_service.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils import logger import metrics_manager import product_service_dal from aws_lambda_powertools import Tracer from decimal import Decimal from types import SimpleNamespace tracer = Tracer() @tracer.capture_lambda_handler def get_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get a product") params = event['pathParameters'] logger.log_with_tenant_context(event, params) key = params['id'] logger.log_with_tenant_context(event, key) product = product_service_dal.get_product(event, key) logger.log_with_tenant_context(event, "Request completed to get a product") metrics_manager.record_metric(event, "SingleProductRequested", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def create_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to create a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) product = product_service_dal.create_product(event, payload) logger.log_with_tenant_context(event, "Request completed to create a product") metrics_manager.record_metric(event, "ProductCreated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def update_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to update a product") payload = json.loads(event['body'], object_hook=lambda d: SimpleNamespace(**d), parse_float=Decimal) params = event['pathParameters'] key = params['id'] product = product_service_dal.update_product(event, payload, key) logger.log_with_tenant_context(event, "Request completed to update a product") metrics_manager.record_metric(event, "ProductUpdated", "Count", 1) return utils.generate_response(product) @tracer.capture_lambda_handler def delete_product(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to delete a product") params = event['pathParameters'] key = params['id'] response = product_service_dal.delete_product(event, key) logger.log_with_tenant_context(event, "Request completed to delete a product") metrics_manager.record_metric(event, "ProductDeleted", "Count", 1) return utils.create_success_response("Successfully deleted the product") @tracer.capture_lambda_handler def get_products(event, context): tenantId = event['requestContext']['authorizer']['tenantId'] tracer.put_annotation(key="TenantId", value=tenantId) logger.log_with_tenant_context(event, "Request received to get all products") response = product_service_dal.get_products(event, tenantId) metrics_manager.record_metric(event, "ProductsRetrieved", "Count", len(response)) logger.log_with_tenant_context(event, "Request completed to get all products") return utils.generate_response(response) ================================================ FILE: Solution/Lab6/server/ProductService/product_service_dal.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from pprint import pprint import os import boto3 from botocore.exceptions import ClientError import uuid import json import logger import random import threading import metrics_manager from product_models import Product from types import SimpleNamespace from boto3.dynamodb.conditions import Key is_pooled_deploy = os.environ['IS_POOLED_DEPLOY'] table_name = os.environ['PRODUCT_TABLE_NAME'] dynamodb = None suffix_start = 1 suffix_end = 10 def get_product(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) response = table.get_item(Key={'shardId': shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') item = response['Item'] product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting a product', e) else: logger.info("GetItem succeeded:"+ str(product)) return product def delete_product(event, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] response = table.delete_item(Key={'shardId':shardId, 'productId': productId}, ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error deleting a product', e) else: logger.info("DeleteItem succeeded:") return response def create_product(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] table = __get_dynamodb_table(event, dynamodb) suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) product = Product(shardId, str(uuid.uuid4()), payload.sku,payload.name, payload.price, payload.category) try: response = table.put_item( Item= { 'shardId': shardId, 'productId': product.productId, 'sku': product.sku, 'name': product.name, 'price': product.price, 'category': product.category }, ReturnConsumedCapacity='TOTAL' ) metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: logger.info("PutItem succeeded:") return product def update_product(event, payload, key): table = __get_dynamodb_table(event, dynamodb) try: shardId = key.split(":")[0] productId = key.split(":")[1] logger.log_with_tenant_context(event, shardId) logger.log_with_tenant_context(event, productId) product = Product(shardId,productId,payload.sku, payload.name, payload.price, payload.category) response = table.update_item(Key={'shardId':product.shardId, 'productId': product.productId}, UpdateExpression="set sku=:sku, #n=:productName, price=:price, category=:category", ExpressionAttributeNames= {'#n':'name'}, ExpressionAttributeValues={ ':sku': product.sku, ':productName': product.name, ':price': product.price, ':category': product.category }, ReturnValues="UPDATED_NEW", ReturnConsumedCapacity='TOTAL') metrics_manager.record_metric(event, "WriteCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error updating a product', e) else: logger.info("UpdateItem succeeded:") return product def get_products(event, tenantId): table = __get_dynamodb_table(event, dynamodb) get_all_products_response =[] try: __query_all_partitions(tenantId,get_all_products_response, table, event) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error getting all products', e) else: logger.info("Get products succeeded") return get_all_products_response def __query_all_partitions(tenantId,get_all_products_response, table, event): threads = [] for suffix in range(suffix_start, suffix_end): partition_id = tenantId+'-'+str(suffix) thread = threading.Thread(target=__get_tenant_data, args=[partition_id, get_all_products_response, table, event]) threads.append(thread) # Start threads for thread in threads: thread.start() # Ensure all threads are finished for thread in threads: thread.join() def __get_tenant_data(partition_id, get_all_products_response, table, event): logger.info(partition_id) response = table.query(KeyConditionExpression=Key('shardId').eq(partition_id), ReturnConsumedCapacity='TOTAL') if (len(response['Items']) > 0): for item in response['Items']: product = Product(item['shardId'], item['productId'], item['sku'], item['name'], item['price'], item['category']) get_all_products_response.append(product) metrics_manager.record_metric(event, "ReadCapacityUnits", "Count", response['ConsumedCapacity']['CapacityUnits']) def __get_dynamodb_table(event, dynamodb): if (is_pooled_deploy=='true'): accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken ) else: if not dynamodb: dynamodb = boto3.resource('dynamodb') return dynamodb.Table(table_name) ================================================ FILE: Solution/Lab6/server/ProductService/requirements.txt ================================================ requests pytest-mock aws-lambda-powertools[Tracer,Logger,Metrics] jsonpickle aws_requests_auth ================================================ FILE: Solution/Lab6/server/README.md ================================================ sam build -t shared-template.yaml --use-container sam deploy --config-file shared-samconfig.toml sam build -t tenant-template.yaml --use-container sam deploy --config-file tenant-samconfig.toml ================================================ FILE: Solution/Lab6/server/Resources/requirements.txt ================================================ python-jose[cryptography] ================================================ FILE: Solution/Lab6/server/Resources/shared_service_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import auth_manager import utils region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] api_key_operation_user = os.environ['OPERATION_USERS_API_KEY'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) if(auth_manager.isSaaSProvider(unauthorized_claims['custom:userRole'])): userpool_id = user_pool_operation_user appclient_id = app_client_operation_user api_key = api_key_operation_user else: #get tenant user pool and app client to validate jwt token against tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': unauthorized_claims['custom:tenantId'] } ) logger.info(tenant_details) userpool_id = tenant_details['Item']['userPoolId'] appclient_id = tenant_details['Item']['appClientId'] api_key = tenant_details['Item']['apiKey'] #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] #only tenant admin and system admin can do certain actions like create and disable users if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): policy.allowAllMethods() if (auth_manager.isTenantAdmin(user_role)): policy.denyMethod(HttpVerb.POST, "tenant-activation") policy.denyMethod(HttpVerb.GET, "tenants") else: #if not tenant admin or system admin then only allow to get info and update info policy.allowMethod(HttpVerb.GET, "user/*") policy.allowMethod(HttpVerb.PUT, "user/*") authResponse = policy.build() # Generate STS credentials to be used for FGAC # Important Note: # We are generating STS token inside Authorizer to take advantage of the caching behavior of authorizer # Another option is to generate the STS token inside the lambda function itself, as mentioned in this blog post: https://aws.amazon.com/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/ # Finally, you can also consider creating one Authorizer per microservice in cases where you want the IAM policy specific to that service iam_policy = auth_manager.getPolicyForUser(user_role, utils.Service_Identifier.SHARED_SERVICES.value, tenant_id, region, aws_account_id) logger.info(iam_policy) role_arn = "arn:aws:iam::{}:role/authorizer-access-role".format(aws_account_id) assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName="tenant-aware-session", Policy=iam_policy, ) credentials = assumed_role["Credentials"] #pass sts credentials to lambda context = { 'accesskey': credentials['AccessKeyId'], # $context.authorizer.key -> value 'secretkey' : credentials['SecretAccessKey'], 'sessiontoken' : credentials["SessionToken"], 'userName': user_name, 'tenantId': tenant_id, 'userPoolId': userpool_id, 'apiKey': api_key, 'userRole': user_role } authResponse['context'] = context authResponse['usageIdentifierKey'] = api_key return authResponse def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Solution/Lab6/server/Resources/tenant_authorizer.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import re import json import os import urllib.request import boto3 import time import logger from jose import jwk, jwt from jose.utils import base64url_decode import auth_manager import utils region = os.environ['AWS_REGION'] sts_client = boto3.client("sts", region_name=region) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') user_pool_operation_user = os.environ['OPERATION_USERS_USER_POOL'] app_client_operation_user = os.environ['OPERATION_USERS_APP_CLIENT'] api_key_operation_user = os.environ['OPERATION_USERS_API_KEY'] def lambda_handler(event, context): #get JWT token after Bearer from authorization token = event['authorizationToken'].split(" ") if (token[0] != 'Bearer'): raise Exception('Authorization header should have a format Bearer Token') jwt_bearer_token = token[1] logger.info("Method ARN: " + event['methodArn']) #only to get tenant id to get user pool info unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) logger.info(unauthorized_claims) if(auth_manager.isSaaSProvider(unauthorized_claims['custom:userRole'])): userpool_id = user_pool_operation_user appclient_id = app_client_operation_user api_key = api_key_operation_user else: #get tenant user pool and app client to validate jwt token against tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': unauthorized_claims['custom:tenantId'] } ) logger.info(tenant_details) userpool_id = tenant_details['Item']['userPoolId'] appclient_id = tenant_details['Item']['appClientId'] apigateway_url = tenant_details['Item']['apiGatewayUrl'] api_key = tenant_details['Item']['apiKey'] #get keys for tenant user pool to validate keys_url = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(region, userpool_id) with urllib.request.urlopen(keys_url) as f: response = f.read() keys = json.loads(response.decode('utf-8'))['keys'] #authenticate against cognito user pool using the key response = validateJWT(jwt_bearer_token, appclient_id, keys) #get authenticated claims if (response == False): logger.error('Unauthorized') raise Exception('Unauthorized') else: logger.info(response) principal_id = response["sub"] user_name = response["cognito:username"] tenant_id = response["custom:tenantId"] user_role = response["custom:userRole"] tmp = event['methodArn'].split(':') api_gateway_arn_tmp = tmp[5].split('/') aws_account_id = tmp[4] policy = AuthPolicy(principal_id, aws_account_id) policy.restApiId = api_gateway_arn_tmp[0] policy.region = tmp[3] policy.stage = api_gateway_arn_tmp[1] if (auth_manager.isSaaSProvider(user_role) == False): if (isTenantAuthorizedForThisAPI(apigateway_url, api_gateway_arn_tmp[0]) == False): logger.error('Unauthorized') raise Exception('Unauthorized') #roles are not fine-grained enough to allow selectively policy.allowAllMethods() authResponse = policy.build() # Generate STS credentials to be used for FGAC # Important Note: # We are generating STS token inside Authorizer to take advantage of the caching behavior of authorizer # Another option is to generate the STS token inside the lambda function itself, as mentioned in this blog post: https://aws.amazon.com/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/ # Finally, you can also consider creating one Authorizer per microservice in cases where you want the IAM policy specific to that service iam_policy = auth_manager.getPolicyForUser(user_role, utils.Service_Identifier.BUSINESS_SERVICES.value, tenant_id, region, aws_account_id) logger.info(iam_policy) role_arn = "arn:aws:iam::{}:role/authorizer-access-role".format(aws_account_id) assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName="tenant-aware-session", Policy=iam_policy, ) credentials = assumed_role["Credentials"] #pass sts credentials to lambda context = { 'accesskey': credentials['AccessKeyId'], # $context.authorizer.key -> value 'secretkey' : credentials['SecretAccessKey'], 'sessiontoken' : credentials["SessionToken"], 'userName': user_name, 'tenantId': tenant_id, 'userPoolId': userpool_id, 'apiKey': api_key, 'userRole': user_role } authResponse['context'] = context authResponse['usageIdentifierKey'] = api_key return authResponse def isTenantAuthorizedForThisAPI(apigateway_url, current_api_id): if(apigateway_url.split('.')[0] != 'https://' + current_api_id): return False else: return True def validateJWT(token, app_client_id, keys): # get the kid from the headers prior to verification headers = jwt.get_unverified_headers(token) kid = headers['kid'] # search for the kid in the downloaded public keys key_index = -1 for i in range(len(keys)): if kid == keys[i]['kid']: key_index = i break if key_index == -1: logger.info('Public key not found in jwks.json') return False # construct the public key public_key = jwk.construct(keys[key_index]) # get the last two sections of the token, # message and signature (encoded in base64) message, encoded_signature = str(token).rsplit('.', 1) # decode the signature decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) # verify the signature if not public_key.verify(message.encode("utf8"), decoded_signature): logger.info('Signature verification failed') return False logger.info('Signature successfully verified') # since we passed the verification, we can now safely # use the unverified claims claims = jwt.get_unverified_claims(token) # additionally we can verify the token expiration if time.time() > claims['exp']: logger.info('Token is expired') return False # and the Audience (use claims['client_id'] if verifying an access token) if claims['aud'] != app_client_id: logger.info('Token was not issued for this audience') return False # now we can use the claims logger.info(claims) return claims class HttpVerb: GET = "GET" POST = "POST" PUT = "PUT" PATCH = "PATCH" HEAD = "HEAD" DELETE = "DELETE" OPTIONS = "OPTIONS" ALL = "*" class AuthPolicy(object): awsAccountId = "" """The AWS account id the policy will be generated for. This is used to create the method ARNs.""" principalId = "" """The principal used for the policy, this should be a unique identifier for the end user.""" version = "2012-10-17" """The policy version used for the evaluation. This should always be '2012-10-17'""" pathRegex = "^[/.a-zA-Z0-9-\*]+$" """The regular expression used to validate resource paths for the policy""" """these are the internal lists of allowed and denied methods. These are lists of objects and each object has 2 properties: A resource ARN and a nullable conditions statement. the build method processes these lists and generates the approriate statements for the final policy""" allowMethods = [] denyMethods = [] restApiId = "*" """The API Gateway API id. By default this is set to '*'""" region = "*" """The region where the API is deployed. By default this is set to '*'""" stage = "*" """The name of the stage used in the policy. By default this is set to '*'""" def __init__(self, principal, awsAccountId): self.awsAccountId = awsAccountId self.principalId = principal self.allowMethods = [] self.denyMethods = [] def _addMethod(self, effect, verb, resource, conditions): """Adds a method to the internal lists of allowed or denied methods. Each object in the internal list contains a resource ARN and a condition statement. The condition statement can be null.""" if verb != "*" and not hasattr(HttpVerb, verb): raise NameError("Invalid HTTP verb " + verb + ". Allowed verbs in HttpVerb class") resourcePattern = re.compile(self.pathRegex) if not resourcePattern.match(resource): raise NameError("Invalid resource path: " + resource + ". Path should match " + self.pathRegex) if resource[:1] == "/": resource = resource[1:] resourceArn = ("arn:aws:execute-api:" + self.region + ":" + self.awsAccountId + ":" + self.restApiId + "/" + self.stage + "/" + verb + "/" + resource) if effect.lower() == "allow": self.allowMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) elif effect.lower() == "deny": self.denyMethods.append({ 'resourceArn' : resourceArn, 'conditions' : conditions }) def _getEmptyStatement(self, effect): """Returns an empty statement object prepopulated with the correct action and the desired effect.""" statement = { 'Action': 'execute-api:Invoke', 'Effect': effect[:1].upper() + effect[1:].lower(), 'Resource': [] } return statement def _getStatementForEffect(self, effect, methods): """This function loops over an array of objects containing a resourceArn and conditions statement and generates the array of statements for the policy.""" statements = [] if len(methods) > 0: statement = self._getEmptyStatement(effect) for curMethod in methods: if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: statement['Resource'].append(curMethod['resourceArn']) else: conditionalStatement = self._getEmptyStatement(effect) conditionalStatement['Resource'].append(curMethod['resourceArn']) conditionalStatement['Condition'] = curMethod['conditions'] statements.append(conditionalStatement) statements.append(statement) return statements def allowAllMethods(self): """Adds a '*' allow to the policy to authorize access to all methods of an API""" self._addMethod("Allow", HttpVerb.ALL, "*", []) def denyAllMethods(self): """Adds a '*' allow to the policy to deny access to all methods of an API""" self._addMethod("Deny", HttpVerb.ALL, "*", []) def allowMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods for the policy""" self._addMethod("Allow", verb, resource, []) def denyMethod(self, verb, resource): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods for the policy""" self._addMethod("Deny", verb, resource, []) def allowMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of allowed methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Allow", verb, resource, conditions) def denyMethodWithConditions(self, verb, resource, conditions): """Adds an API Gateway method (Http verb + Resource path) to the list of denied methods and includes a condition for the policy statement. More on AWS policy conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition""" self._addMethod("Deny", verb, resource, conditions) def build(self): """Generates the policy document based on the internal lists of allowed and denied conditions. This will generate a policy with two main statements for the effect: one statement for Allow and one statement for Deny. Methods that includes conditions will have their own statement in the policy.""" if ((self.allowMethods is None or len(self.allowMethods) == 0) and (self.denyMethods is None or len(self.denyMethods) == 0)): raise NameError("No statements defined for the policy") policy = { 'principalId' : self.principalId, 'policyDocument' : { 'Version' : self.version, 'Statement' : [] } } policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Allow", self.allowMethods)) policy['policyDocument']['Statement'].extend(self._getStatementForEffect("Deny", self.denyMethods)) return policy ================================================ FILE: Solution/Lab6/server/TenantManagementService/events/env.json ================================================ { "CreateTenantAdminUserFunction": { "DEFAULT_USER_POOL_ID": "us-west-2_uliP336sh", "LOG_LEVEL": "INFO" }, "RegisterTenantFunction": { "CREATE_TENANT_ADMIN_USER_FUNCTION": "arn:aws:lambda:us-west-2:779954754415:function:serverless-saas-admin-CreateTenantAdminUserFunctio-D5K1EGEMC4QG", "CREATE_TENANT_FUNCTION": "arn:aws:lambda:us-west-2:779954754415:function:serverless-saas-admin-CreateTenantFunction-13GSSCVNKTV71", "PREMIUM_TIER_API_KEY": "yy", "STANDARD_TIER_API_KEY": "xx", "BASIC_TIER_API_KEY": "zz", "LOG_LEVEL": "DEBUG" } } ================================================ FILE: Solution/Lab6/server/TenantManagementService/events/tenant-registration.json ================================================ { "body": "{\"tenantName\": \"First Tenant\", \"tenantAddress\": \"123 St\", \"tenantEmail\": \"a@a.com\", \"tenantPhone\": \"1234567890\", \"tenantTier\": \"Standard\", \"tenantId\": \"62d1f8793b5e11eb876\", \"apiKey\": \"62d2062a3b5e11eb839457148f07121x\"}"} ================================================ FILE: Solution/Lab6/server/TenantManagementService/events/update_users_apikey_by_tenant.json ================================================ { "body": "{\"tenantId\": \"94d1bc6976ef11eb80cb81ceaed21f3f\", \"userPoolId\": \"us-west-2_vvN1hB5pd\", \"apiKey\": \"6db2bdc2-6d96-11eb-a56f-38f9d33cfd0f\"}" } ================================================ FILE: Solution/Lab6/server/TenantManagementService/events/user-management.json ================================================ { "tenantName": "First Tenant", "tenantAddress": "123 St", "tenantEmail": "a@a.com", "tenantPhone": "1234567890", "tenantTier": "Standard", "dedicatedTenancy": "true", "tenantId": "62d1f8793b5e11eb876", "apiKey": "62d2062a3b5e11eb839457148f07121x" } ================================================ FILE: Solution/Lab6/server/TenantManagementService/requirements.txt ================================================ requests aws_requests_auth ================================================ FILE: Solution/Lab6/server/TenantManagementService/tenant-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import os import json import boto3 from boto3.dynamodb.conditions import Key import urllib.parse import utils from botocore.exceptions import ClientError import logger import metrics_manager import auth_manager import requests from aws_requests_auth.aws_auth import AWSRequestsAuth from aws_lambda_powertools import Tracer tracer = Tracer() region = os.environ['AWS_REGION'] #This method has been locked down to be only def create_tenant(event, context): api_gateway_url = '' tenant_details = json.loads(event['body']) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails')#TODO: read table names from env vars table_system_settings = dynamodb.Table('ServerlessSaaS-Settings') try: # for pooled tenants the apigateway url is saving in settings during stack creation # update from there during tenant creation if(tenant_details['dedicatedTenancy'].lower()!= 'true'): settings_response = table_system_settings.get_item( Key={ 'settingName': 'apiGatewayUrl-Pooled' } ) api_gateway_url = settings_response['Item']['settingValue'] response = table_tenant_details.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'tenantName' : tenant_details['tenantName'], 'tenantAddress': tenant_details['tenantAddress'], 'tenantEmail': tenant_details['tenantEmail'], 'tenantPhone': tenant_details['tenantPhone'], 'tenantTier': tenant_details['tenantTier'], 'apiKey': tenant_details['apiKey'], 'userPoolId': tenant_details['userPoolId'], 'appClientId': tenant_details['appClientId'], 'dedicatedTenancy': tenant_details['dedicatedTenancy'], 'isActive': True, 'apiGatewayUrl': api_gateway_url } ) except Exception as e: raise Exception('Error creating a new tenant', e) else: return utils.create_success_response("Tenant Created") def get_tenants(event, context): table_tenant_details = __getTenantManagementTable(event) try: response = table_tenant_details.scan() except Exception as e: raise Exception('Error getting all tenants', e) else: return utils.generate_response(response['Items']) @tracer.capture_lambda_handler def update_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_details = json.loads(event['body']) tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): exiting_tenant_details = table_tenant_details.get_item( Key={ 'tenantId': tenant_id, } ) if (exiting_tenant_details['Item']['tenantTier'].upper() != tenant_details['tenantTier'].upper()): api_key = __getApiKey(tenant_details['tenantTier']) else: api_key = exiting_tenant_details['Item']['apiKey'] response_update = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set tenantName = :tenantName, tenantAddress = :tenantAddress, tenantEmail = :tenantEmail, tenantPhone = :tenantPhone, tenantTier=:tenantTier, apiKey=:apiKey", ExpressionAttributeValues={ ':tenantName' : tenant_details['tenantName'], ':tenantAddress': tenant_details['tenantAddress'], ':tenantEmail': tenant_details['tenantEmail'], ':tenantPhone': tenant_details['tenantPhone'], ':tenantTier': tenant_details['tenantTier'], ':apiKey': api_key }, ReturnValues="UPDATED_NEW" ) logger.log_with_tenant_context(event, response_update) logger.log_with_tenant_context(event, "Request completed to update tenant") return utils.create_success_response("Tenant Updated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get tenant details") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): tenant_details = table_tenant_details.get_item( Key={ 'tenantId': tenant_id, }, AttributesToGet=[ 'tenantName', 'tenantAddress', 'tenantEmail', 'tenantPhone' ] ) item = tenant_details['Item'] tenant_info = TenantInfo(item['tenantName'], item['tenantAddress'],item['tenantEmail'], item['tenantPhone']) logger.log_with_tenant_context(event, tenant_info) logger.log_with_tenant_context(event, "Request completed to get tenant details") return utils.create_success_response(tenant_info.__dict__) else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def deactivate_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) url_disable_users = os.environ['DISABLE_USERS_BY_TENANT'] url_deprovision_tenant = os.environ['DEPROVISION_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to deactivate tenant") if ((auth_manager.isTenantAdmin(user_role) and tenant_id == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': False }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) if (response["Attributes"]["dedicatedTenancy"].upper() == "TRUE"): update_details = {} update_details['tenantId'] = tenant_id update_user_response = __invoke_deprovision_tenant(update_details, headers, auth, host, stage_name, url_deprovision_tenant) update_details = {} update_details['userPoolId'] = response["Attributes"]['userPoolId'] update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_disable_users(update_details, headers, auth, host, stage_name, url_disable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to deactivate tenant") return utils.create_success_response("Tenant Deactivated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can deactivate tenant!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def activate_tenant(event, context): table_tenant_details = __getTenantManagementTable(event) url_enable_users = os.environ['ENABLE_USERS_BY_TENANT'] url_provision_tenant = os.environ['PROVISION_TENANT'] stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) requesting_tenant_id = event['requestContext']['authorizer']['tenantId'] user_role = event['requestContext']['authorizer']['userRole'] tenant_id = event['pathParameters']['tenantid'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to activate tenant") if (auth_manager.isSystemAdmin(user_role)): response = table_tenant_details.update_item( Key={ 'tenantId': tenant_id, }, UpdateExpression="set isActive = :isActive", ExpressionAttributeValues={ ':isActive': True }, ReturnValues="ALL_NEW" ) logger.log_with_tenant_context(event, response) if (response["Attributes"]["dedicatedTenancy"].upper() == "TRUE"): update_details = {} update_details['tenantId'] = tenant_id provision_response = __invoke_provision_tenant(update_details, headers, auth, host, stage_name, url_provision_tenant) logger.log_with_tenant_context(event, provision_response) update_details = {} update_details['userPoolId'] = response["Attributes"]['userPoolId'] update_details['tenantId'] = tenant_id update_details['requestingTenantId'] = requesting_tenant_id update_details['userRole'] = user_role update_user_response = __invoke_enable_users(update_details, headers, auth, host, stage_name, url_enable_users) logger.log_with_tenant_context(event, update_user_response) logger.log_with_tenant_context(event, "Request completed to activate tenant") return utils.create_success_response("Tenant Activated") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only system admin can activate tenant!") return utils.create_unauthorized_response() def load_tenant_config(event, context): params = event['pathParameters'] tenantName = urllib.parse.unquote(params['tenantname']) dynamodb = boto3.resource('dynamodb') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails')#TODO: read table names from env vars try: response = table_tenant_details.query( IndexName="ServerlessSaas-TenantConfig", KeyConditionExpression=Key('tenantName').eq(tenantName), ProjectionExpression="userPoolId, appClientId, apiGatewayUrl" ) except Exception as e: raise Exception('Error getting tenant config', e) else: if (response['Count'] == 0): return utils.create_notfound_response("Tenant not found."+ "Please enter exact tenant name used during tenant registration.") else: return utils.generate_response(response['Items'][0]) def __invoke_disable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url]) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while disabling users for the tenant') except Exception as e: logger.error('Error occured while disabling users for the tenant') raise Exception('Error occured while disabling users for the tenant', e) else: return "Success invoking disable users" def __invoke_deprovision_tenant(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url + update_details['tenantId']]) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while deprovisioning tenant') except Exception as e: logger.error('Error occured while deprovisioning tenant') raise Exception('Error occured while deprovisioning tenant', e) else: return "Success invoking deprovision tenant" def __invoke_enable_users(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url]) response = requests.put(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while enabling users for the tenant') except Exception as e: logger.error('Error occured while enabling users for the tenant') raise Exception('Error occured while enabling users for the tenant', e) else: return "Success invoking enable users" def __invoke_provision_tenant(update_details, headers, auth, host, stage_name, invoke_url): try: url = ''.join(['https://', host, '/', stage_name, invoke_url]) response = requests.post(url, data=json.dumps(update_details), auth=auth, headers=headers) logger.info(response.status_code) if (int(response.status_code) != int(utils.StatusCodes.SUCCESS.value)): raise Exception('Error occured while provisioning tenant') except Exception as e: logger.error('Error occured while provisioning tenant') raise Exception('Error occured while provisioning tenant', e) else: return "Success invoking provision tenant" def __getApiKey(tenant_tier): if (tenant_tier.upper() == utils.TenantTier.PLATINUM.value.upper()): return os.environ['PLATINUM_TIER_API_KEY'] elif (tenant_tier.upper() == utils.TenantTier.PREMIUM.value.upper()): return os.environ['PREMIUM_TIER_API_KEY'] elif (tenant_tier.upper() == utils.TenantTier.STANDARD.value.upper()): return os.environ['STANDARD_TIER_API_KEY'] elif (tenant_tier.upper() == utils.TenantTier.BASIC.value.upper()): return os.environ['BASIC_TIER_API_KEY'] def __getTenantManagementTable(event): accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken) table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails')#TODO: read table names from env vars return table_tenant_details class TenantInfo: def __init__(self, tenant_name, tenant_address, tenant_email, tenant_phone): self.tenant_name = tenant_name self.tenant_address = tenant_address self.tenant_email = tenant_email self.tenant_phone = tenant_phone ================================================ FILE: Solution/Lab6/server/TenantManagementService/tenant-provisioning.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import utils from botocore.exceptions import ClientError import logger import os from aws_lambda_powertools import Tracer tracer = Tracer() tenant_stack_mapping_table_name = os.environ['TENANT_STACK_MAPPING_TABLE_NAME'] dynamodb = boto3.resource('dynamodb') codepipeline = boto3.client('codepipeline') cloudformation = boto3.client('cloudformation') table_tenant_stack_mapping = dynamodb.Table(tenant_stack_mapping_table_name) stack_name = 'stack-{0}' @tracer.capture_lambda_handler def provision_tenant(event, context): tenant_details = json.loads(event['body']) try: response_ddb = table_tenant_stack_mapping.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'stackName': stack_name.format(tenant_details['tenantId']), 'applyLatestRelease': True, 'codeCommitId': '' } ) logger.info(response_ddb) response_codepipeline = codepipeline.start_pipeline_execution( name='serverless-saas-pipeline' ) logger.info(response_ddb) except Exception as e: raise else: return utils.create_success_response("Tenant Provisioning Started") @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def deprovision_tenant(event, context): logger.info("Request received to deprovision a tenant") tenantid_to_deprovision = event['tenantId'] try: response_ddb = table_tenant_stack_mapping.delete_item( Key={ 'tenantId': tenantid_to_deprovision } ) logger.info(response_ddb) response_cloudformation = cloudformation.delete_stack( StackName=stack_name.format(tenantid_to_deprovision) ) logger.info(response_cloudformation) except Exception as e: raise else: return utils.create_success_response("Tenant Deprovisioning Started") ================================================ FILE: Solution/Lab6/server/TenantManagementService/tenant-registration.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import utils import uuid import logger import requests import re region = os.environ['AWS_REGION'] create_tenant_admin_user_resource_path = os.environ['CREATE_TENANT_ADMIN_USER_RESOURCE_PATH'] create_tenant_resource_path = os.environ['CREATE_TENANT_RESOURCE_PATH'] provision_tenant_resource_path = os.environ['PROVISION_TENANT_RESOURCE_PATH'] platinum_tier_api_key = os.environ['PLATINUM_TIER_API_KEY'] premium_tier_api_key = os.environ['PREMIUM_TIER_API_KEY'] standard_tier_api_key = os.environ['STANDARD_TIER_API_KEY'] basic_tier_api_key = os.environ['BASIC_TIER_API_KEY'] lambda_client = boto3.client('lambda') def register_tenant(event, context): try: api_key='' tenant_id = uuid.uuid1().hex tenant_details = json.loads(event['body']) tenant_details['dedicatedTenancy'] = 'false' if (tenant_details['tenantTier'].upper() == utils.TenantTier.PLATINUM.value.upper()): tenant_details['dedicatedTenancy'] = 'true' api_key = platinum_tier_api_key elif (tenant_details['tenantTier'].upper() == utils.TenantTier.PREMIUM.value.upper()): api_key = premium_tier_api_key elif (tenant_details['tenantTier'].upper() == utils.TenantTier.STANDARD.value.upper()): api_key = standard_tier_api_key elif (tenant_details['tenantTier'].upper() == utils.TenantTier.BASIC.value.upper()): api_key = basic_tier_api_key tenant_details['tenantId'] = tenant_id tenant_details['apiKey'] = api_key logger.info(tenant_details) stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) create_user_response = __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name) logger.info (create_user_response) tenant_details['userPoolId'] = create_user_response['message']['userPoolId'] tenant_details['appClientId'] = create_user_response['message']['appClientId'] tenant_details['tenantAdminUserName'] = create_user_response['message']['tenantAdminUserName'] create_tenant_response = __create_tenant(tenant_details, headers, auth, host, stage_name) logger.info (create_tenant_response) if (tenant_details['dedicatedTenancy'].upper() == 'TRUE'): provision_tenant_response = __provision_tenant(tenant_details, headers, auth, host, stage_name) logger.info(provision_tenant_response) except Exception as e: logger.error('Error registering a new tenant') raise Exception('Error registering a new tenant', e) else: return utils.create_success_response("You have been registered in our system") def __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_admin_user_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while calling the create tenant admin user service') raise Exception('Error occured while calling the create tenant admin user service', e) else: return response_json def __create_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, create_tenant_resource_path]) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json() except Exception as e: logger.error('Error occured while creating the tenant record in table') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json def __provision_tenant(tenant_details, headers, auth, host, stage_name): try: url = ''.join(['https://', host, '/', stage_name, provision_tenant_resource_path]) logger.info(url) response = requests.post(url, data=json.dumps(tenant_details), auth=auth, headers=headers) response_json = response.json()['message'] except Exception as e: logger.error('Error occured while provisioning the tenant') raise Exception('Error occured while creating the tenant record in table', e) else: return response_json ================================================ FILE: Solution/Lab6/server/TenantManagementService/user-management.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import os import sys import logger import utils import metrics_manager import auth_manager from boto3.dynamodb.conditions import Key from aws_lambda_powertools import Tracer tracer = Tracer() client = boto3.client('cognito-idp') dynamodb = boto3.resource('dynamodb') table_tenant_user_map = dynamodb.Table('ServerlessSaaS-TenantUserMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') def create_tenant_admin_user(event, context): tenant_user_pool_id = os.environ['TENANT_USER_POOL_ID'] tenant_app_client_id = os.environ['TENANT_APP_CLIENT_ID'] tenant_details = json.loads(event['body']) tenant_id = tenant_details['tenantId'] logger.info(tenant_details) user_mgmt = UserManagement() if (tenant_details['dedicatedTenancy'] == 'true'): user_pool_response = user_mgmt.create_user_pool(tenant_id) user_pool_id = user_pool_response['UserPool']['Id'] logger.info (user_pool_id) app_client_response = user_mgmt.create_user_pool_client(user_pool_id) logger.info(app_client_response) app_client_id = app_client_response['UserPoolClient']['ClientId'] user_pool_domain_response = user_mgmt.create_user_pool_domain(user_pool_id, tenant_id) logger.info ("New Tenant Created") else: user_pool_id = tenant_user_pool_id app_client_id = tenant_app_client_id #Add tenant admin now based upon user pool tenant_user_group_response = user_mgmt.create_user_group(user_pool_id,tenant_id,"User group for tenant {0}".format(tenant_id)) tenant_admin_user_name = 'tenant-admin-{0}'.format(tenant_details['tenantId']) create_tenant_admin_response = user_mgmt.create_tenant_admin(user_pool_id, tenant_admin_user_name, tenant_details) add_tenant_admin_to_group_response = user_mgmt.add_user_to_group(user_pool_id, tenant_admin_user_name, tenant_user_group_response['Group']['GroupName']) tenant_user_mapping_response = user_mgmt.create_user_tenant_mapping(tenant_admin_user_name,tenant_id) response = {"userPoolId": user_pool_id, "appClientId": app_client_id, "tenantAdminUserName": tenant_admin_user_name} return utils.create_success_response(response) @tracer.capture_lambda_handler #only tenant admin can create users def create_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to create new user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = user_details['tenantId'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] else: user_tenant_id = tenant_id if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): metrics_manager.record_metric(event, "UserCreated", "Count", 1) response = client.admin_create_user( Username=user_details['userName'], UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] }, { 'Name': 'custom:tenantId', 'Value': user_tenant_id } ] ) logger.log_with_tenant_context(event, response) user_mgmt = UserManagement() user_mgmt.add_user_to_group(user_pool_id, user_details['userName'], user_tenant_id) response_mapping = user_mgmt.create_user_tenant_mapping(user_details['userName'], user_tenant_id) logger.log_with_tenant_context(event, "Request completed to create new user") return utils.create_success_response("New user created") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can create user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_users(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] users = [] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get users") if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): response = client.list_users( UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) num_of_users = len(response['Users']) metrics_manager.record_metric(event, "Number of users", "Count", num_of_users) if (num_of_users > 0): for user in response['Users']: is_same_tenant_user = False user_info = UserInfo() for attr in user["Attributes"]: if(attr["Name"] == "custom:tenantId" and attr["Value"] == tenant_id): is_same_tenant_user = True user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] if(is_same_tenant_user): user_info.enabled = user["Enabled"] user_info.created = user["UserCreateDate"] user_info.modified = user["UserLastModifiedDate"] user_info.status = user["UserStatus"] user_info.user_name = user["Username"] users.append(user_info) return utils.generate_response(users) else: logger.log_with_tenant_context(event, "Request completed as unauthorized.") return utils.create_unauthorized_response() @tracer.capture_lambda_handler def get_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to get user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = event['queryStringParameters']['tenantid'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] if (auth_manager.isTenantUser(user_role) and user_name != requesting_user_name): logger.log_with_tenant_context(event, "Request completed as unauthorized. User can only get its information.") return utils.create_unauthorized_response() else: user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: logger.log_with_tenant_context(event, "Request completed to get new user") return utils.create_success_response(user_info.__dict__) @tracer.capture_lambda_handler def update_user(event, context): requesting_user_name = event['requestContext']['authorizer']['userName'] tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_details = json.loads(event['body']) user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to update user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = user_details['tenantId'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] if (auth_manager.isTenantUser(user_role)): logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can update user!") return utils.create_unauthorized_response() else: user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserUpdated", "Count", 1) response = client.admin_update_user_attributes( Username=user_name, UserPoolId=user_pool_id, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] } ] ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to update user") return utils.create_success_response("user updated") @tracer.capture_lambda_handler def disable_user(event, context): tenant_id = event['requestContext']['authorizer']['tenantId'] user_pool_id = event['requestContext']['authorizer']['userPoolId'] user_role = event['requestContext']['authorizer']['userRole'] user_name = event['pathParameters']['username'] tracer.put_annotation(key="TenantId", value=tenant_id) logger.log_with_tenant_context(event, "Request received to disable new user") if (auth_manager.isSystemAdmin(user_role)): user_tenant_id = event['queryStringParameters']['tenantid'] tenant_details = table_tenant_details.get_item( Key ={ 'tenantId': user_tenant_id } ) logger.info(tenant_details) user_pool_id = tenant_details['Item']['userPoolId'] if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): user_info = get_user_info(event, user_pool_id, user_name) if(not auth_manager.isSystemAdmin(user_role) and user_info.tenant_id!=tenant_id): logger.log_with_tenant_context(event, "Request completed as unauthorized. Users in other tenants cannot be accessed") return utils.create_unauthorized_response() else: metrics_manager.record_metric(event, "UserDisabled", "Count", 1) response = client.admin_disable_user( Username=user_name, UserPoolId=user_pool_id ) logger.log_with_tenant_context(event, response) logger.log_with_tenant_context(event, "Request completed to disable new user") return utils.create_success_response("User disabled") else: logger.log_with_tenant_context(event, "Request completed as unauthorized. Only tenant admin or system admin can disable user!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def disable_users_by_tenant(event, context): logger.info("Request received to disable users by tenant") tenantid_to_update = event['tenantId'] tenant_user_pool_id = event['userPoolId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if ((auth_manager.isTenantAdmin(user_role) and tenantid_to_update == requesting_tenant_id) or auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_disable_user( Username=user['userName'], UserPoolId=tenant_user_pool_id ) logger.info(response) logger.info("Request completed to disable users") return utils.create_success_response("Users disabled") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() @tracer.capture_lambda_handler #this method uses IAM Authorization and protected using a resource policy. This method is also invoked async def enable_users_by_tenant(event, context): logger.info("Request received to enable users by tenant") tenantid_to_update = event['tenantId'] tenant_user_pool_id = event['userPoolId'] user_role = event['userRole'] requesting_tenant_id = event['requestingTenantId'] tracer.put_annotation(key="TenantId", value=tenantid_to_update) if (auth_manager.isSystemAdmin(user_role)): filtering_exp = Key('tenantId').eq(tenantid_to_update) response = table_tenant_user_map.query(KeyConditionExpression=filtering_exp) users = response.get('Items') for user in users: response = client.admin_enable_user( Username=user['userName'], UserPoolId=tenant_user_pool_id ) logger.info(response) logger.info("Request completed to enable users") return utils.create_success_response("Users enables") else: logger.info("Request completed as unauthorized. Only tenant admin or system admin can update!") return utils.create_unauthorized_response() def get_user_info(event, user_pool_id, user_name): metrics_manager.record_metric(event, "UserInfoRequested", "Count", 1) response = client.admin_get_user( UserPoolId=user_pool_id, Username=user_name ) logger.log_with_tenant_context(event, response) user_info = UserInfo() user_info.user_name = response["Username"] for attr in response["UserAttributes"]: if(attr["Name"] == "custom:tenantId"): user_info.tenant_id = attr["Value"] if(attr["Name"] == "custom:userRole"): user_info.user_role = attr["Value"] if(attr["Name"] == "email"): user_info.email = attr["Value"] logger.log_with_tenant_context(event, user_info) return user_info class UserManagement: def create_user_pool(self, tenant_id): application_site_url = os.environ['TENANT_USER_POOL_CALLBACK_URL'] email_message = ''.join(["Login into tenant UI application at ", application_site_url, " with username {username} and temporary password {####}"]) email_subject = "Your temporary password for tenant UI application" response = client.create_user_pool( PoolName= tenant_id + '-ServerlessSaaSUserPool', AutoVerifiedAttributes=['email'], AccountRecoverySetting={ 'RecoveryMechanisms': [ { 'Priority': 1, 'Name': 'verified_email' }, ] }, Schema=[ { 'Name': 'email', 'AttributeDataType': 'String', 'Required': True, }, { 'Name': 'tenantId', 'AttributeDataType': 'String', 'Required': False, }, { 'Name': 'userRole', 'AttributeDataType': 'String', 'Required': False, } ], AdminCreateUserConfig={ 'InviteMessageTemplate': { 'EmailMessage': email_message, 'EmailSubject': email_subject } } ) return response def create_user_pool_client(self, user_pool_id): user_pool_callback_url = os.environ['TENANT_USER_POOL_CALLBACK_URL'] response = client.create_user_pool_client( UserPoolId= user_pool_id, ClientName= 'ServerlessSaaSClient', GenerateSecret= False, AllowedOAuthFlowsUserPoolClient= True, AllowedOAuthFlows=[ 'code', 'implicit' ], SupportedIdentityProviders=[ 'COGNITO', ], CallbackURLs=[ user_pool_callback_url, ], LogoutURLs= [ user_pool_callback_url, ], AllowedOAuthScopes=[ 'email', 'openid', 'profile' ], WriteAttributes=[ 'email', 'custom:tenantId' ] ) return response def create_user_pool_domain(self, user_pool_id, tenant_id): response = client.create_user_pool_domain( Domain= tenant_id + '-serverlesssaas', UserPoolId=user_pool_id ) return response def create_user_group(self, user_pool_id, group_name, group_description): response = client.create_group( GroupName=group_name, UserPoolId=user_pool_id, Description= group_description, Precedence=0 ) return response def create_tenant_admin(self, user_pool_id, tenant_admin_user_name, user_details): response = client.admin_create_user( Username=tenant_admin_user_name, UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['tenantEmail'] }, { 'Name': 'email_verified', 'Value': 'true' }, { 'Name': 'custom:userRole', 'Value': 'TenantAdmin' }, { 'Name': 'custom:tenantId', 'Value': user_details['tenantId'] } ] ) return response def add_user_to_group(self, user_pool_id, user_name, group_name): response = client.admin_add_user_to_group( UserPoolId=user_pool_id, Username=user_name, GroupName=group_name ) return response def create_user_tenant_mapping(self, user_name, tenant_id): response = table_tenant_user_map.put_item( Item={ 'tenantId': tenant_id, 'userName': user_name } ) return response class UserInfo: def __init__(self, user_name=None, tenant_id=None, user_role=None, email=None, status=None, enabled=None, created=None, modified=None): self.user_name = user_name self.tenant_id = tenant_id self.user_role = user_role self.email = email self.status = status self.enabled = enabled self.created = created self.modified = modified ================================================ FILE: Solution/Lab6/server/TenantPipeline/.gitignore ================================================ *.js !jest.config.js *.d.ts node_modules # CDK asset staging directory .cdk.staging cdk.out # Parcel default cache directory .parcel-cache ================================================ FILE: Solution/Lab6/server/TenantPipeline/.npmignore ================================================ *.ts !*.d.ts # CDK asset staging directory .cdk.staging cdk.out ================================================ FILE: Solution/Lab6/server/TenantPipeline/README.md ================================================ # Welcome to your CDK TypeScript project! This is a blank project for TypeScript development with CDK. The `cdk.json` file tells the CDK Toolkit how to execute your app. ## Useful commands * `npm run build` compile typescript to js * `npm run watch` watch for changes and compile * `npm run test` perform the jest unit tests * `cdk deploy` deploy this stack to your default AWS account/region * `cdk diff` compare deployed stack with current state * `cdk synth` emits the synthesized CloudFormation template ================================================ FILE: Solution/Lab6/server/TenantPipeline/bin/pipeline.ts ================================================ #!/usr/bin/env node import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; import { ServerlessSaaSStack } from '../lib/serverless-saas-stack'; const app = new cdk.App(); new ServerlessSaaSStack(app, 'serverless-saas-pipeline'); ================================================ FILE: Solution/Lab6/server/TenantPipeline/cdk.json ================================================ { "app": "npx ts-node bin/pipeline.ts", "context": {} } ================================================ FILE: Solution/Lab6/server/TenantPipeline/jest.config.js ================================================ module.exports = { roots: ['/test'], testMatch: ['**/*.test.ts'], transform: { '^.+\\.tsx?$': 'ts-jest' } }; ================================================ FILE: Solution/Lab6/server/TenantPipeline/lib/serverless-saas-stack.ts ================================================ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 import { Construct } from 'constructs'; import * as cdk from 'aws-cdk-lib'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as codecommit from 'aws-cdk-lib/aws-codecommit'; import * as codepipeline from 'aws-cdk-lib/aws-codepipeline'; import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions'; import * as codebuild from 'aws-cdk-lib/aws-codebuild'; import { Function, Runtime, AssetCode } from 'aws-cdk-lib/aws-lambda'; import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Duration } from 'aws-cdk-lib'; export class ServerlessSaaSStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const artifactsBucket = new s3.Bucket(this, "ArtifactsBucket", { encryption: s3.BucketEncryption.S3_MANAGED, }); //Since this lambda is invoking cloudformation which is inturn deploying AWS resources, we are giving overly permissive permissions to this lambda. //You can limit this based upon your use case and AWS Resources you need to deploy. const lambdaPolicy = new PolicyStatement() lambdaPolicy.addActions("*") lambdaPolicy.addResources("*") const lambdaFunction = new Function(this, "deploy-tenant-stack", { handler: "lambda-deploy-tenant-stack.lambda_handler", runtime: Runtime.PYTHON_3_9, code: new AssetCode(`./resources`), memorySize: 512, timeout: Duration.seconds(10), environment: { BUCKET: artifactsBucket.bucketName, }, initialPolicy: [lambdaPolicy], }) // Pipeline creation starts const pipeline = new codepipeline.Pipeline(this, 'Pipeline', { pipelineName: 'serverless-saas-pipeline', artifactBucket: artifactsBucket }); // Import existing CodeCommit sam-app repository const codeRepo = codecommit.Repository.fromRepositoryName( this, 'AppRepository', 'aws-serverless-saas-workshop' ); // Declare source code as an artifact const sourceOutput = new codepipeline.Artifact(); // Add source stage to pipeline pipeline.addStage({ stageName: 'Source', actions: [ new codepipeline_actions.CodeCommitSourceAction({ actionName: 'CodeCommit_Source', repository: codeRepo, branch: 'main', output: sourceOutput, variablesNamespace: 'SourceVariables' }), ], }); // Declare build output as artifacts const buildOutput = new codepipeline.Artifact(); //Declare a new CodeBuild project const buildProject = new codebuild.PipelineProject(this, 'Build', { buildSpec : codebuild.BuildSpec.fromSourceFilename("Lab6/server/tenant-buildspec.yml"), environment: { buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_4 }, environmentVariables: { 'PACKAGE_BUCKET': { value: artifactsBucket.bucketName } } }); // Add the build stage to our pipeline pipeline.addStage({ stageName: 'Build', actions: [ new codepipeline_actions.CodeBuildAction({ actionName: 'Build-Serverless-SaaS', project: buildProject, input: sourceOutput, outputs: [buildOutput], }), ], }); const deployOutput = new codepipeline.Artifact(); //Add the Lambda function that will deploy the tenant stack in a multitenant way pipeline.addStage({ stageName: 'Deploy', actions: [ new codepipeline_actions.LambdaInvokeAction({ actionName: 'DeployTenantStack', lambda: lambdaFunction, inputs: [buildOutput], outputs: [deployOutput], userParameters: { 'artifact': 'Artifact_Build_Build-Serverless-SaaS', 'template_file': 'packaged.yaml', 'commit_id': '#{SourceVariables.CommitId}' } }), ], }); } } ================================================ FILE: Solution/Lab6/server/TenantPipeline/package.json ================================================ { "name": "pipeline", "version": "0.1.0", "bin": { "pipeline": "bin/pipeline.js" }, "scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "cdk": "cdk" }, "devDependencies": { "@aws-cdk/assert": "1.64.1", "@types/jest": "^26.0.10", "@types/node": "10.17.27", "aws-cdk-lib": "^2.0.0", "constructs": "^10.0.0", "jest": "^26.4.2", "node-notifier": "^8.0.1", "ts-jest": "^26.2.0", "ts-node": "^8.1.0", "typescript": "4.9.5", "@types/prettier": "2.6.0" }, "dependencies": { "aws-cdk-lib": "^2.0.0", "constructs": "^10.0.0", "source-map-support": "^0.5.19" } } ================================================ FILE: Solution/Lab6/server/TenantPipeline/resources/lambda-deploy-tenant-stack.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from boto3.session import Session import json import boto3 import zipfile import tempfile import botocore import traceback import time print('Loading function') cf = boto3.client('cloudformation') code_pipeline = boto3.client('codepipeline') dynamodb = boto3.resource('dynamodb') table_tenant_stack_mapping = dynamodb.Table('ServerlessSaaS-TenantStackMapping') table_tenant_details = dynamodb.Table('ServerlessSaaS-TenantDetails') table_tenant_settings = dynamodb.Table('ServerlessSaaS-Settings') def find_artifact(artifacts, name): """Finds the artifact 'name' among the 'artifacts' Args: artifacts: The list of artifacts available to the function name: The artifact we wish to use Returns: The artifact dictionary found Raises: Exception: If no matching artifact is found """ for artifact in artifacts: if artifact['name'] == name: return artifact raise Exception('Input artifact named "{0}" not found in event'.format(name)) def get_template_url(s3, artifact, file_in_zip): """Gets the template artifact Downloads the artifact from the S3 artifact store to a temporary file then extracts the zip and returns the file containing the CloudFormation template. Args: artifact: The artifact to download file_in_zip: The path to the file within the zip containing the template Returns: The CloudFormation template as a string Raises: Exception: Any exception thrown while downloading the artifact or unzipping it """ tmp_file = tempfile.NamedTemporaryFile() bucket = artifact['location']['s3Location']['bucketName'] print(bucket) key = artifact['location']['s3Location']['objectKey'] print(key) with tempfile.NamedTemporaryFile() as tmp_file: s3.download_file(bucket, key, tmp_file.name) with zipfile.ZipFile(tmp_file.name, 'r') as zip: extracted_file = zip.extract(file_in_zip, '/tmp/') s3.upload_file(extracted_file, bucket, file_in_zip) template_url =''.join(['https://', bucket,'.s3.amazonaws.com/',file_in_zip]) return template_url def update_stack(stack, template_url, params): """Start a CloudFormation stack update Args: stack: The stack to update template_url: The template to apply Returns: True if an update was started, false if there were no changes to the template since the last update. Raises: Exception: Any exception besides "No updates are to be performed." """ try: cf.update_stack(StackName=stack, TemplateURL=template_url, Capabilities=['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], Parameters=params) return True except botocore.exceptions.ClientError as e: if e.response['Error']['Message'] == 'No updates are to be performed.': return False else: raise Exception('Error updating CloudFormation stack "{0}"'.format(stack), e) def stack_exists(stack): """Check if a stack exists or not Args: stack: The stack to check Returns: True or False depending on whether the stack exists Raises: Any exceptions raised .describe_stacks() besides that the stack doesn't exist. """ try: cf.describe_stacks(StackName=stack) return True except botocore.exceptions.ClientError as e: if "does not exist" in e.response['Error']['Message']: return False else: raise e def create_stack(stack, template_url, params): """Starts a new CloudFormation stack creation Args: stack: The stack to be created template_url: The template for the stack to be created with Throws: Exception: Any exception thrown by .create_stack() """ cf.create_stack(StackName=stack, TemplateURL=template_url, Capabilities=['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], Parameters=params) def get_stack_status(stack): """Get the status of an existing CloudFormation stack Args: stack: The name of the stack to check Returns: The CloudFormation status string of the stack such as CREATE_COMPLETE Raises: Exception: Any exception thrown by .describe_stacks() """ stack_description = cf.describe_stacks(StackName=stack) return stack_description['Stacks'][0]['StackStatus'] def put_job_success(job, message): """Notify CodePipeline of a successful job Args: job: The CodePipeline job ID message: A message to be logged relating to the job status Raises: Exception: Any exception thrown by .put_job_success_result() """ print('Putting job success') print(message) code_pipeline.put_job_success_result(jobId=job) def put_job_failure(job, message): """Notify CodePipeline of a failed job Args: job: The CodePipeline job ID message: A message to be logged relating to the job status Raises: Exception: Any exception thrown by .put_job_failure_result() """ print('Putting job failure') print(message) code_pipeline.put_job_failure_result(jobId=job, failureDetails={'message': message, 'type': 'JobFailed'}) def continue_job_later(job, message): """Notify CodePipeline of a continuing job This will cause CodePipeline to invoke the function again with the supplied continuation token. Args: job: The JobID message: A message to be logged relating to the job status continuation_token: The continuation token Raises: Exception: Any exception thrown by .put_job_success_result() """ # Use the continuation token to keep track of any job execution state # This data will be available when a new job is scheduled to continue the current execution continuation_token = json.dumps({'previous_job_id': job}) print('Putting job continuation') print(message) code_pipeline.put_job_success_result(jobId=job, continuationToken=continuation_token) def start_update_or_create(job_id, stack, template_url, params): """Starts the stack update or create process If the stack exists then update, otherwise create. Args: job_id: The ID of the CodePipeline job stack: The stack to create or update template_url: The template to create/update the stack with """ if stack_exists(stack): status = get_stack_status(stack) if status not in ['CREATE_COMPLETE', 'ROLLBACK_COMPLETE', 'UPDATE_COMPLETE']: # If the CloudFormation stack is not in a state where # it can be updated again then fail the job right away. put_job_failure(job_id, 'Stack cannot be updated when status is: ' + status) return were_updates = update_stack(stack, template_url, params) if were_updates: # If there were updates then continue the job so it can monitor # the progress of the update. continue_job_later(job_id, 'Stack update started') else: # If there were no updates then succeed the job immediately put_job_success(job_id, 'There were no stack updates') else: # If the stack doesn't already exist then create it instead # of updating it. create_stack(stack, template_url, params) # Continue the job so the pipeline will wait for the CloudFormation # stack to be created. continue_job_later(job_id, 'Stack create started') def check_stack_update_status(job_id, stack): """Monitor an already-running CloudFormation update/create Succeeds, fails or continues the job depending on the stack status. Args: job_id: The CodePipeline job ID stack: The stack to monitor """ status = get_stack_status(stack) if status in ['UPDATE_COMPLETE', 'CREATE_COMPLETE']: # If the update/create finished successfully then # succeed the job and don't continue. put_job_success(job_id, 'Stack update complete') elif status in ['UPDATE_IN_PROGRESS', 'UPDATE_ROLLBACK_IN_PROGRESS', 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', 'CREATE_IN_PROGRESS', 'ROLLBACK_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS']: # If the job isn't finished yet then continue it continue_job_later(job_id, 'Stack update still in progress') else: # If the Stack is a state which isn't "in progress" or "complete" # then the stack update/create has failed so end the job with # a failed result. put_job_failure(job_id, 'Update failed: ' + status) def get_user_params(job_data): """Decodes the JSON user parameters and validates the required properties. Args: job_data: The job data structure containing the UserParameters string which should be a valid JSON structure Returns: The JSON parameters decoded as a dictionary. Raises: Exception: The JSON can't be decoded or a property is missing. """ try: # Get the user parameters which contain the stack, artifact and file settings user_parameters = job_data['actionConfiguration']['configuration']['UserParameters'] decoded_parameters = json.loads(user_parameters) except Exception: # We're expecting the user parameters to be encoded as JSON # so we can pass multiple values. If the JSON can't be decoded # then fail the job with a helpful message. raise Exception('UserParameters could not be decoded as JSON') if 'artifact' not in decoded_parameters: # Validate that the artifact name is provided, otherwise fail the job # with a helpful message. raise Exception('Your UserParameters JSON must include the artifact name') if 'template_file' not in decoded_parameters: # Validate that the template file is provided, otherwise fail the job # with a helpful message. raise Exception('Your UserParameters JSON must include the template file name') return decoded_parameters def setup_s3_client(job_data): """Creates an S3 client Uses the credentials passed in the event by CodePipeline. These credentials can be used to access the artifact bucket. Args: job_data: The job data structure Returns: An S3 client with the appropriate credentials """ # Could not use the artifact credentials to put object to artifacts s3 bucket. # We are running into issue as described in https://github.com/aws/aws-cdk/issues/3274 # key_id = job_data['artifactCredentials']['accessKeyId'] # key_secret = job_data['artifactCredentials']['secretAccessKey'] # session_token = job_data['artifactCredentials']['sessionToken'] # session = Session(aws_access_key_id=key_id, # aws_secret_access_key=key_secret, # aws_session_token=session_token) # return session.client('s3') return boto3.client('s3') def get_tenant_params(tenantId): """Get tenant details to be supplied to Cloud formation Args: tenantId (str): tenantId for which details are needed Returns: params from tenant management table """ params = [] param_tenantid = {} param_tenantid['ParameterKey'] = 'TenantIdParameter' param_tenantid['ParameterValue'] = tenantId params.append(param_tenantid) return params def add_parameter(params, parameter_key, parameter_value): parameter = {} parameter['ParameterKey'] = parameter_key parameter['ParameterValue'] = parameter_value params.append(parameter) def update_tenantstackmapping(tenantId, commit_id): """Update the tenant stack mapping table with the code pipeline job id Args: tenantId ([string]): tenant id for which data needs to be updated job_id ([type]): current code pipeline job id Returns: [type]: [description] """ response = table_tenant_stack_mapping.update_item( Key={'tenantId': tenantId}, UpdateExpression="set codeCommitId=:codeCommitId", ExpressionAttributeValues={ ':codeCommitId': commit_id }, ReturnValues="NONE") return response def lambda_handler(event, context): """The Lambda function handler If a continuing job then checks the CloudFormation stack status and updates the job accordingly. If a new job then kick of an update or creation of the target CloudFormation stack. Args: event: The event passed by Lambda context: The context passed by Lambda """ try: # Extract the Job ID job_id = event['CodePipeline.job']['id'] # Extract the Job Data job_data = event['CodePipeline.job']['data'] # Extract the params params = get_user_params(job_data) # Get the list of artifacts passed to the function artifacts = job_data['inputArtifacts'] artifact = params['artifact'] template_file = params['template_file'] commit_id = params['commit_id'] # Get all the stacks for each tenant to be updated/created from tenant stack mapping table mappings = table_tenant_stack_mapping.scan() print (mappings) #Update/Create stacks for all tenants for mapping in mappings['Items']: stack = mapping['stackName'] tenantId = mapping['tenantId'] applyLatestRelease = mapping['applyLatestRelease'] if (applyLatestRelease): # Get the parameters to be passed to the Cloudformation from tenant table params = get_tenant_params(tenantId) if 'continuationToken' in job_data: # If we're continuing then the create/update has already been triggered # we just need to check if it has finished. check_stack_update_status(job_id, stack) else: # Get the artifact details artifact_data = find_artifact(artifacts, artifact) # Get S3 client to access artifact with s3 = setup_s3_client(job_data) # Get the JSON template file out of the artifact template_url = get_template_url(s3, artifact_data, template_file) # Kick off a stack update or create start_update_or_create(job_id, stack, template_url, params) # If we are applying the release, update tenant stack mapping with the pipe line id update_tenantstackmapping(tenantId, commit_id) except Exception as e: # If any other exceptions which we didn't expect are raised # then fail the job and log the exception message. print('Function failed due to exception.') print(e) traceback.print_exc() put_job_failure(job_id, 'Function exception: ' + str(e)) #put_job_success(job_id, "Changeset executed successfully") print('Function complete.') return "Complete." ================================================ FILE: Solution/Lab6/server/TenantPipeline/test/pipeline.test.ts ================================================ // import { SynthUtils } from '@aws-cdk/assert'; // import { Stack, App } from 'aws-cdk-lib'; // import { Template } from 'aws-cdk-lib/assertions'; // import * as Pipeline from '../lib/serverless-saas-stack'; // test('synthesized cloudformation template should match original template', () => { // const app = new App(); // const stack = new Pipeline.ServerlessSaaSStack(app, 'MyTestStack'); // const template = Template.fromStack(stack); // expect(template).toMatchSnapshot(); // }); ================================================ FILE: Solution/Lab6/server/TenantPipeline/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2018", "module": "commonjs", "lib": ["es2018"], "declaration": true, "strict": true, "noImplicitAny": true, "strictNullChecks": true, "noImplicitThis": true, "alwaysStrict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": false, "inlineSourceMap": true, "inlineSources": true, "experimentalDecorators": true, "strictPropertyInitialization": false, "typeRoots": ["./node_modules/@types"] }, "exclude": ["cdk.out"] } ================================================ FILE: Solution/Lab6/server/custom_resources/requirements.txt ================================================ requests crhelper ================================================ FILE: Solution/Lab6/server/custom_resources/update_settings_table.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import logger from crhelper import CfnResource helper = CfnResource() try: dynamodb = boto3.resource('dynamodb') except Exception as e: helper.init_failure(e) @helper.create @helper.update def do_action(event, _): """ Called as part of bootstrap template. Inserts/Updates Settings table based upon the resources deployed inside bootstrap template We use these settings inside tenant template Args: event ([type]): [description] _ ([type]): [description] """ logger.info("Updating settings") settings_table_name = event['ResourceProperties']['SettingsTableName'] cognitoUserPoolId = event['ResourceProperties']['cognitoUserPoolId'] cognitoUserPoolClientId = event['ResourceProperties']['cognitoUserPoolClientId'] table_system_settings = dynamodb.Table(settings_table_name) response = table_system_settings.put_item( Item={ 'settingName': 'userPoolId-pooled', 'settingValue' : cognitoUserPoolId } ) response = table_system_settings.put_item( Item={ 'settingName': 'appClientId-pooled', 'settingValue' : cognitoUserPoolClientId } ) @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Solution/Lab6/server/custom_resources/update_tenant_apigatewayurl.py ================================================ import json import boto3 import logger from boto3.dynamodb.conditions import Key from crhelper import CfnResource helper = CfnResource() try: client = boto3.client('dynamodb') dynamodb = boto3.resource('dynamodb') except Exception as e: helper.init_failure(e) @helper.create @helper.update def do_action(event, _): """ The URL for Tenant APIs(Product/Order) can differ by tenant. For Pooled tenants it is shared and for Silo (Platinum tier tenants) it is unique to them. This method keeps the URL for Pooled tenants inside Settings Table, since it is shared across multiple tenants, And for Silo tenants inside the tenant management table along with other tenant settings, for that tenant Args: event ([type]): [description] _ ([type]): [description] """ logger.info("Updating Tenant Details table") tenant_details_table_name = event['ResourceProperties']['TenantDetailsTableName'] settings_table_name = event['ResourceProperties']['SettingsTableName'] tenant_id = event['ResourceProperties']['TenantId'] tenant_api_gateway_url = event['ResourceProperties']['TenantApiGatewayUrl'] if(tenant_id.lower() =='pooled'): # Note: Tenant management service will use below setting to update apiGatewayUrl for pooled tenants in TenantDetails table settings_table = dynamodb.Table(settings_table_name) settings_table.put_item(Item={ 'settingName': 'apiGatewayUrl-Pooled', 'settingValue' : tenant_api_gateway_url }) else: tenant_details = dynamodb.Table(tenant_details_table_name) response = tenant_details.update_item( Key={'tenantId': tenant_id}, UpdateExpression="set apiGatewayUrl=:apiGatewayUrl", ExpressionAttributeValues={ ':apiGatewayUrl': tenant_api_gateway_url }, ReturnValues="NONE") @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Solution/Lab6/server/custom_resources/update_tenantstackmap_table.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import logger from crhelper import CfnResource helper = CfnResource() try: dynamodb = boto3.resource('dynamodb') except Exception as e: helper.init_failure(e) @helper.create @helper.update def do_action(event, _): """ One time entry for pooled tenants inside tenant stack mapping table. This ensures that when code pipeline for tenant template is kicked off, it always create a default stack for pooled tenants. Args: event ([type]): [description] _ ([type]): [description] """ logger.info("Updating Tenant Stack Map") tenantstackmap_table_name = event['ResourceProperties']['TenantStackMappingTableName'] table_stack_mapping = dynamodb.Table(tenantstackmap_table_name) response = table_stack_mapping.put_item( Item={ 'tenantId': 'pooled', 'stackName' : 'stack-pooled', 'applyLatestRelease': True, 'codeCommitId': '' } ) @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Solution/Lab6/server/custom_resources/update_usage_plan.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import boto3 import logger from crhelper import CfnResource helper = CfnResource() try: dynamodb = boto3.resource('dynamodb') apigateway = boto3.client('apigateway') except Exception as e: helper.init_failure(e) @helper.create def do_action(event, _): """ Usage plans are created as part of bootstrap template. This method associates the usage plans for various tiers with tenant Apis Args: event ([type]): [description] _ ([type]): [description] """ logger.info("adding api gateway stage to usage plan") api_id = event['ResourceProperties']['ApiGatewayId'] settings_table_name = event['ResourceProperties']['SettingsTableName'] is_pooled_deploy = event['ResourceProperties']['IsPooledDeploy'] stage = event['ResourceProperties']['Stage'] usage_plan_id_basic = event['ResourceProperties']['UsagePlanBasicTier'] usage_plan_id_standard = event['ResourceProperties']['UsagePlanStandardTier'] usage_plan_id_premium = event['ResourceProperties']['UsagePlanPremiumTier'] usage_plan_id_platinum = event['ResourceProperties']['UsagePlanPlatinumTier'] table_system_settings = dynamodb.Table(settings_table_name) if(is_pooled_deploy == "true"): response_apigateway = apigateway.update_usage_plan ( usagePlanId=usage_plan_id_basic, patchOperations=[ { 'op':'add', 'path':'/apiStages', 'value': api_id + ":" + stage } ] ) response_apigateway = apigateway.update_usage_plan ( usagePlanId=usage_plan_id_standard, patchOperations=[ { 'op':'add', 'path':'/apiStages', 'value': api_id + ":" + stage } ] ) response_apigateway = apigateway.update_usage_plan ( usagePlanId=usage_plan_id_premium, patchOperations=[ { 'op':'add', 'path':'/apiStages', 'value': api_id + ":" + stage } ] ) else: response_apigateway = apigateway.update_usage_plan ( usagePlanId=usage_plan_id_platinum, patchOperations=[ { 'op':'add', 'path':'/apiStages', 'value': api_id + ":" + stage } ] ) @helper.update @helper.delete def do_nothing(_, __): pass def handler(event, context): helper(event, context) ================================================ FILE: Solution/Lab6/server/layers/auth_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import utils # These are the roles being supported in this reference architecture class UserRoles: SYSTEM_ADMIN = "SystemAdmin" CUSTOMER_SUPPORT = "CustomerSupport" TENANT_ADMIN = "TenantAdmin" TENANT_USER = "TenantUser" def isTenantAdmin(user_role): if (user_role == UserRoles.TENANT_ADMIN): return True else: return False def isSystemAdmin(user_role): if (user_role == UserRoles.SYSTEM_ADMIN): return True else: return False def isSaaSProvider(user_role): if (user_role == UserRoles.SYSTEM_ADMIN or user_role == UserRoles.CUSTOMER_SUPPORT): return True else: return False def isTenantUser(user_role): if (user_role == UserRoles.TENANT_USER): return True else: return False def getPolicyForUser(user_role, service_identifier, tenant_id, region, aws_account_id): """ This method is being used by Authorizer to get appropriate policy by user role Args: user_role (string): UserRoles enum tenant_id (string): region (string): aws_account_id (string): Returns: string: policy that tenant needs to assume """ iam_policy = "" if (isSystemAdmin(user_role)): iam_policy = __getPolicyForSystemAdmin(region, aws_account_id) elif (isTenantAdmin(user_role)): iam_policy = __getPolicyForTenantAdmin(tenant_id, service_identifier, region, aws_account_id) elif (isTenantUser(user_role)): iam_policy = __getPolicyForTenantUser(tenant_id, region, aws_account_id) return iam_policy def __getPolicyForSystemAdmin(region, aws_account_id): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/*".format(region, aws_account_id), ] } ] } return json.dumps(policy) def __getPolicyForTenantAdmin(tenant_id, sevice_identifier, region, aws_account_id): if (sevice_identifier == utils.Service_Identifier.SHARED_SERVICES.value): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantUserMapping".format(region, aws_account_id), "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantDetails".format(region, aws_account_id) ], "Condition": { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": [ "{0}".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-TenantStackMapping".format(region, aws_account_id), "arn:aws:dynamodb:{0}:{1}:table/ServerlessSaaS-Settings".format(region, aws_account_id) ] } ] } else: policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Product-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Order-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } } ] } return json.dumps(policy) def __getPolicyForTenantUser(tenant_id, region, aws_account_id): policy = { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Product-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } }, { "Effect": "Allow", "Action": [ "dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:Query" ], "Resource": [ "arn:aws:dynamodb:{0}:{1}:table/Order-*".format(region, aws_account_id), ], "Condition": { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [ "{0}-*".format(tenant_id) ] } } } ] } return json.dumps(policy) ================================================ FILE: Solution/Lab6/server/layers/logger.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 from aws_lambda_powertools import Logger logger = Logger() """Log info messages """ def info(log_message): #logger.structure_logs(append=True, tenant_id=tenant_id) logger.info (log_message) """Log error messages """ def error(log_message): #logger.structure_logs(append=True, tenant_id=tenant_id) logger.error (log_message) """Log with tenant context. Extracts tenant context from the lambda events """ def log_with_tenant_context(event, log_message): logger.structure_logs(append=True, tenant_id= event['requestContext']['authorizer']['tenantId']) logger.info (log_message) ================================================ FILE: Solution/Lab6/server/layers/metrics_manager.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json from aws_lambda_powertools import Metrics metrics = Metrics() def record_metric(event, metric_name, metric_unit, metric_value): """ Record the metric in Cloudwatch using EMF format Args: event ([type]): [description] metric_name ([type]): [description] metric_unit ([type]): [description] metric_value ([type]): [description] """ metrics.add_dimension(name="tenant_id", value=event['requestContext']['authorizer']['tenantId']) metrics.add_metric(name=metric_name, unit=metric_unit, value=metric_value) metrics_object = metrics.serialize_metric_set() metrics.clear_metrics() print(json.dumps(metrics_object)) ================================================ FILE: Solution/Lab6/server/layers/requirements.txt ================================================ aws-lambda-powertools[Tracer,Logger,Metrics] simplejson jsonpickle aws_requests_auth python-jose[cryptography] aws_requests_auth ================================================ FILE: Solution/Lab6/server/layers/utils.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import json import jsonpickle import simplejson import boto3 from aws_requests_auth.aws_auth import AWSRequestsAuth from enum import Enum class TenantTier(Enum): PLATINUM = "Platinum" PREMIUM = "Premium" STANDARD = "Standard" BASIC = "Basic" class StatusCodes(Enum): SUCCESS = 200 UN_AUTHORIZED = 401 NOT_FOUND = 404 class Service_Identifier(Enum): SHARED_SERVICES = "SharedServices" BUSINESS_SERVICES = "BusinessServices" def create_success_response(message): return { "statusCode": StatusCodes.SUCCESS.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def create_unauthorized_response(): return { "statusCode": StatusCodes.UN_AUTHORIZED.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": "User not authorized to perform this action" }), } def create_notfound_response(message): return { "statusCode": StatusCodes.NOT_FOUND.value, "headers": { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": json.dumps({ "message": message }), } def get_auth(host, region): session = boto3.Session() credentials = session.get_credentials() auth = AWSRequestsAuth(aws_access_key=credentials.access_key, aws_secret_access_key=credentials.secret_key, aws_token=credentials.token, aws_host=host, aws_region=region, aws_service='execute-api') return auth def get_headers(event): return event['headers'] def generate_response(inputObject): return { "statusCode": 200, "headers": { "Access-Control-Allow-Headers" : "Content-Type, Origin, X-Requested-With, Accept, Authorization, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Origin", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST,GET,PUT" }, "body": encode_to_json_object(inputObject), } def encode_to_json_object(inputObject): jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True) jsonpickle.set_preferred_backend('simplejson') return jsonpickle.encode(inputObject, unpicklable=False, use_decimal=True) ================================================ FILE: Solution/Lab6/server/nested_templates/apigateway.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway, apis, api keys and usage plan as part of bootstrap Parameters: StageName: Type: String RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ProvisionTenantFunctionArn: Type: String DeProvisionTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetTenantConfigFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String ApiKeyOperationUsersParameter: Type: String ApiKeyPlatinumTierParameter: Type: String ApiKeyPremiumTierParameter: Type: String ApiKeyStandardTierParameter: Type: String ApiKeyBasicTierParameter: Type: String Resources: ApiGatewayCloudWatchLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub apigateway-cloudwatch-publish-role-${AWS::Region} Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole ApiGatewayAttachCloudwatchLogArn: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchLogRole.Arn AdminApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/api-gateway/access-logs-serverless-saas-admin-api RetentionInDays: 30 AdminApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: "/*" HttpMethod: "*" Auth: ResourcePolicy: CustomStatements: - Effect: Allow Principal: "*" Action: "execute-api:Invoke" Resource: ["execute-api:/*/*/*"] - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/tenant" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/user/tenant-admin" ] ] - !Join [ "", [ "execute-api:/", !Ref StageName, "/POST/provisioning" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref RegisterTenantLambdaExecutionRoleArn - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/disable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/users/enable" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn - Effect: Deny Principal: "*" Action: "execute-api:Invoke" Resource: - !Join [ "", [ "execute-api:/", !Ref StageName, "/PUT/provisioning/{tenantid}" ] ] Condition: StringNotEquals: aws:PrincipalArn: - !Ref TenantManagementLambdaExecutionRoleArn AccessLogSetting: DestinationArn: !GetAtt AdminApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ["", ["serverless-saas-admin-api-", !Ref "AWS::Region"]] basePath: !Join ["", ["/", !Ref StageName]] x-amazon-apigateway-api-key-source : "AUTHORIZER" schemes: - https paths: /registration: post: summary: Register a new tenant description: Register a new tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref RegisterTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /provisioning: post: summary: provisions resource for new tenant description: provisions resource for new tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ProvisionTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /provisioning/{tenantid}: put: summary: deprovision by tenant description: deprovision by tenant produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeProvisionTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/activation/{tenantid}: put: security: - api_key: [] - Authorizer: [] summary: Activate an existing tenant description: Activate an existing tenant produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - "" - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref ActivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenants: get: summary: Returns all tenants description: Returns all tenants produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantsFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /tenant: post: summary: Creates a tenant description: Creates a tenant produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/init/{tenantname}: get: summary: Returns a tenant config description: Return a tenant config by a tenant name produces: - application/json responses: {} x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantConfigFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /tenant/{tenantid}: get: summary: Returns a tenant description: Return a tenant by a tenant id produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Disables a tenant description: Disables a tenant by a tenant id produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DeactivateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy put: summary: Updates a tenant description: Updates a tenant produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateTenantFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user/{username}: get: summary: Returns a user description: Return a user by a user id produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUserFunctionArn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref UpdateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy delete: summary: Diables a user description: Disable a user by a user id produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUserFunctionArn - /invocations httpMethod: POST type: aws_proxy /user/tenant-admin: post: summary: Creates a tenant admin user description: Creates a tenant admin user produces: - application/json responses: {} security: - sigv4Reference: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateTenantAdminUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /user: post: summary: Create a user description: Create a user by a user id produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref CreateUserFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users: get: summary: Get all users by tenantId description: Get all users by tenantId produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref GetUsersFunctionArn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/disable: put: summary: disable users by tenant id description: disable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref DisableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /users/enable: put: summary: enable users by tenant id description: enable users by tenant id produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string security: - sigv4Reference: [] x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref EnableUsersByTenantFunctionArn - /invocations httpMethod: POST type: AWS requestParameters: integration.request.header.X-Amz-Invocation-Type: '''Event''' options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: api_key: type: "apiKey" name: "x-api-key" in: "header" sigv4Reference: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "awsSigv4" Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !Ref AuthorizerFunctionArn - /invocations authorizerResultTtlInSeconds: 60 type: "token" StageName: prod #Create API Keys and Usage Plans APIGatewayApiKeySystemAdmin: Type: AWS::ApiGateway::ApiKey Properties: Description: "This is the api key to be used by system admin" Enabled: True Name: Serverless-SaaS-SysAdmin-ApiKey Value: !Ref ApiKeyOperationUsersParameter APIGatewayApiKeyPlatinumTier: Type: AWS::ApiGateway::ApiKey Properties: Description: 'This is the api key to be used by platinum tier tenants' Enabled: True Name: Serverless-SaaS-PlatinumTier-ApiKey Value: !Ref ApiKeyPlatinumTierParameter APIGatewayApiKeyPremiumTier: Type: AWS::ApiGateway::ApiKey Properties: Description: 'This is the api key to be used by premium tier tenants' Enabled: True Name: Serverless-SaaS-PremiumTier-ApiKey Value: !Ref ApiKeyPremiumTierParameter APIGatewayApiKeyStandardTier: Type: AWS::ApiGateway::ApiKey Properties: Description: 'This is the api key to be used by standard tier tenants' Enabled: True Name: Serverless-SaaS-StandardTier-ApiKey Value: !Ref ApiKeyStandardTierParameter APIGatewayApiKeyBasicTier: Type: AWS::ApiGateway::ApiKey Properties: Description: 'This is the api key to be used by basic tier tenants' Enabled: True Name: Serverless-SaaS-BasicTier-ApiKey Value: !Ref ApiKeyBasicTierParameter UsagePlanPlatinumTier: Type: 'AWS::ApiGateway::UsagePlan' Properties: ApiStages: - ApiId: !Ref AdminApiGatewayApi Stage: !Ref StageName Description: Usage plan for platinum tier tenants Quota: Limit: 10000 Period: DAY Throttle: BurstLimit: 300 RateLimit: 300 UsagePlanName: Plan_Platinum_Tier DependsOn: - AdminApiGatewayApiprodStage UsagePlanPremiumTier: Type: 'AWS::ApiGateway::UsagePlan' Properties: ApiStages: - ApiId: !Ref AdminApiGatewayApi Stage: !Ref StageName Description: Usage plan for premium tier tenants Quota: Limit: 5000 Period: DAY Throttle: BurstLimit: 200 RateLimit: 100 UsagePlanName: Plan_Premium_Tier DependsOn: - AdminApiGatewayApiprodStage UsagePlanStandardTier: Type: 'AWS::ApiGateway::UsagePlan' Properties: ApiStages: - ApiId: !Ref AdminApiGatewayApi Stage: !Ref StageName Description: Usage plan for standard tier tenants Quota: Limit: 3000 Period: DAY Throttle: BurstLimit: 100 RateLimit: 75 UsagePlanName: Plan_Standard_Tier DependsOn: - AdminApiGatewayApiprodStage UsagePlanBasicTier: Type: 'AWS::ApiGateway::UsagePlan' Properties: ApiStages: - ApiId: !Ref AdminApiGatewayApi Stage: !Ref StageName Description: Usage plan for basic tier tenants Quota: Limit: 500 Period: DAY Throttle: BurstLimit: 50 RateLimit: 50 UsagePlanName: Plan_Basic_Tier DependsOn: - AdminApiGatewayApiprodStage UsagePlanSystemAdmin: Type: "AWS::ApiGateway::UsagePlan" Properties: ApiStages: - ApiId: !Ref AdminApiGatewayApi Stage: !Ref StageName Description: Usage plan for system admin Quota: Limit: 10000 Period: DAY Throttle: BurstLimit: 5000 RateLimit: 500 UsagePlanName: System_Admin_Usage_Plan DependsOn: - AdminApiGatewayApiprodStage AssociateAPIKeyToUsagePlan: Type: AWS::ApiGateway::UsagePlanKey Properties: KeyId: !Ref APIGatewayApiKeySystemAdmin KeyType: API_KEY UsagePlanId: !Ref UsagePlanSystemAdmin DependsOn: UsagePlanSystemAdmin AssociatePlatinumAPIKeyToUsagePlan: Type: AWS::ApiGateway::UsagePlanKey Properties: KeyId: !Ref APIGatewayApiKeyPlatinumTier KeyType: API_KEY UsagePlanId: !Ref UsagePlanPlatinumTier DependsOn: UsagePlanPlatinumTier AssociatePremiumAPIKeyToUsagePlan: Type: AWS::ApiGateway::UsagePlanKey Properties: KeyId: !Ref APIGatewayApiKeyPremiumTier KeyType: API_KEY UsagePlanId: !Ref UsagePlanPremiumTier DependsOn: UsagePlanPremiumTier AssociateStandardAPIKeyToUsagePlan: Type: AWS::ApiGateway::UsagePlanKey Properties: KeyId: !Ref APIGatewayApiKeyStandardTier KeyType: API_KEY UsagePlanId: !Ref UsagePlanStandardTier DependsOn: UsagePlanStandardTier AssociateBasicAPIKeyToUsagePlan: Type: AWS::ApiGateway::UsagePlanKey Properties: KeyId: !Ref APIGatewayApiKeyBasicTier KeyType: API_KEY UsagePlanId: !Ref UsagePlanBasicTier DependsOn: UsagePlanBasicTier Outputs: UsagePlanBasicTier: Value: !Ref UsagePlanBasicTier UsagePlanStandardTier: Value: !Ref UsagePlanStandardTier UsagePlanPremiumTier: Value: !Ref UsagePlanPremiumTier UsagePlanPlatinumTier: Value: !Ref UsagePlanPlatinumTier AdminApiGatewayApi: Value: !Ref AdminApiGatewayApi ================================================ FILE: Solution/Lab6/server/nested_templates/apigateway_lambdapermissions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup api gateway and apis as part of bootstrap Parameters: RegisterTenantLambdaExecutionRoleArn: Type: String TenantManagementLambdaExecutionRoleArn: Type: String RegisterTenantFunctionArn: Type: String ProvisionTenantFunctionArn: Type: String DeProvisionTenantFunctionArn: Type: String ActivateTenantFunctionArn: Type: String GetTenantsFunctionArn: Type: String CreateTenantFunctionArn: Type: String GetTenantFunctionArn: Type: String DeactivateTenantFunctionArn: Type: String UpdateTenantFunctionArn: Type: String GetTenantConfigFunctionArn: Type: String GetUsersFunctionArn: Type: String GetUserFunctionArn: Type: String UpdateUserFunctionArn: Type: String DisableUserFunctionArn: Type: String CreateTenantAdminUserFunctionArn: Type: String CreateUserFunctionArn: Type: String DisableUsersByTenantFunctionArn: Type: String EnableUsersByTenantFunctionArn: Type: String AuthorizerFunctionArn: Type: String AdminApiGatewayApi: Type: String Resources: #provide api gateway permissions to call lambda functions RegisterTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref RegisterTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateTenantAdminUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantAdminUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ProvisionTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ProvisionTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeProvisionTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeProvisionTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] CreateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DisableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DisableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] EnableUsersByTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref EnableUsersByTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUsersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUsersFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetUserLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetUserFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref AuthorizerFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*" ]] CreateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref CreateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] UpdateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref UpdateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantsFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] DeactivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref DeactivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ActivateTenantLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref ActivateTenantFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] GetTenantConfigLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref GetTenantConfigFunctionArn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref AdminApiGatewayApi, "/*/*/*" ]] ================================================ FILE: Solution/Lab6/server/nested_templates/cognito.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to setup cognito as part of bootstrap Parameters: AdminEmailParameter: Type: String Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Description: "Enter the role name for system admin" AdminUserPoolCallbackURLParameter: Type: String Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Description: "Enter Tenant Management userpool call back url" Resources: CognitoUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: PooledTenant-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into tenant UI application at " - "https://" - !Ref TenantUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for tenant UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSClient GenerateSecret: false UserPoolId: !Ref CognitoUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [pooledtenant-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoUserPool CognitoOperationUsersUserPool: Type: "AWS::Cognito::UserPool" Properties: UserPoolName: OperationUsers-ServerlessSaaSUserPool AutoVerifiedAttributes: - "email" AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: InviteMessageTemplate: EmailMessage: !Join - "" - - "Login into admin UI application at " - "https://" - !Ref AdminUserPoolCallbackURLParameter - "/" - " with username {username} and temporary password {####}" EmailSubject: !Join - "" - - "Your temporary password for admin UI application" Schema: - AttributeDataType: "String" Name: email Required: True Mutable: True - AttributeDataType: "String" Name: tenantId - AttributeDataType: "String" Name: userRole Required: False Mutable: True CognitoOperationUsersUserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: ServerlessSaaSOperationUsersPoolClient GenerateSecret: false UserPoolId: !Ref CognitoOperationUsersUserPool AllowedOAuthFlowsUserPoolClient: True AllowedOAuthFlows: - code - implicit SupportedIdentityProviders: - COGNITO CallbackURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] LogoutURLs: - !Join ["",["https://", !Ref AdminUserPoolCallbackURLParameter, "/"]] AllowedOAuthScopes: - email - openid - profile WriteAttributes: - "email" - "custom:tenantId" - "custom:userRole" CognitoOperationUsersUserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: Domain: !Join ["-", [operationsusers-serverlesssaas,!Ref "AWS::AccountId"]] UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUserGroup: Type: AWS::Cognito::UserPoolGroup Properties: GroupName: SystemAdmins Description: Admin user group Precedence: 0 UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAdminUser: Type: AWS::Cognito::UserPoolUser Properties: Username: admin DesiredDeliveryMediums: - EMAIL ForceAliasCreation: true UserAttributes: - Name: email Value: !Ref AdminEmailParameter - Name: custom:tenantId Value: system_admins - Name: custom:userRole Value: !Ref SystemAdminRoleNameParameter UserPoolId: !Ref CognitoOperationUsersUserPool CognitoAddUserToGroup: Type: AWS::Cognito::UserPoolUserToGroupAttachment Properties: GroupName: !Ref CognitoAdminUserGroup Username: !Ref CognitoAdminUser UserPoolId: !Ref CognitoOperationUsersUserPool Outputs: CognitoUserPoolId: Value: !Ref CognitoUserPool CognitoUserPoolClientId: Value: !Ref CognitoUserPoolClient CognitoOperationUsersUserPoolId: Value: !Ref CognitoOperationUsersUserPool CognitoOperationUsersUserPoolClientId: Value: !Ref CognitoOperationUsersUserPoolClient CognitoOperationUsersUserPoolProviderURL: Value: !GetAtt CognitoOperationUsersUserPool.ProviderURL ================================================ FILE: Solution/Lab6/server/nested_templates/custom_resources.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: ServerlessSaaSSettingsTableArn: Type: String ServerlessSaaSSettingsTableName: Type: String TenantStackMappingTableArn: Type: String TenantStackMappingTableName: Type: String UpdateSettingsTableFunctionArn: Type: String UpdateTenantStackMapTableFunctionArn: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String Resources: #Custom resources UpdateSettingsTable: Type: Custom::UpdateSettingsTable Properties: ServiceToken: !Ref UpdateSettingsTableFunctionArn SettingsTableName: !Ref ServerlessSaaSSettingsTableName cognitoUserPoolId: !Ref CognitoUserPoolId cognitoUserPoolClientId: !Ref CognitoUserPoolClientId UpdateTenantStackMap: Type: Custom::UpdateTenantStackMap Properties: ServiceToken: !Ref UpdateTenantStackMapTableFunctionArn TenantStackMappingTableName: !Ref TenantStackMappingTableName ================================================ FILE: Solution/Lab6/server/nested_templates/lambdafunctions.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy lambda functions as part of bootstrap Parameters: CognitoOperationUsersUserPoolId: Type: String CognitoOperationUsersUserPoolClientId: Type: String CognitoUserPoolId: Type: String CognitoUserPoolClientId: Type: String TenantDetailsTableArn: Type: String ServerlessSaaSSettingsTableArn: Type: String ApiKeyOperationUsersParameter: Type: String ApiKeyPlatinumTierParameter: Type: String ApiKeyPremiumTierParameter: Type: String ApiKeyStandardTierParameter: Type: String ApiKeyBasicTierParameter: Type: String TenantStackMappingTableArn: Type: String TenantUserMappingTableArn: Type: String TenantStackMappingTableName: Type: String TenantUserPoolCallbackURLParameter: Type: String Description: "Enter Tenant Management userpool call back url" Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-dependencies Description: Utilities for project ContentUri: ../layers/ CompatibleRuntimes: - python3.9 LicenseInfo: "MIT" RetentionPolicy: Retain Metadata: BuildMethod: python3.9 #Tenant Authorizer AuthorizerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: authorizer-execution-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: authorizer-execution-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:List* Resource: - !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn AuthorizerAccessRole: Type: AWS::IAM::Role DependsOn: AuthorizerExecutionRole Properties: RoleName: authorizer-access-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: - !GetAtt 'AuthorizerExecutionRole.Arn' Action: - sts:AssumeRole Policies: - PolicyName: authorizer-access-role-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:BatchGetItem - dynamodb:GetItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:UpdateItem - dynamodb:Query - dynamodb:Scan Resource: - !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/* SharedServicesAuthorizerFunction: Type: AWS::Serverless::Function DependsOn: AuthorizerAccessRole Properties: CodeUri: ../Resources/ Handler: shared_service_authorizer.lambda_handler Runtime: python3.9 Role: !GetAtt AuthorizerExecutionRole.Arn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !Ref CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !Ref CognitoOperationUsersUserPoolClientId OPERATION_USERS_API_KEY : !Ref ApiKeyOperationUsersParameter #Create user pool for the tenant TenantUserPoolLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-userpool-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-userpool-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn - Effect: Allow Action: - dynamodb:GetItem - dynamodb:Query Resource: - !Ref TenantUserMappingTableArn CreateUserLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub create-user-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-user-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:* Resource: "*" - Effect: Allow Action: - dynamodb:PutItem Resource: - !Ref TenantUserMappingTableArn - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref TenantDetailsTableArn CreateTenantAdminUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_tenant_admin_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_USER_POOL_ID: !Ref CognitoUserPoolId TENANT_APP_CLIENT_ID: !Ref CognitoUserPoolClientId TENANT_USER_POOL_CALLBACK_URL: !Join ["",["https://",!Ref TenantUserPoolCallbackURLParameter, "/"]] POWERTOOLS_SERVICE_NAME: "UserManagement.CreateTenantAdmin" #User management CreateUserFunction: Type: AWS::Serverless::Function DependsOn: CreateUserLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.create_user Runtime: python3.9 Role: !GetAtt CreateUserLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.CreateUser" UpdateUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.update_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.UpdateUser" DisableUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUser" DisableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.disable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.DisableUsersByTenant" EnableUsersByTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.enable_users_by_tenant Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.EnableUsersByTenant" GetUserFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_user Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.GetUser" GetUsersFunction: Type: AWS::Serverless::Function DependsOn: TenantUserPoolLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: user-management.get_users Runtime: python3.9 Role: !GetAtt TenantUserPoolLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "UserManagement.GetUsers" #Tenant Management TenantManagementLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-management-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub create-tenant-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:Scan - dynamodb:Query Resource: - !Ref TenantDetailsTableArn - !Join ["", [!Ref TenantDetailsTableArn, '/index/*']] - Effect: Allow Action: - dynamodb:GetItem Resource: - !Ref ServerlessSaaSSettingsTableArn CreateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.create_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.CreateTenant" ActivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.activate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.ActivateTenant" ENABLE_USERS_BY_TENANT: "/users/enable" PROVISION_TENANT: "/provisioning/" GetTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.GetTenant" DeactivateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.deactivate_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.DeactivateTenant" DEPROVISION_TENANT: "/provisioning/" DISABLE_USERS_BY_TENANT: "/users/disable" UpdateTenantFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.update_tenant Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "TenantManagement.UpdateTenant" PLATINUM_TIER_API_KEY: !Ref ApiKeyPlatinumTierParameter PREMIUM_TIER_API_KEY: !Ref ApiKeyPremiumTierParameter STANDARD_TIER_API_KEY: !Ref ApiKeyStandardTierParameter BASIC_TIER_API_KEY: !Ref ApiKeyBasicTierParameter GetTenantsFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.get_tenants Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers GetTenantConfigFunction: Type: AWS::Serverless::Function DependsOn: TenantManagementLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-management.load_tenant_config Runtime: python3.9 Role: !GetAtt TenantManagementLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers #Tenant Registration RegisterTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-registration-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess RegisterTenantFunction: Type: AWS::Serverless::Function DependsOn: RegisterTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-registration.register_tenant Runtime: python3.9 Role: !GetAtt RegisterTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: # Need to find a better way than hard coding resource paths CREATE_TENANT_ADMIN_USER_RESOURCE_PATH: "/user/tenant-admin" CREATE_TENANT_RESOURCE_PATH: "/tenant" PROVISION_TENANT_RESOURCE_PATH: "/provisioning" PLATINUM_TIER_API_KEY: !Ref ApiKeyPlatinumTierParameter PREMIUM_TIER_API_KEY: !Ref ApiKeyPremiumTierParameter STANDARD_TIER_API_KEY: !Ref ApiKeyStandardTierParameter BASIC_TIER_API_KEY: !Ref ApiKeyBasicTierParameter POWERTOOLS_SERVICE_NAME: "TenantRegistration.RegisterTenant" #Tenant Provisioning ProvisionTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-provisioning-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-provisioning-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem - dynamodb:DeleteItem Resource: - !Ref TenantStackMappingTableArn - Effect: Allow Action: - codepipeline:StartPipelineExecution Resource: - !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:serverless-saas-pipeline - Effect: Allow Action: - cloudformation:DeleteStack Resource: "*" ProvisionTenantFunction: Type: AWS::Serverless::Function DependsOn: ProvisionTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-provisioning.provision_tenant Runtime: python3.9 Role: !GetAtt ProvisionTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_STACK_MAPPING_TABLE_NAME: !Ref TenantStackMappingTableName DeProvisionTenantLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub tenant-deprovisioning-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub tenant-deprovisioning-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 #Since this lambda is invoking cloudformation which is inturn removing AWS resources, we are giving overly permissive permissions to this lambda. #You can limit this based upon your use case and AWS Resources you need to remove. Statement: - Effect: Allow Action: "*" Resource: "*" DeProvisionTenantFunction: Type: AWS::Serverless::Function DependsOn: DeProvisionTenantLambdaExecutionRole Properties: CodeUri: ../TenantManagementService/ Handler: tenant-provisioning.deprovision_tenant Runtime: python3.9 Role: !GetAtt DeProvisionTenantLambdaExecutionRole.Arn Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: TENANT_STACK_MAPPING_TABLE_NAME: !Ref TenantStackMappingTableName UpdateSettingsTableLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub update-settingstable-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub update-settingstable-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: !Ref ServerlessSaaSSettingsTableArn UpdateSettingsTableFunction: Type: AWS::Serverless::Function DependsOn: UpdateSettingsTableLambdaExecutionRole Properties: CodeUri: ../custom_resources/ Handler: update_settings_table.handler Runtime: python3.9 Role: !GetAtt UpdateSettingsTableLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers UpdateTenantStackMapTableLambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub update-tenantstackmap-lambda-execution-role-${AWS::Region} Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Sub update-tenantstackmap-lambda-execution-policy-${AWS::Region} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: !Ref TenantStackMappingTableArn UpdateTenantStackMapTableFunction: Type: AWS::Serverless::Function DependsOn: UpdateTenantStackMapTableLambdaExecutionRole Properties: CodeUri: ../custom_resources/ Handler: update_tenantstackmap_table.handler Runtime: python3.9 Role: !GetAtt UpdateTenantStackMapTableLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Outputs: RegisterTenantLambdaExecutionRoleArn: Value: !GetAtt RegisterTenantLambdaExecutionRole.Arn TenantManagementLambdaExecutionRoleArn: Value: !GetAtt TenantManagementLambdaExecutionRole.Arn RegisterTenantFunctionArn: Value: !GetAtt RegisterTenantFunction.Arn ProvisionTenantFunctionArn: Value: !GetAtt ProvisionTenantFunction.Arn DeProvisionTenantFunctionArn: Value: !GetAtt DeProvisionTenantFunction.Arn ActivateTenantFunctionArn: Value: !GetAtt ActivateTenantFunction.Arn GetTenantConfigFunctionArn: Value: !GetAtt GetTenantConfigFunction.Arn GetTenantsFunctionArn: Value: !GetAtt GetTenantsFunction.Arn CreateTenantFunctionArn: Value: !GetAtt CreateTenantFunction.Arn GetTenantFunctionArn: Value: !GetAtt GetTenantFunction.Arn DeactivateTenantFunctionArn: Value: !GetAtt DeactivateTenantFunction.Arn UpdateTenantFunctionArn: Value: !GetAtt UpdateTenantFunction.Arn GetUsersFunctionArn: Value: !GetAtt GetUsersFunction.Arn GetUserFunctionArn: Value: !GetAtt GetUserFunction.Arn UpdateUserFunctionArn: Value: !GetAtt UpdateUserFunction.Arn DisableUserFunctionArn: Value: !GetAtt DisableUserFunction.Arn CreateTenantAdminUserFunctionArn: Value: !GetAtt CreateTenantAdminUserFunction.Arn CreateUserFunctionArn: Value: !GetAtt CreateUserFunction.Arn DisableUsersByTenantFunctionArn: Value: !GetAtt DisableUsersByTenantFunction.Arn EnableUsersByTenantFunctionArn: Value: !GetAtt EnableUsersByTenantFunction.Arn SharedServicesAuthorizerFunctionArn: Value: !GetAtt SharedServicesAuthorizerFunction.Arn AuthorizerExecutionRoleArn: Value: !GetAtt AuthorizerExecutionRole.Arn UpdateSettingsTableFunctionArn: Value: !GetAtt UpdateSettingsTableFunction.Arn UpdateTenantStackMapTableFunctionArn: Value: !GetAtt UpdateTenantStackMapTableFunction.Arn ================================================ FILE: Solution/Lab6/server/nested_templates/tables.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to create dynamodb tables as part of bootstrap Resources: ServerlessSaaSSettingsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: settingName AttributeType: S KeySchema: - AttributeName: settingName KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-Settings TenantStackMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantStackMapping TenantDetailsTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: tenantName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: ServerlessSaas-TenantConfig KeySchema: - AttributeName: tenantName KeyType: HASH Projection: NonKeyAttributes: - userPoolId - appClientId - apiGatewayUrl ProjectionType: INCLUDE TableName: ServerlessSaaS-TenantDetails TenantUserMappingTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: tenantId AttributeType: S - AttributeName: userName AttributeType: S KeySchema: - AttributeName: tenantId KeyType: HASH - AttributeName: userName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: ServerlessSaaS-TenantUserMapping GlobalSecondaryIndexes: - IndexName: UserName KeySchema: - AttributeName: userName KeyType: HASH - AttributeName: tenantId KeyType: RANGE Projection: ProjectionType: ALL Outputs: ServerlessSaaSSettingsTableArn: Value: !GetAtt ServerlessSaaSSettingsTable.Arn ServerlessSaaSSettingsTableName: Value: !Ref ServerlessSaaSSettingsTable TenantStackMappingTableArn: Value: !GetAtt TenantStackMappingTable.Arn TenantStackMappingTableName: Value: !Ref TenantStackMappingTable TenantDetailsTableArn: Value: !GetAtt TenantDetailsTable.Arn TenantDetailsTableName: Value: !Ref TenantDetailsTable TenantUserMappingTableArn: Value: !GetAtt TenantUserMappingTable.Arn TenantUserMappingTableName: Value: !Ref TenantUserMappingTable ================================================ FILE: Solution/Lab6/server/nested_templates/userinterface.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code Parameters: IsCloudFrontAndS3PreProvisioned: Type: String Default: false Description: "Tells if cloudfront and s3 buckets are pre-provisioned or not. They get pre-provisioned when the workshop is running as a part of AWS Event through AWS event engine tool." Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref IsCloudFrontAndS3PreProvisioned, true] ] Resources: CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: IsNotRunningInEventEngine Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distributions" AdminAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AdminAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AdminAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AdminAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId AdminAppSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: #Aliases: # - !Sub 'admin.${CustomDomainName}' CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS CachedMethods: - GET - HEAD - OPTIONS Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: adminapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt AdminAppBucket.RegionalDomainName Id: adminapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' AppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref AppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId ApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: tenantapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'AppBucket.RegionalDomainName' Id: tenantapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' LandingAppBucket: Type: AWS::S3::Bucket Condition: IsNotRunningInEventEngine DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True LandingAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Condition: IsNotRunningInEventEngine Properties: Bucket: !Ref LandingAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${LandingAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId LandingApplicationSite: Type: AWS::CloudFront::Distribution Condition: IsNotRunningInEventEngine Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: landingapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'LandingAppBucket.RegionalDomainName' Id: landingapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: AdminBucket: Description: The name of the bucket for uploading the Admin Management site to Value: !Ref AdminAppBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt AdminAppSite.DomainName Condition: IsNotRunningInEventEngine AppBucket: Description: The name of the bucket for uploading the Tenant Management site to Value: !Ref AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt ApplicationSite.DomainName Condition: IsNotRunningInEventEngine LandingAppBucket: Description: The name of the bucket for uploading the Landing site to Value: !Ref LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt LandingApplicationSite.DomainName Condition: IsNotRunningInEventEngine ================================================ FILE: Solution/Lab6/server/shared-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas" s3_bucket = "aws-saas-sam-cli-ujwbuket" s3_prefix = "serverless-saas" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" ================================================ FILE: Solution/Lab6/server/shared-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to Bootstrap the Common Resources Parameters: AdminEmailParameter: Type: String Default: "test@test.com" Description: "Enter system admin email address" SystemAdminRoleNameParameter: Type: String Default: "SystemAdmin" Description: "Enter the role name for system admin" ApiKeyOperationUsersParameter: Type: String Default: "9a7743fa-3ae7-11eb-adc1-0242ac120002" Description: "Enter default api key value to be used by api gateway for system admins" ApiKeyPlatinumTierParameter: Type: String Default: "88b43c36-802e-11eb-af35-38f9d35b2c15" Description: "Enter default api key value to be used by api gateway for platinum tier tenants" ApiKeyPremiumTierParameter: Type: String Default: "6db2bdc2-6d96-11eb-a56f-38f9d33cfd0f" Description: "Enter default api key value to be used by api gateway for premium tier tenants" ApiKeyStandardTierParameter: Type: String Default: "b1c735d8-6d96-11eb-a28b-38f9d33cfd0f" Description: "Enter default api key value to be used by api gateway for standard tier tenants" ApiKeyBasicTierParameter: Type: String Default: "daae9784-6d96-11eb-a28b-38f9d33cfd0f" Description: "Enter default api key value to be used by api gateway for basic tier tenants" StageName: Type: String Default: "prod" Description: "Stage Name for the api" EventEngineParameter: Type: String Default: false Description: "Tells if this workshop is running as a part of AWS Event through AWS EventEngine or not" AdminUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Admin Management userpool call back url" TenantUserPoolCallbackURLParameter: Type: String Default: "http://example.com" Description: "Enter Tenant Management userpool call back url" Conditions: IsNotRunningInEventEngine: !Not [ !Equals [ !Ref EventEngineParameter, true] ] Resources: DynamoDBTables: Type: AWS::Serverless::Application Properties: Location: nested_templates/tables.yaml #Create cloudfront and s3 for UI Cde UserInterface: Type: AWS::Serverless::Application Properties: Location: nested_templates/userinterface.yaml Parameters: IsCloudFrontAndS3PreProvisioned: !Ref EventEngineParameter Cognito: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/cognito.yaml Parameters: AdminEmailParameter: !Ref AdminEmailParameter SystemAdminRoleNameParameter: !Ref SystemAdminRoleNameParameter AdminUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.AdminAppSite, !Ref AdminUserPoolCallbackURLParameter] TenantUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.ApplicationSite, !Ref TenantUserPoolCallbackURLParameter] LambdaFunctions: Type: AWS::Serverless::Application DependsOn: UserInterface Properties: Location: nested_templates/lambdafunctions.yaml Parameters: CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId CognitoOperationUsersUserPoolId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId CognitoOperationUsersUserPoolClientId: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId TenantDetailsTableArn: !GetAtt DynamoDBTables.Outputs.TenantDetailsTableArn ServerlessSaaSSettingsTableArn: !GetAtt DynamoDBTables.Outputs.ServerlessSaaSSettingsTableArn ApiKeyOperationUsersParameter: !Ref ApiKeyOperationUsersParameter ApiKeyPlatinumTierParameter: !Ref ApiKeyPlatinumTierParameter ApiKeyPremiumTierParameter: !Ref ApiKeyPremiumTierParameter ApiKeyStandardTierParameter: !Ref ApiKeyStandardTierParameter ApiKeyBasicTierParameter: !Ref ApiKeyBasicTierParameter TenantStackMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableArn TenantUserMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantUserMappingTableArn TenantStackMappingTableName: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableName TenantUserPoolCallbackURLParameter: !If [IsNotRunningInEventEngine, !GetAtt UserInterface.Outputs.ApplicationSite, !Ref TenantUserPoolCallbackURLParameter] APIs: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway.yaml Parameters: StageName: !Ref StageName RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ProvisionTenantFunctionArn DeProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeProvisionTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn GetTenantConfigFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantConfigFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn ApiKeyOperationUsersParameter: !Ref ApiKeyOperationUsersParameter ApiKeyPlatinumTierParameter: !Ref ApiKeyPlatinumTierParameter ApiKeyPremiumTierParameter: !Ref ApiKeyPremiumTierParameter ApiKeyStandardTierParameter: !Ref ApiKeyStandardTierParameter ApiKeyBasicTierParameter: !Ref ApiKeyBasicTierParameter APIGatewayLambdaPermissions: Type: AWS::Serverless::Application DependsOn: LambdaFunctions Properties: Location: nested_templates/apigateway_lambdapermissions.yaml Parameters: RegisterTenantLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantLambdaExecutionRoleArn TenantManagementLambdaExecutionRoleArn: !GetAtt LambdaFunctions.Outputs.TenantManagementLambdaExecutionRoleArn RegisterTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.RegisterTenantFunctionArn ProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ProvisionTenantFunctionArn DeProvisionTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeProvisionTenantFunctionArn ActivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.ActivateTenantFunctionArn GetTenantConfigFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantConfigFunctionArn GetTenantsFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantsFunctionArn CreateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantFunctionArn GetTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.GetTenantFunctionArn DeactivateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DeactivateTenantFunctionArn UpdateTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantFunctionArn GetUsersFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUsersFunctionArn GetUserFunctionArn: !GetAtt LambdaFunctions.Outputs.GetUserFunctionArn UpdateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateUserFunctionArn DisableUserFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUserFunctionArn CreateTenantAdminUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateTenantAdminUserFunctionArn CreateUserFunctionArn: !GetAtt LambdaFunctions.Outputs.CreateUserFunctionArn DisableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.DisableUsersByTenantFunctionArn EnableUsersByTenantFunctionArn: !GetAtt LambdaFunctions.Outputs.EnableUsersByTenantFunctionArn AuthorizerFunctionArn: !GetAtt LambdaFunctions.Outputs.SharedServicesAuthorizerFunctionArn AdminApiGatewayApi: !GetAtt APIs.Outputs.AdminApiGatewayApi #setup custom resources CustomResources: Type: AWS::Serverless::Application DependsOn: APIs Properties: Location: nested_templates/custom_resources.yaml Parameters: ServerlessSaaSSettingsTableArn: !GetAtt DynamoDBTables.Outputs.ServerlessSaaSSettingsTableArn ServerlessSaaSSettingsTableName: !GetAtt DynamoDBTables.Outputs.ServerlessSaaSSettingsTableName TenantStackMappingTableArn: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableArn TenantStackMappingTableName: !GetAtt DynamoDBTables.Outputs.TenantStackMappingTableName UpdateSettingsTableFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateSettingsTableFunctionArn UpdateTenantStackMapTableFunctionArn: !GetAtt LambdaFunctions.Outputs.UpdateTenantStackMapTableFunctionArn CognitoUserPoolId: !GetAtt Cognito.Outputs.CognitoUserPoolId CognitoUserPoolClientId: !GetAtt Cognito.Outputs.CognitoUserPoolClientId Outputs: AdminApi: Description: "API Gateway endpoint URL for Admin API" Value: !Join ["", ["https://", !GetAtt APIs.Outputs.AdminApiGatewayApi, ".execute-api.", !Ref "AWS::Region", ".amazonaws.com/", !Ref StageName]] AdminSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant administration application Value: !GetAtt UserInterface.Outputs.AdminBucket Condition: IsNotRunningInEventEngine AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt UserInterface.Outputs.AdminAppSite Condition: IsNotRunningInEventEngine LandingApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the landing application Value: !GetAtt UserInterface.Outputs.LandingAppBucket Condition: IsNotRunningInEventEngine LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt UserInterface.Outputs.LandingApplicationSite Condition: IsNotRunningInEventEngine ApplicationSiteBucket: Description: The S3 Bucket that will contain the static assets for the tenant application Value: !GetAtt UserInterface.Outputs.AppBucket Condition: IsNotRunningInEventEngine ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt UserInterface.Outputs.ApplicationSite Condition: IsNotRunningInEventEngine CognitoOperationUsersUserPoolId: Description: The user pool id of Admin Management userpool Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolId Export: Name: "Serverless-SaaS-CognitoOperationUsersUserPoolId" CognitoOperationUsersUserPoolProviderURL: Description: The Admin Management userpool provider url Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolProviderURL CognitoOperationUsersUserPoolClientId: Description: The Admin Management userpool client id Value: !GetAtt Cognito.Outputs.CognitoOperationUsersUserPoolClientId Export: Name: "Serverless-SaaS-CognitoOperationUsersUserPoolClientId" CognitoTenantUserPoolId: Description: The user pool id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolId Export: Name: "Serverless-SaaS-CognitoTenantUserPoolId" CognitoTenantAppClientId: Description: The app client id for tenant user pool Value: !GetAtt Cognito.Outputs.CognitoUserPoolClientId Export: Name: "Serverless-SaaS-CognitoTenantAppClientId" AuthorizerExecutionRoleArn: Description: The Lambda authorizer execution role Value: !GetAtt LambdaFunctions.Outputs.AuthorizerExecutionRoleArn Export: Name: "Serverless-SaaS-AuthorizerExecutionRoleArn" UsagePlanBasicTier: Description: The basic tier usage plan Value: !GetAtt APIs.Outputs.UsagePlanBasicTier Export: Name: "Serverless-SaaS-UsagePlanBasicTier" UsagePlanStandardTier: Description: The standard tier usage plan Value: !GetAtt APIs.Outputs.UsagePlanStandardTier Export: Name: "Serverless-SaaS-UsagePlanStandardTier" UsagePlanPremiumTier: Description: The premium tier usage plan Value: !GetAtt APIs.Outputs.UsagePlanPremiumTier Export: Name: "Serverless-SaaS-UsagePlanPremiumTier" UsagePlanPlatinumTier: Description: The premium tier usage plan Value: !GetAtt APIs.Outputs.UsagePlanPlatinumTier Export: Name: "Serverless-SaaS-UsagePlanPlatinumTier" ApiKeyOperationUsers: Description: The api key of system admins Value: !Ref ApiKeyOperationUsersParameter Export: Name: "Serverless-SaaS-ApiKeyOperationUsers" ================================================ FILE: Solution/Lab6/server/tenant-buildspec.yml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 version: 0.2 phases: install: runtime-versions: python: 3.9 commands: # Install packages or any pre-reqs in this phase. # Upgrading SAM CLI to 1.33.0 version - python -m pip install aws-sam-cli==1.33.0 - sam --version # Installing project dependencies - cd Lab6/server/ProductService - python -m pip install -r requirements.txt - cd ../OrderService - python -m pip install -r requirements.txt pre_build: commands: # Run tests, lint scripts or any other pre-build checks. - cd .. - export PYTHONPATH=./ProductService/ # unit tests needs to be fixed. Commenting for now #- python -m pytest tests/unit/ProductService-test_handler.py build: commands: # Use Build phase to build your artifacts (compile, etc.) - sam build -t tenant-template.yaml post_build: commands: # Use Post-Build for notifications, git tags, upload artifacts to S3 - sam package --s3-bucket $PACKAGE_BUCKET --output-template-file packaged.yaml artifacts: discard-paths: yes files: # List of local artifacts that will be passed down the pipeline - Lab6/server/packaged.yaml ================================================ FILE: Solution/Lab6/server/tenant-samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "stack-pooled" s3_bucket = "aws-saas-sam-cli-ujwbuket" s3_prefix = "serverless-saas-tenant" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM" ================================================ FILE: Solution/Lab6/server/tenant-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Serverless SaaS Reference Architecture Globals: Function: Timeout: 29 Layers: - !Sub "arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14" Environment: Variables: LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: "ServerlessSaaS" Parameters: TenantIdParameter: Type: String Default: pooled Description: Tenant ID for the stack StageName: Type: String Default: "prod" Description: "Stage Name for the api" LambdaReserveConcurrency: Type: Number Default: 20 Description: "Reserve concurrency for lambda function. You can customize this on per tenant basis, if needed, by storing in the tenant table" Conditions: IsPooledDeploy: !Equals [ !Ref TenantIdParameter, pooled] IsSiloDeploy: !Not [!Equals [ !Ref TenantIdParameter, pooled]] Resources: ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: !Join ['-', [serverless-saas-dependencies, !Ref TenantIdParameter]] Description: Utilities for project ContentUri: layers/ CompatibleRuntimes: - python3.9 LicenseInfo: 'MIT' RetentionPolicy: Retain Metadata: BuildMethod: python3.9 ProductTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: productId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: productId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: !Join ['-', [Product, !Ref TenantIdParameter]] Tags: - Key: "TenantId" Value: !Ref TenantIdParameter OrderTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: shardId AttributeType: S - AttributeName: orderId AttributeType: S KeySchema: - AttributeName: shardId KeyType: HASH - AttributeName: orderId KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 TableName: !Join ['-', [Order, !Ref TenantIdParameter]] Tags: - Key: "TenantId" Value: !Ref TenantIdParameter ProductFunctionExecutionRolePolicy: Condition: IsSiloDeploy Type: AWS::IAM::Policy Properties: PolicyName: !Join ['-', [!Ref TenantIdParameter, product-function-policy]] Roles: - !Ref ProductFunctionExecutionRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt ProductTable.Arn ProductFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, product-function-execution-role]] Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess GetProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn ReservedConcurrentExecutions: !If [IsPooledDeploy, !Ref "AWS::NoValue" , !Ref LambdaReserveConcurrency] Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter GetProductsFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.get_products Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn ReservedConcurrentExecutions: !If [IsPooledDeploy, !Ref "AWS::NoValue" , !Ref LambdaReserveConcurrency] Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter CreateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.create_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter UpdateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.update_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter DeleteProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: ProductService/ Handler: product_service.delete_product Runtime: python3.9 Tracing: Active Role: !GetAtt ProductFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "ProductService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] PRODUCT_TABLE_NAME: !Ref ProductTable Tags: TenantId: !Ref TenantIdParameter OrderFunctionExecutionRolePolicy: Condition: IsSiloDeploy Type: AWS::IAM::Policy Properties: PolicyName: !Join ['-', [!Ref TenantIdParameter, order-function-policy]] Roles: - !Ref OrderFunctionExecutionRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query Resource: - !GetAtt OrderTable.Arn OrderFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, order-function-execution-role]] Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess GetOrdersFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_orders Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn ReservedConcurrentExecutions: !If [IsPooledDeploy, !Ref "AWS::NoValue" , !Ref LambdaReserveConcurrency] Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter GetOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.get_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn ReservedConcurrentExecutions: !If [IsPooledDeploy, !Ref "AWS::NoValue" , !Ref LambdaReserveConcurrency] Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter CreateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.create_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter UpdateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.update_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter DeleteOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: OrderService/ Handler: order_service.delete_order Runtime: python3.9 Tracing: Active Role: !GetAtt OrderFunctionExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: "OrderService" IS_POOLED_DEPLOY: !If [IsPooledDeploy, true, false] ORDER_TABLE_NAME: !Ref OrderTable Tags: TenantId: !Ref TenantIdParameter BusinessServicesAuthorizerFunction: Type: AWS::Serverless::Function Properties: CodeUri: Resources/ Handler: tenant_authorizer.lambda_handler Runtime: python3.9 Role: !ImportValue Serverless-SaaS-AuthorizerExecutionRoleArn MemorySize: 256 Tracing: Active Layers: - !Ref ServerlessSaaSLayers Environment: Variables: OPERATION_USERS_USER_POOL: !ImportValue Serverless-SaaS-CognitoOperationUsersUserPoolId OPERATION_USERS_APP_CLIENT: !ImportValue Serverless-SaaS-CognitoOperationUsersUserPoolClientId OPERATION_USERS_API_KEY : !ImportValue Serverless-SaaS-ApiKeyOperationUsers ApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Join ['-', [/aws/api-gateway/access-logs-serverless-saas-tenant-api-, !Ref TenantIdParameter]] RetentionInDays: 30 ThrottlingLimitMetricFilter: Type: AWS::Logs::MetricFilter Properties: LogGroupName: Ref: "ApiGatewayAccessLogs" FilterPattern: '{$.status = "429"}' MetricTransformations: - MetricValue: "1" MetricNamespace: "Serverless-SaaS-Reference-Architecture" MetricName: !Join ['-', ["ThrottlingLimitExceeded", !Ref TenantIdParameter]] ThrottlingLimitExceeded: Type: AWS::CloudWatch::Alarm Properties: AlarmDescription: Throttling limit exceeded errors ComparisonOperator: GreaterThanThreshold EvaluationPeriods: 1 MetricName: !Join ['-', ["ThrottlingLimitExceeded", !Ref TenantIdParameter]] Namespace: "Serverless-SaaS-Reference-Architecture" Period: 60 Statistic: SampleCount Threshold: 0 ApiGatewayTenantApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: False LoggingLevel: INFO MetricsEnabled: True ResourcePath: '/*' HttpMethod: '*' AccessLogSetting: DestinationArn: !GetAtt ApiGatewayAccessLogs.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: True DefinitionBody: openapi: 3.0.1 info: title: !Join ['-', [!Ref TenantIdParameter, 'serverless-saas-tenant-api']] basePath: !Join ['', ['/', !Ref StageName]] x-amazon-apigateway-api-key-source : "AUTHORIZER" schemes: - https paths: /order/{id}: get: summary: Returns a order description: Return a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a order description: Deletes a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /orders: get: summary: Returns all orders description: Returns all orders. produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetOrdersFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /order: post: produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateOrderFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product/{id}: get: summary: Returns a product description: Return a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt UpdateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a product description: Deletes a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt DeleteProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /products: get: summary: Returns all products description: Returns all products. produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt GetProductsFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock /product: post: produces: - application/json responses: {} security: - api_key: [] - Authorizer: [] x-amazon-apigateway-integration: uri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt CreateProductFunction.Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: "#/definitions/Empty" headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'" method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Origin: "'*'" passthroughBehavior: when_no_match requestTemplates: application/json: "{\"statusCode\": 200}" type: mock components: securitySchemes: Authorizer: type: "apiKey" name: "Authorization" in: "header" x-amazon-apigateway-authtype: "custom" x-amazon-apigateway-authorizer: authorizerUri: !Join - '' - - !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - !GetAtt BusinessServicesAuthorizerFunction.Arn - /invocations authorizerResultTtlInSeconds: 30 type: "token" StageName: !Ref StageName GetProductsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt GetProductsFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrdersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrdersFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] GetOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - GetOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] CreateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - CreateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] UpdateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - UpdateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] DeleteOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt - DeleteOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: !Join [ "", [ "arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", !Ref ApiGatewayTenantApi, "/*/*/*" ] ] AuthorizerLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt BusinessServicesAuthorizerFunction.Arn Principal: apigateway.amazonaws.com SourceArn: !Join ["", ["arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref ApiGatewayTenantApi, "/*/*" ]] UpdateUsagePlanLambdaExecutionRole: Type: AWS::IAM::Role DependsOn: ApiGatewayTenantApi Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, update-usage-plan-role]] Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy Policies: - PolicyName: !Join ['-', [!Ref TenantIdParameter, update-usage-plan-policy]] PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - kms:Decrypt Resource: !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/* - Effect: Allow Action: - logs:CreateLogGroup - logs:PutLogEvents - logs:CreateLogStream Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:* - Effect: Allow Action: - logs:DescribeLogStreams Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - xray:PutTraceSegments - xray:PutTelemetryRecords Resource: "*" - Effect: Allow Action: - apigateway:PATCH Resource: !Sub arn:aws:apigateway:${AWS::Region}::/usageplans/* - Effect: Allow Action: - dynamodb:GetItem Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/ServerlessSaaS-Settings UpdateUsagePlanFunction: Type: AWS::Serverless::Function DependsOn: UpdateUsagePlanLambdaExecutionRole Properties: CodeUri: custom_resources/ Handler: update_usage_plan.handler Runtime: python3.9 Role: !GetAtt UpdateUsagePlanLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers AssociateUsagePlanWithTenantAPI: Type: Custom::AssociateUsagePlanWithTenantAPI DependsOn: UpdateUsagePlanFunction Properties: ServiceToken: !GetAtt UpdateUsagePlanFunction.Arn ApiGatewayId: !Ref ApiGatewayTenantApi SettingsTableName: ServerlessSaaS-Settings IsPooledDeploy: !If [IsPooledDeploy, true, false] Stage: !Ref StageName UsagePlanBasicTier: !ImportValue Serverless-SaaS-UsagePlanBasicTier UsagePlanStandardTier: !ImportValue Serverless-SaaS-UsagePlanStandardTier UsagePlanPremiumTier: !ImportValue Serverless-SaaS-UsagePlanPremiumTier UsagePlanPlatinumTier: !ImportValue Serverless-SaaS-UsagePlanPlatinumTier UpdateTenantApiGatewayUrlLambdaExecutionRole: Type: AWS::IAM::Role DependsOn: ApiGatewayTenantApi Properties: RoleName: !Join ['-', [!Ref TenantIdParameter, apigwurl-lambda-exec-role]] Path: "/" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: !Join ['-', [!Ref TenantIdParameter, apigwurl-lambda-exe-policy ]] PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/ServerlessSaaS-Settings - Effect: Allow Action: - dynamodb:UpdateItem Resource: !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/ServerlessSaaS-TenantDetails UpdateTenantApiGatewayUrlFunction: Type: AWS::Serverless::Function DependsOn: UpdateTenantApiGatewayUrlLambdaExecutionRole Properties: CodeUri: custom_resources/ Handler: update_tenant_apigatewayurl.handler Runtime: python3.9 Role: !GetAtt UpdateTenantApiGatewayUrlLambdaExecutionRole.Arn Layers: - !Ref ServerlessSaaSLayers UpdateTenantApiGatewayUrl: Type: Custom::UpdateTenantApiGatewayUrl DependsOn: UpdateTenantApiGatewayUrlFunction Properties: ServiceToken: !GetAtt UpdateTenantApiGatewayUrlFunction.Arn TenantDetailsTableName: ServerlessSaaS-TenantDetails SettingsTableName: ServerlessSaaS-Settings TenantId: !Ref TenantIdParameter TenantApiGatewayUrl: !Sub "https://${ApiGatewayTenantApi}.execute-api.${AWS::Region}.amazonaws.com/prod/" Outputs: TenantApiGatewayId: Description: Id for Tenant API Gateway Value: !Ref ApiGatewayTenantApi TenantAPI: Description: "API Gateway endpoint URL for Tenant API" Value: !Join ['', [!Sub "https://${ApiGatewayTenantApi}.execute-api.${AWS::Region}.amazonaws.com/", !Ref StageName]] ================================================ FILE: Solution/Lab7/.aws-sam/build/GetDynamoDBUsageAndCostByTenant/requirements.txt ================================================ ================================================ FILE: Solution/Lab7/.aws-sam/build/GetDynamoDBUsageAndCostByTenant/tenant_usage_and_cost.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import boto3 import time import os from datetime import datetime, timedelta from botocore.exceptions import ClientError from decimal import * cloudformation = boto3.client('cloudformation') logs = boto3.client('logs') athena = boto3.client('athena') dynamodb = boto3.resource('dynamodb') attribution_table = dynamodb.Table("TenantCostAndUsageAttribution") ATHENA_S3_OUTPUT = os.getenv("ATHENA_S3_OUTPUT") RETRY_COUNT = 100 #This function needs to be scheduled on daily basis def calculate_daily_dynamodb_attribution_by_tenant(event, context): time_zone = datetime.now().astimezone().tzinfo start_date_time = int(datetime.now(tz=time_zone).date().strftime('%s')) #current day epoch end_date_time = int((datetime.now(tz=time_zone) + timedelta(days=1)).date().strftime('%s')) #next day epoch #Get total dynamodb cost for the given duration total_dynamodb_cost = __get_total_service_cost('AmazonDynamoDB', start_date_time, end_date_time) log_group_names = __get_list_of_log_group_names() usage_by_tenant_by_day_query = 'fields @timestamp, @message \ | filter @message like /ReadCapacityUnits|WriteCapacityUnits/ \ | fields tenant_id as TenantId, service as Service, \ ReadCapacityUnits.0 as RCapacityUnits, WriteCapacityUnits.0 as WCapacityUnits \ | stats sum(RCapacityUnits) as ReadCapacityUnits, sum(WCapacityUnits) as WriteCapacityUnits by TenantId, dateceil(@timestamp, 1d) as timestamp' print( log_group_names) usage_by_tenant_by_day = __query_cloudwatch_logs(logs, log_group_names, usage_by_tenant_by_day_query, start_date_time, end_date_time) print(usage_by_tenant_by_day) #optionally save this data in a table total_usage_by_day_query = 'fields @timestamp, @message \ | filter @message like /ReadCapacityUnits|WriteCapacityUnits/ \ | fields ReadCapacityUnits.0 as RCapacityUnits, WriteCapacityUnits.0 as WCapacityUnits \ | stats sum(RCapacityUnits) as ReadCapacityUnits, sum(WCapacityUnits) as WriteCapacityUnits by dateceil(@timestamp, 1d) as timestamp' total_usage_by_day = __query_cloudwatch_logs(logs, log_group_names, total_usage_by_day_query, start_date_time, end_date_time) print(total_usage_by_day) total_RCU = 0 total_WCU = 0 for result in total_usage_by_day['results'][0]: if 'ReadCapacityUnits' in result['field']: total_RCU = Decimal(result['value']) if 'WriteCapacityUnits' in result['field']: total_WCU = Decimal(result['value']) print (total_RCU) print (total_WCU) if (total_RCU + total_WCU > 0): total_RCU_By_Tenant = 0 total_WCU_By_Tenant = 0 for result in usage_by_tenant_by_day['results']: for field in result: if 'TenantId' in field['field']: tenant_id = field['value'] if 'ReadCapacityUnits' in field['field']: total_RCU_By_Tenant = Decimal(field['value']) if 'WriteCapacityUnits' in field['field']: total_WCU_By_Tenant = Decimal(field['value']) #RCU is about 5 times cheaper tenant_attribution_percentage= (((total_RCU_By_Tenant * 5) + total_WCU_By_Tenant) / ((total_RCU * 5) + total_WCU)) tenant_dynamodb_cost = tenant_attribution_percentage * total_dynamodb_cost try: response = attribution_table.put_item( Item= { "Date": start_date_time, "ServiceName": "DynamoDB", "TenantId": tenant_id, "TotalRCU": total_RCU, "TenantTotalRCU": total_RCU_By_Tenant, "TotalWCU": total_WCU, "TenantTotalWCU": total_WCU_By_Tenant, "TenantAttributionPercentage": tenant_attribution_percentage, "TenantServiceCost": tenant_dynamodb_cost, "TotalServiceCost": total_dynamodb_cost } ) except ClientError as e: print(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: print("PutItem succeeded:") tenant_id = 'unknown' total_RCU_By_Tenant = 0.0 total_WCU_By_Tenant = 0.0 #Below function considers number of invocation as the metrics to calculate usage and cost. #You can go granluar by recording duration of each metrics and use that to get more granular #Since our functions are basic CRUD this might work as a ball park cost estimate def calculate_daily_lambda_attribution_by_tenant(event, context): #Get total dynamodb cost for the given duration time_zone = datetime.now().astimezone().tzinfo start_date_time = int(datetime.now(tz=time_zone).date().strftime('%s')) #current day epoch end_date_time = int((datetime.now(tz=time_zone) + timedelta(days=1)).date().strftime('%s')) #next day epoch #Get total dynamodb cost for the given duration total_lambda_cost = __get_total_service_cost('AWSLambda', start_date_time, end_date_time) log_group_names = __get_list_of_log_group_names() usage_by_tenant_by_day_query='fields @timestamp, @message \ | filter @message like /Request completed/ \ | fields tenant_id as TenantId , CountLambdaInvocations.0 As LambdaInvocations, timestamp\ | stats count (tenant_id) as CountLambdaInvocations by TenantId, dateceil(@timestamp, 1d) as timestamp' usage_by_tenant_by_day = __query_cloudwatch_logs(logs, log_group_names, usage_by_tenant_by_day_query, start_date_time, end_date_time) print(usage_by_tenant_by_day) total_usage_by_day_query = 'filter @message like /Request completed/ \ | fields CountLambdaInvocations.0 As LambdaInvocations, timestamp\ | stats count (tenant_id) as CountLambdaInvocations by dateceil(@timestamp, 1d) as timestamp' total_usage_by_day = __query_cloudwatch_logs(logs, log_group_names, total_usage_by_day_query, start_date_time, end_date_time) print(total_usage_by_day) total_invocations = 1 #to avoid divide by zero for result in total_usage_by_day['results'][0]: if 'LambdaInvocations' in result['field']: total_invocations = Decimal(result['value']) print (total_invocations) if (total_invocations>0): total_invocations_by_tenant = 0 for result in usage_by_tenant_by_day['results']: for field in result: if 'TenantId' in field['field']: tenant_id = field['value'] if 'LambdaInvocations' in field['field']: total_invocations_by_tenant = Decimal(field['value']) #RCU is about 5 times cheaper tenant_attribution_percentage= (total_invocations_by_tenant / total_invocations) tenant_lambda_cost = tenant_attribution_percentage * total_lambda_cost try: response = attribution_table.put_item( Item= { "Date": start_date_time, "ServiceName": "AWSLambda", "TenantId": tenant_id, "TotalInvocations": total_invocations, "TenantTotalInvocations": total_invocations_by_tenant, "TenantAttributionPercentage": tenant_attribution_percentage, "TenantServiceCost": tenant_lambda_cost, "TotalServiceCost": total_lambda_cost } ) except ClientError as e: print(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: print("PutItem succeeded:") tenant_id = 'unknown' tenant_total_RCU = 0.0 tenant_total_WCU = 0.0 def __get_total_service_cost(servicename, start_date_time, end_date_time): # We need to add more filters for day, month, year, resource ids etc. Below query is because we are just using a sample cur file #Ignoting startTime and endTime filter for now since we have a static/sample cur file query = "SELECT sum(line_item_blended_cost) AS cost FROM costexplorerdb.curoutput WHERE line_item_product_code='{0}'".format(servicename) # Execution response = athena.start_query_execution( QueryString=query, QueryExecutionContext={ 'Database': 'costexplorerdb' }, ResultConfiguration={ 'OutputLocation': "s3://" + ATHENA_S3_OUTPUT, } ) # get query execution id query_execution_id = response['QueryExecutionId'] print(query_execution_id) # get execution status for i in range(1, 1 + RETRY_COUNT): # get query execution query_status = athena.get_query_execution(QueryExecutionId=query_execution_id) print (query_status) query_execution_status = query_status['QueryExecution']['Status']['State'] if query_execution_status == 'SUCCEEDED': print("STATUS:" + query_execution_status) break if query_execution_status == 'FAILED': raise Exception("STATUS:" + query_execution_status) else: print("STATUS:" + query_execution_status) time.sleep(i) else: athena.stop_query_execution(QueryExecutionId=query_execution_id) raise Exception('TIME OVER') # get query results result = athena.get_query_results(QueryExecutionId=query_execution_id) print (result) total_dynamo_db_cost = result['ResultSet']['Rows'][1]['Data'][0]['VarCharValue'] print(total_dynamo_db_cost) return Decimal(total_dynamo_db_cost) def __query_cloudwatch_logs(logs, log_group_names, query_string, start_time, end_time): query = logs.start_query(logGroupNames=log_group_names, startTime=start_time, endTime=end_time, queryString=query_string) query_results = logs.get_query_results(queryId=query["queryId"]) while query_results['status']=='Running' or query_results['status']=='Scheduled': time.sleep(5) query_results = logs.get_query_results(queryId=query["queryId"]) return query_results def __is_log_group_exists(logs_client, log_group_name): logs_paginator = logs_client.get_paginator('describe_log_groups') response_iterator = logs_paginator.paginate(logGroupNamePrefix=log_group_name) for log_groups_list in response_iterator: if not log_groups_list["logGroups"]: return False else: return True def __add_log_group_name(logs_client, log_group_name, log_group_names_list): if __is_log_group_exists(logs_client, log_group_name): log_group_names_list.append(log_group_name) def __get_list_of_log_group_names(): log_group_names = [] log_group_prefix = '/aws/lambda/' cloudformation_paginator = cloudformation.get_paginator('list_stack_resources') response_iterator = cloudformation_paginator.paginate(StackName='stack-pooled') for stack_resources in response_iterator: for resource in stack_resources['StackResourceSummaries']: if (resource["LogicalResourceId"] == "CreateProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "UpdateProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "GetProductsFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "DeleteProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "CreateOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "UpdateOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "GetOrdersFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "DeleteOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue return log_group_names ================================================ FILE: Solution/Lab7/.aws-sam/build/GetLambdaUsageAndCostByTenant/requirements.txt ================================================ ================================================ FILE: Solution/Lab7/.aws-sam/build/GetLambdaUsageAndCostByTenant/tenant_usage_and_cost.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import boto3 import time import os from datetime import datetime, timedelta from botocore.exceptions import ClientError from decimal import * cloudformation = boto3.client('cloudformation') logs = boto3.client('logs') athena = boto3.client('athena') dynamodb = boto3.resource('dynamodb') attribution_table = dynamodb.Table("TenantCostAndUsageAttribution") ATHENA_S3_OUTPUT = os.getenv("ATHENA_S3_OUTPUT") RETRY_COUNT = 100 #This function needs to be scheduled on daily basis def calculate_daily_dynamodb_attribution_by_tenant(event, context): time_zone = datetime.now().astimezone().tzinfo start_date_time = int(datetime.now(tz=time_zone).date().strftime('%s')) #current day epoch end_date_time = int((datetime.now(tz=time_zone) + timedelta(days=1)).date().strftime('%s')) #next day epoch #Get total dynamodb cost for the given duration total_dynamodb_cost = __get_total_service_cost('AmazonDynamoDB', start_date_time, end_date_time) log_group_names = __get_list_of_log_group_names() usage_by_tenant_by_day_query = 'fields @timestamp, @message \ | filter @message like /ReadCapacityUnits|WriteCapacityUnits/ \ | fields tenant_id as TenantId, service as Service, \ ReadCapacityUnits.0 as RCapacityUnits, WriteCapacityUnits.0 as WCapacityUnits \ | stats sum(RCapacityUnits) as ReadCapacityUnits, sum(WCapacityUnits) as WriteCapacityUnits by TenantId, dateceil(@timestamp, 1d) as timestamp' print( log_group_names) usage_by_tenant_by_day = __query_cloudwatch_logs(logs, log_group_names, usage_by_tenant_by_day_query, start_date_time, end_date_time) print(usage_by_tenant_by_day) #optionally save this data in a table total_usage_by_day_query = 'fields @timestamp, @message \ | filter @message like /ReadCapacityUnits|WriteCapacityUnits/ \ | fields ReadCapacityUnits.0 as RCapacityUnits, WriteCapacityUnits.0 as WCapacityUnits \ | stats sum(RCapacityUnits) as ReadCapacityUnits, sum(WCapacityUnits) as WriteCapacityUnits by dateceil(@timestamp, 1d) as timestamp' total_usage_by_day = __query_cloudwatch_logs(logs, log_group_names, total_usage_by_day_query, start_date_time, end_date_time) print(total_usage_by_day) total_RCU = 0 total_WCU = 0 for result in total_usage_by_day['results'][0]: if 'ReadCapacityUnits' in result['field']: total_RCU = Decimal(result['value']) if 'WriteCapacityUnits' in result['field']: total_WCU = Decimal(result['value']) print (total_RCU) print (total_WCU) if (total_RCU + total_WCU > 0): total_RCU_By_Tenant = 0 total_WCU_By_Tenant = 0 for result in usage_by_tenant_by_day['results']: for field in result: if 'TenantId' in field['field']: tenant_id = field['value'] if 'ReadCapacityUnits' in field['field']: total_RCU_By_Tenant = Decimal(field['value']) if 'WriteCapacityUnits' in field['field']: total_WCU_By_Tenant = Decimal(field['value']) #RCU is about 5 times cheaper tenant_attribution_percentage= (((total_RCU_By_Tenant * 5) + total_WCU_By_Tenant) / ((total_RCU * 5) + total_WCU)) tenant_dynamodb_cost = tenant_attribution_percentage * total_dynamodb_cost try: response = attribution_table.put_item( Item= { "Date": start_date_time, "ServiceName": "DynamoDB", "TenantId": tenant_id, "TotalRCU": total_RCU, "TenantTotalRCU": total_RCU_By_Tenant, "TotalWCU": total_WCU, "TenantTotalWCU": total_WCU_By_Tenant, "TenantAttributionPercentage": tenant_attribution_percentage, "TenantServiceCost": tenant_dynamodb_cost, "TotalServiceCost": total_dynamodb_cost } ) except ClientError as e: print(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: print("PutItem succeeded:") tenant_id = 'unknown' total_RCU_By_Tenant = 0.0 total_WCU_By_Tenant = 0.0 #Below function considers number of invocation as the metrics to calculate usage and cost. #You can go granluar by recording duration of each metrics and use that to get more granular #Since our functions are basic CRUD this might work as a ball park cost estimate def calculate_daily_lambda_attribution_by_tenant(event, context): #Get total dynamodb cost for the given duration time_zone = datetime.now().astimezone().tzinfo start_date_time = int(datetime.now(tz=time_zone).date().strftime('%s')) #current day epoch end_date_time = int((datetime.now(tz=time_zone) + timedelta(days=1)).date().strftime('%s')) #next day epoch #Get total dynamodb cost for the given duration total_lambda_cost = __get_total_service_cost('AWSLambda', start_date_time, end_date_time) log_group_names = __get_list_of_log_group_names() usage_by_tenant_by_day_query='fields @timestamp, @message \ | filter @message like /Request completed/ \ | fields tenant_id as TenantId , CountLambdaInvocations.0 As LambdaInvocations, timestamp\ | stats count (tenant_id) as CountLambdaInvocations by TenantId, dateceil(@timestamp, 1d) as timestamp' usage_by_tenant_by_day = __query_cloudwatch_logs(logs, log_group_names, usage_by_tenant_by_day_query, start_date_time, end_date_time) print(usage_by_tenant_by_day) total_usage_by_day_query = 'filter @message like /Request completed/ \ | fields CountLambdaInvocations.0 As LambdaInvocations, timestamp\ | stats count (tenant_id) as CountLambdaInvocations by dateceil(@timestamp, 1d) as timestamp' total_usage_by_day = __query_cloudwatch_logs(logs, log_group_names, total_usage_by_day_query, start_date_time, end_date_time) print(total_usage_by_day) total_invocations = 1 #to avoid divide by zero for result in total_usage_by_day['results'][0]: if 'LambdaInvocations' in result['field']: total_invocations = Decimal(result['value']) print (total_invocations) if (total_invocations>0): total_invocations_by_tenant = 0 for result in usage_by_tenant_by_day['results']: for field in result: if 'TenantId' in field['field']: tenant_id = field['value'] if 'LambdaInvocations' in field['field']: total_invocations_by_tenant = Decimal(field['value']) #RCU is about 5 times cheaper tenant_attribution_percentage= (total_invocations_by_tenant / total_invocations) tenant_lambda_cost = tenant_attribution_percentage * total_lambda_cost try: response = attribution_table.put_item( Item= { "Date": start_date_time, "ServiceName": "AWSLambda", "TenantId": tenant_id, "TotalInvocations": total_invocations, "TenantTotalInvocations": total_invocations_by_tenant, "TenantAttributionPercentage": tenant_attribution_percentage, "TenantServiceCost": tenant_lambda_cost, "TotalServiceCost": total_lambda_cost } ) except ClientError as e: print(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: print("PutItem succeeded:") tenant_id = 'unknown' tenant_total_RCU = 0.0 tenant_total_WCU = 0.0 def __get_total_service_cost(servicename, start_date_time, end_date_time): # We need to add more filters for day, month, year, resource ids etc. Below query is because we are just using a sample cur file #Ignoting startTime and endTime filter for now since we have a static/sample cur file query = "SELECT sum(line_item_blended_cost) AS cost FROM costexplorerdb.curoutput WHERE line_item_product_code='{0}'".format(servicename) # Execution response = athena.start_query_execution( QueryString=query, QueryExecutionContext={ 'Database': 'costexplorerdb' }, ResultConfiguration={ 'OutputLocation': "s3://" + ATHENA_S3_OUTPUT, } ) # get query execution id query_execution_id = response['QueryExecutionId'] print(query_execution_id) # get execution status for i in range(1, 1 + RETRY_COUNT): # get query execution query_status = athena.get_query_execution(QueryExecutionId=query_execution_id) print (query_status) query_execution_status = query_status['QueryExecution']['Status']['State'] if query_execution_status == 'SUCCEEDED': print("STATUS:" + query_execution_status) break if query_execution_status == 'FAILED': raise Exception("STATUS:" + query_execution_status) else: print("STATUS:" + query_execution_status) time.sleep(i) else: athena.stop_query_execution(QueryExecutionId=query_execution_id) raise Exception('TIME OVER') # get query results result = athena.get_query_results(QueryExecutionId=query_execution_id) print (result) total_dynamo_db_cost = result['ResultSet']['Rows'][1]['Data'][0]['VarCharValue'] print(total_dynamo_db_cost) return Decimal(total_dynamo_db_cost) def __query_cloudwatch_logs(logs, log_group_names, query_string, start_time, end_time): query = logs.start_query(logGroupNames=log_group_names, startTime=start_time, endTime=end_time, queryString=query_string) query_results = logs.get_query_results(queryId=query["queryId"]) while query_results['status']=='Running' or query_results['status']=='Scheduled': time.sleep(5) query_results = logs.get_query_results(queryId=query["queryId"]) return query_results def __is_log_group_exists(logs_client, log_group_name): logs_paginator = logs_client.get_paginator('describe_log_groups') response_iterator = logs_paginator.paginate(logGroupNamePrefix=log_group_name) for log_groups_list in response_iterator: if not log_groups_list["logGroups"]: return False else: return True def __add_log_group_name(logs_client, log_group_name, log_group_names_list): if __is_log_group_exists(logs_client, log_group_name): log_group_names_list.append(log_group_name) def __get_list_of_log_group_names(): log_group_names = [] log_group_prefix = '/aws/lambda/' cloudformation_paginator = cloudformation.get_paginator('list_stack_resources') response_iterator = cloudformation_paginator.paginate(StackName='stack-pooled') for stack_resources in response_iterator: for resource in stack_resources['StackResourceSummaries']: if (resource["LogicalResourceId"] == "CreateProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "UpdateProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "GetProductsFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "DeleteProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "CreateOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "UpdateOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "GetOrdersFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "DeleteOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue return log_group_names ================================================ FILE: Solution/Lab7/.aws-sam/build/template.yaml ================================================ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: 'Serverless SaaS - Cost by tenant ' Globals: Function: Timeout: 29 Resources: CURBucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true AWSCURDatabase: Type: AWS::Glue::Database Properties: DatabaseInput: Name: Fn::Sub: costexplorerdb CatalogId: Ref: AWS::AccountId AWSCURCrawlerComponentFunction: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - glue.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSGlueServiceRole Policies: - PolicyName: AWSCURCrawlerComponentFunction PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: Fn::Sub: arn:${AWS::Partition}:logs:*:*:* - Effect: Allow Action: - glue:UpdateDatabase - glue:UpdatePartition - glue:CreateTable - glue:UpdateTable - glue:ImportCatalogToGlue Resource: '*' - Effect: Allow Action: - s3:GetObject - s3:PutObject Resource: Fn::Sub: ${CURBucket.Arn}* - PolicyName: AWSCURKMSDecryption PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - kms:Decrypt Resource: '*' AWSCURCrawlerLambdaExecutor: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: AWSCURCrawlerLambdaExecutor PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: Fn::Sub: arn:${AWS::Partition}:logs:*:*:* - Effect: Allow Action: - glue:StartCrawler Resource: '*' AWSCURCrawler: Type: AWS::Glue::Crawler DependsOn: - AWSCURDatabase - AWSCURCrawlerComponentFunction Properties: Name: AWSCURCrawler-Multi-tenant Description: A recurring crawler that keeps your CUR table in Athena up-to-date. Role: Fn::GetAtt: - AWSCURCrawlerComponentFunction - Arn DatabaseName: Ref: AWSCURDatabase Targets: S3Targets: - Path: Fn::Sub: s3://${CURBucket}/curoutput Exclusions: - '**.json' - '**.yml' - '**.sql' - '**.csv' - '**.gz' - '**.zip' SchemaChangePolicy: UpdateBehavior: UPDATE_IN_DATABASE DeleteBehavior: DELETE_FROM_DATABASE AWSCURInitializer: Type: AWS::Lambda::Function DependsOn: AWSCURCrawler Properties: Code: ZipFile: "const AWS = require('aws-sdk'); const response = require('./cfn-response');\ \ exports.handler = function(event, context, callback) {\n if (event.RequestType\ \ === 'Delete') {\n response.send(event, context, response.SUCCESS);\n\ \ } else {\n const glue = new AWS.Glue();\n glue.startCrawler({ Name:\ \ 'AWSCURCrawler-Multi-tenant' }, function(err, data) {\n if (err)\ \ {\n const responseData = JSON.parse(this.httpResponse.body);\n\ \ if (responseData['__type'] == 'CrawlerRunningException') {\n \ \ callback(null, responseData.Message);\n } else {\n \ \ const responseString = JSON.stringify(responseData);\n if\ \ (event.ResponseURL) {\n response.send(event, context, response.FAILED,{\ \ msg: responseString });\n } else {\n callback(responseString);\n\ \ }\n }\n }\n else {\n if (event.ResponseURL)\ \ {\n response.send(event, context, response.SUCCESS);\n \ \ } else {\n callback(null, response.SUCCESS);\n }\n \ \ }\n });\n }\n};\n" Handler: index.handler Timeout: 30 Runtime: nodejs16.x ReservedConcurrentExecutions: 1 Role: Fn::GetAtt: - AWSCURCrawlerLambdaExecutor - Arn TenantCostandUsageAttributionTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: Date AttributeType: N - AttributeName: ServiceName AttributeType: S KeySchema: - AttributeName: Date KeyType: HASH - AttributeName: ServiceName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: TenantCostAndUsageAttribution QueryLogInsightsExecutionRole: Type: AWS::IAM::Role Properties: RoleName: product-function-execution-role-lab1 Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: query-log-insight-lab7 PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:GetQueryResults - logs:StartQuery - logs:StopQuery - logs:FilterLogEvents - logs:DescribeLogGroups - cloudformation:ListStackResources Resource: - '*' - Effect: Allow Action: - s3:* Resource: - Fn::Sub: arn:aws:s3:::${CURBucket}* - Effect: Allow Action: - dynamodb:* Resource: - Fn::GetAtt: - TenantCostandUsageAttributionTable - Arn - Effect: Allow Action: - Athena:* Resource: - '*' - Effect: Allow Action: - glue:* Resource: - '*' GetDynamoDBUsageAndCostByTenant: Type: AWS::Serverless::Function DependsOn: QueryLogInsightsExecutionRole Properties: CodeUri: GetDynamoDBUsageAndCostByTenant Handler: tenant_usage_and_cost.calculate_daily_dynamodb_attribution_by_tenant Runtime: python3.9 Role: Fn::GetAtt: - QueryLogInsightsExecutionRole - Arn Environment: Variables: ATHENA_S3_OUTPUT: Ref: CURBucket Events: ScheduledEvent: Type: Schedule Properties: Name: CalculateDynamoUsageAndCostByTenant Schedule: rate(5 minutes) GetLambdaUsageAndCostByTenant: Type: AWS::Serverless::Function DependsOn: QueryLogInsightsExecutionRole Properties: CodeUri: GetLambdaUsageAndCostByTenant Handler: tenant_usage_and_cost.calculate_daily_lambda_attribution_by_tenant Runtime: python3.9 Role: Fn::GetAtt: - QueryLogInsightsExecutionRole - Arn Environment: Variables: ATHENA_S3_OUTPUT: Ref: CURBucket Events: ScheduledEvent: Type: Schedule Properties: Name: CalculateLambdaUsageAndCostByTenant Schedule: rate(5 minutes) Outputs: CURBucketname: Description: The name of S3 bucket name Value: Ref: CURBucket Export: Name: CURBucketname AWSCURInitializerFunctionName: Description: Function name of CUR initializer Value: Ref: AWSCURInitializer Export: Name: AWSCURInitializerFunctionName ================================================ FILE: Solution/Lab7/.aws-sam/build.toml ================================================ # This file is auto generated by SAM CLI build command [function_build_definitions] [function_build_definitions.8d6cee39-9fc9-4073-9b65-22bf29298af4] codeuri = "/Users/shaanubh/Documents/code/serverless-saas-workshop/code/aws-serverless-saas-workshop/Lab7/TenantUsageAndCost" runtime = "python3.8" architecture = "x86_64" manifest_hash = "" packagetype = "Zip" functions = ["GetDynamoDBUsageAndCostByTenant", "GetLambdaUsageAndCostByTenant"] [layer_build_definitions] ================================================ FILE: Solution/Lab7/TenantUsageAndCost/requirements.txt ================================================ ================================================ FILE: Solution/Lab7/TenantUsageAndCost/tenant_usage_and_cost.py ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 import boto3 import time import os from datetime import datetime, timedelta from botocore.exceptions import ClientError from decimal import * cloudformation = boto3.client('cloudformation') logs = boto3.client('logs') athena = boto3.client('athena') dynamodb = boto3.resource('dynamodb') attribution_table = dynamodb.Table("TenantCostAndUsageAttribution") ATHENA_S3_OUTPUT = os.getenv("ATHENA_S3_OUTPUT") RETRY_COUNT = 100 #This function needs to be scheduled on daily basis def calculate_daily_dynamodb_attribution_by_tenant(event, context): start_date_time = __get_start_date_time() #current day epoch end_date_time = __get_end_date_time() #next day epoch #Get total dynamodb cost for the given duration total_dynamodb_cost = __get_total_service_cost('AmazonDynamoDB', start_date_time, end_date_time) log_group_names = __get_list_of_log_group_names() print( log_group_names) usage_by_tenant_by_day_query = 'filter @message like /ReadCapacityUnits|WriteCapacityUnits/ \ | fields tenant_id as TenantId, ReadCapacityUnits.0 as RCapacityUnits, WriteCapacityUnits.0 as WCapacityUnits \ | stats sum(RCapacityUnits) as ReadCapacityUnits, sum(WCapacityUnits) as WriteCapacityUnits by TenantId, dateceil(@timestamp, 1d) as timestamp' usage_by_tenant_by_day = __query_cloudwatch_logs(logs, log_group_names, usage_by_tenant_by_day_query, start_date_time, end_date_time) print(usage_by_tenant_by_day) total_usage_by_day_query = 'filter @message like /ReadCapacityUnits|WriteCapacityUnits/ \ | fields ReadCapacityUnits.0 as RCapacityUnits, WriteCapacityUnits.0 as WCapacityUnits \ | stats sum(RCapacityUnits) as ReadCapacityUnits, sum(WCapacityUnits) as WriteCapacityUnits by dateceil(@timestamp, 1d) as timestamp' total_usage_by_day = __query_cloudwatch_logs(logs, log_group_names, total_usage_by_day_query, start_date_time, end_date_time) print(total_usage_by_day) total_RCU = 0 total_WCU = 0 for result in total_usage_by_day['results'][0]: if 'ReadCapacityUnits' in result['field']: total_RCU = Decimal(result['value']) if 'WriteCapacityUnits' in result['field']: total_WCU = Decimal(result['value']) print (total_RCU) print (total_WCU) if (total_RCU + total_WCU > 0): total_RCU_By_Tenant = 0 total_WCU_By_Tenant = 0 for result in usage_by_tenant_by_day['results']: for field in result: if 'TenantId' in field['field']: tenant_id = field['value'] if 'ReadCapacityUnits' in field['field']: total_RCU_By_Tenant = Decimal(field['value']) if 'WriteCapacityUnits' in field['field']: total_WCU_By_Tenant = Decimal(field['value']) #RCU is about 5 times cheaper tenant_attribution_percentage= (((total_RCU_By_Tenant * 5) + total_WCU_By_Tenant) / ((total_RCU * 5) + total_WCU)) tenant_dynamodb_cost = tenant_attribution_percentage * total_dynamodb_cost try: response = attribution_table.put_item( Item= { "Date": start_date_time, "TenantId#ServiceName": tenant_id+"#"+"DynamoDB", "TenantId": tenant_id, "TotalRCU": total_RCU, "TenantTotalRCU": total_RCU_By_Tenant, "TotalWCU": total_WCU, "TenantTotalWCU": total_WCU_By_Tenant, "TenantAttributionPercentage": tenant_attribution_percentage, "TenantServiceCost": tenant_dynamodb_cost, "TotalServiceCost": total_dynamodb_cost } ) except ClientError as e: print(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: print("PutItem succeeded:") tenant_id = 'unknown' total_RCU_By_Tenant = 0.0 total_WCU_By_Tenant = 0.0 #Below function considers number of invocation as the metrics to calculate usage and cost. #You can go granluar by recording duration of each metrics and use that to get more granular #Since our functions are basic CRUD this might work as a ball park cost estimate def calculate_daily_lambda_attribution_by_tenant(event, context): #Get total dynamodb cost for the given duration start_date_time = __get_start_date_time() #current day epoch end_date_time = __get_end_date_time() #next day epoch #Get total dynamodb cost for the given duration total_lambda_cost = __get_total_service_cost('AWSLambda', start_date_time, end_date_time) log_group_names = __get_list_of_log_group_names() usage_by_tenant_by_day_query='fields @timestamp, @message \ | filter @message like /Request completed/ \ | fields tenant_id as TenantId , CountLambdaInvocations.0 As LambdaInvocations, timestamp\ | stats count (tenant_id) as CountLambdaInvocations by TenantId, dateceil(@timestamp, 1d) as timestamp' usage_by_tenant_by_day = __query_cloudwatch_logs(logs, log_group_names, usage_by_tenant_by_day_query, start_date_time, end_date_time) print(usage_by_tenant_by_day) total_usage_by_day_query = 'filter @message like /Request completed/ \ | fields CountLambdaInvocations.0 As LambdaInvocations, timestamp\ | stats count (tenant_id) as CountLambdaInvocations by dateceil(@timestamp, 1d) as timestamp' total_usage_by_day = __query_cloudwatch_logs(logs, log_group_names, total_usage_by_day_query, start_date_time, end_date_time) print(total_usage_by_day) total_invocations = 1 #to avoid divide by zero for result in total_usage_by_day['results'][0]: if 'LambdaInvocations' in result['field']: total_invocations = Decimal(result['value']) print (total_invocations) if (total_invocations>0): total_invocations_by_tenant = 0 for result in usage_by_tenant_by_day['results']: for field in result: if 'TenantId' in field['field']: tenant_id = field['value'] if 'LambdaInvocations' in field['field']: total_invocations_by_tenant = Decimal(field['value']) #RCU is about 5 times cheaper tenant_attribution_percentage= (total_invocations_by_tenant / total_invocations) tenant_lambda_cost = tenant_attribution_percentage * total_lambda_cost try: response = attribution_table.put_item( Item= { "Date": start_date_time, "TenantId#ServiceName": tenant_id+"#"+"AWSLambda", "TenantId": tenant_id, "TotalInvocations": total_invocations, "TenantTotalInvocations": total_invocations_by_tenant, "TenantAttributionPercentage": tenant_attribution_percentage, "TenantServiceCost": tenant_lambda_cost, "TotalServiceCost": total_lambda_cost } ) except ClientError as e: print(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: print("PutItem succeeded:") tenant_id = 'unknown' tenant_total_RCU = 0.0 tenant_total_WCU = 0.0 def __get_total_service_cost(servicename, start_date_time, end_date_time): # We need to add more filters for day, month, year, resource ids etc. Below query is because we are just using a sample cur file #Ignoting startTime and endTime filter for now since we have a static/sample cur file query = "SELECT sum(line_item_blended_cost) AS cost FROM costexplorerdb.curoutput WHERE line_item_product_code='{0}'".format(servicename) # Execution response = athena.start_query_execution( QueryString=query, QueryExecutionContext={ 'Database': 'costexplorerdb' }, ResultConfiguration={ 'OutputLocation': "s3://" + ATHENA_S3_OUTPUT, } ) # get query execution id query_execution_id = response['QueryExecutionId'] print(query_execution_id) # get execution status for i in range(1, 1 + RETRY_COUNT): # get query execution query_status = athena.get_query_execution(QueryExecutionId=query_execution_id) print (query_status) query_execution_status = query_status['QueryExecution']['Status']['State'] if query_execution_status == 'SUCCEEDED': print("STATUS:" + query_execution_status) break if query_execution_status == 'FAILED': raise Exception("STATUS:" + query_execution_status) else: print("STATUS:" + query_execution_status) time.sleep(i) else: athena.stop_query_execution(QueryExecutionId=query_execution_id) raise Exception('TIME OVER') # get query results result = athena.get_query_results(QueryExecutionId=query_execution_id) print (result) total_dynamo_db_cost = result['ResultSet']['Rows'][1]['Data'][0]['VarCharValue'] print(total_dynamo_db_cost) return Decimal(total_dynamo_db_cost) def __query_cloudwatch_logs(logs, log_group_names, query_string, start_time, end_time): query = logs.start_query(logGroupNames=log_group_names, startTime=start_time, endTime=end_time, queryString=query_string) query_results = logs.get_query_results(queryId=query["queryId"]) while query_results['status']=='Running' or query_results['status']=='Scheduled': time.sleep(5) query_results = logs.get_query_results(queryId=query["queryId"]) return query_results def __is_log_group_exists(logs_client, log_group_name): logs_paginator = logs_client.get_paginator('describe_log_groups') response_iterator = logs_paginator.paginate(logGroupNamePrefix=log_group_name) for log_groups_list in response_iterator: if not log_groups_list["logGroups"]: return False else: return True def __add_log_group_name(logs_client, log_group_name, log_group_names_list): if __is_log_group_exists(logs_client, log_group_name): log_group_names_list.append(log_group_name) def __get_list_of_log_group_names(): log_group_names = [] log_group_prefix = '/aws/lambda/' cloudformation_paginator = cloudformation.get_paginator('list_stack_resources') response_iterator = cloudformation_paginator.paginate(StackName='stack-pooled') for stack_resources in response_iterator: for resource in stack_resources['StackResourceSummaries']: if (resource["LogicalResourceId"] == "CreateProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "UpdateProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "GetProductsFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "DeleteProductFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "CreateOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "UpdateOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "GetOrdersFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue if (resource["LogicalResourceId"] == "DeleteOrderFunction"): __add_log_group_name(logs, ''.join([log_group_prefix,resource["PhysicalResourceId"]]), log_group_names) continue return log_group_names def __get_start_date_time(): time_zone = datetime.now().astimezone().tzinfo start_date_time = int(datetime.now(tz=time_zone).date().strftime('%s')) #current day epoch return start_date_time def __get_end_date_time(): time_zone = datetime.now().astimezone().tzinfo end_date_time = int((datetime.now(tz=time_zone) + timedelta(days=1)).date().strftime('%s')) #next day epoch return end_date_time ================================================ FILE: Solution/Lab7/deployment.sh ================================================ REGION=$(aws configure get region) sam build -t template.yaml --use-container sam deploy --config-file samconfig.toml --region=$REGION CUR_BUCKET=$(aws cloudformation list-exports --query "Exports[?Name=='CURBucketname'].Value" --output text) AWSCURInitializerFunctionName=$(aws cloudformation list-exports --query "Exports[?Name=='AWSCURInitializerFunctionName'].Value" --output text) aws s3 cp SampleCUR/ s3://$CUR_BUCKET/curoutput/year=2022/month=10/ --recursive aws lambda invoke --function-name $AWSCURInitializerFunctionName lambdaoutput.json ================================================ FILE: Solution/Lab7/lambdaoutput.json ================================================ "Crawler with name AWSCURCrawler-Multi-tenant has already started" ================================================ FILE: Solution/Lab7/samconfig.toml ================================================ version = 0.1 [default] [default.deploy] [default.deploy.parameters] stack_name = "serverless-saas-cost-per-tenant-lab7" s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-1p6m7gm2vwaaz" s3_prefix = "serverless-saas-lab7" region = "us-west-2" confirm_changeset = false capabilities = "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND" cached="true" parallel="true" ================================================ FILE: Solution/Lab7/template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > Serverless SaaS - Cost by tenant Globals: Function: Timeout: 29 Resources: CURBucket: Type: AWS::S3::Bucket DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AWSCURDatabase: Type: 'AWS::Glue::Database' Properties: DatabaseInput: Name: !Sub 'costexplorerdb' CatalogId: !Ref AWS::AccountId AWSCURCrawlerComponentFunction: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - glue.amazonaws.com Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSGlueServiceRole' Policies: - PolicyName: AWSCURCrawlerComponentFunction PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub 'arn:${AWS::Partition}:logs:*:*:*' - Effect: Allow Action: - 'glue:UpdateDatabase' - 'glue:UpdatePartition' - 'glue:CreateTable' - 'glue:UpdateTable' - 'glue:ImportCatalogToGlue' Resource: '*' - Effect: Allow Action: - 's3:GetObject' - 's3:PutObject' Resource: !Sub '${CURBucket.Arn}*' - PolicyName: AWSCURKMSDecryption PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'kms:Decrypt' Resource: '*' AWSCURCrawlerLambdaExecutor: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / Policies: - PolicyName: AWSCURCrawlerLambdaExecutor PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub 'arn:${AWS::Partition}:logs:*:*:*' - Effect: Allow Action: - 'glue:StartCrawler' Resource: '*' AWSCURCrawler: Type: 'AWS::Glue::Crawler' DependsOn: - AWSCURDatabase - AWSCURCrawlerComponentFunction Properties: Name: AWSCURCrawler-Multi-tenant Description: A recurring crawler that keeps your CUR table in Athena up-to-date. Role: !GetAtt AWSCURCrawlerComponentFunction.Arn DatabaseName: !Ref AWSCURDatabase Targets: S3Targets: - Path: !Sub 's3://${CURBucket}/curoutput' Exclusions: - '**.json' - '**.yml' - '**.sql' - '**.csv' - '**.gz' - '**.zip' SchemaChangePolicy: UpdateBehavior: UPDATE_IN_DATABASE DeleteBehavior: DELETE_FROM_DATABASE AWSCURInitializer: Type: 'AWS::Lambda::Function' DependsOn: AWSCURCrawler Properties: Code: ZipFile: > const AWS = require('aws-sdk'); const response = require('./cfn-response'); exports.handler = function(event, context, callback) { if (event.RequestType === 'Delete') { response.send(event, context, response.SUCCESS); } else { const glue = new AWS.Glue(); glue.startCrawler({ Name: 'AWSCURCrawler-Multi-tenant' }, function(err, data) { if (err) { const responseData = JSON.parse(this.httpResponse.body); if (responseData['__type'] == 'CrawlerRunningException') { callback(null, responseData.Message); } else { const responseString = JSON.stringify(responseData); if (event.ResponseURL) { response.send(event, context, response.FAILED,{ msg: responseString }); } else { callback(responseString); } } } else { if (event.ResponseURL) { response.send(event, context, response.SUCCESS); } else { callback(null, response.SUCCESS); } } }); } }; Handler: 'index.handler' Timeout: 30 Runtime: nodejs16.x ReservedConcurrentExecutions: 1 Role: !GetAtt AWSCURCrawlerLambdaExecutor.Arn TenantCostandUsageAttributionTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: Date AttributeType: N - AttributeName: TenantId#ServiceName AttributeType: S KeySchema: - AttributeName: Date KeyType: HASH - AttributeName: TenantId#ServiceName KeyType: RANGE BillingMode: PAY_PER_REQUEST TableName: TenantCostAndUsageAttribution QueryLogInsightsExecutionRole: Type: AWS::IAM::Role Properties: RoleName: product-function-execution-role-lab1 Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: query-log-insight-lab7 PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:GetQueryResults - logs:StartQuery - logs:StopQuery - logs:FilterLogEvents - logs:DescribeLogGroups - cloudformation:ListStackResources Resource: - "*" - Effect: Allow Action: - s3:* Resource: - !Sub 'arn:aws:s3:::${CURBucket}*' - Effect: Allow Action: - dynamodb:* Resource: - !GetAtt TenantCostandUsageAttributionTable.Arn - Effect: Allow Action: - Athena:* Resource: - "*" - Effect: Allow Action: - glue:* Resource: - "*" GetDynamoDBUsageAndCostByTenant: Type: AWS::Serverless::Function DependsOn: QueryLogInsightsExecutionRole Properties: CodeUri: TenantUsageAndCost/ Handler: tenant_usage_and_cost.calculate_daily_dynamodb_attribution_by_tenant Runtime: python3.9 Role: !GetAtt QueryLogInsightsExecutionRole.Arn Environment: Variables: ATHENA_S3_OUTPUT: !Ref CURBucket Events: ScheduledEvent: Type: Schedule Properties: Name: CalculateDynamoUsageAndCostByTenant Schedule: rate(5 minutes) GetLambdaUsageAndCostByTenant: Type: AWS::Serverless::Function DependsOn: QueryLogInsightsExecutionRole Properties: CodeUri: TenantUsageAndCost/ Handler: tenant_usage_and_cost.calculate_daily_lambda_attribution_by_tenant Runtime: python3.9 Role: !GetAtt QueryLogInsightsExecutionRole.Arn Environment: Variables: ATHENA_S3_OUTPUT: !Ref CURBucket Events: ScheduledEvent: Type: Schedule Properties: Name: CalculateLambdaUsageAndCostByTenant Schedule: rate(5 minutes) Outputs: CURBucketname: Description: The name of S3 bucket name Value: !Ref CURBucket Export: Name: "CURBucketname" AWSCURInitializerFunctionName: Description: Function name of CUR initializer Value: !Ref AWSCURInitializer Export: Name: "AWSCURInitializerFunctionName" ================================================ FILE: THIRD-PARTY-LICENSES.txt ================================================ ** CrHelper; version 2.0.6 -- https://github.com/aws-cloudformation/custom-resource-helper Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. * For CrHelper see also this required NOTICE: Custom Resource Helper Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. This library is licensed under the Apache 2.0 License. Decorator implementation inspired by https://github.com/ryansb/cfn-wrapper-python Log implementation inspired by https://gitlab.com/hadrien/aws_lambda_logging ------ ** jsonpickle; version 1.4.1 -- https://github.com/jsonpickle/jsonpickle Copyright (C) 2008 John Paulett (john -at- paulett.org) Copyright (C) 2009-2018 David Aguilar (davvid -at- gmail.com) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2008 John Paulett (john -at- paulett.org) Copyright (C) 2009-2018 David Aguilar (davvid -at- gmail.com) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------ ** aws-requests-auth; version 0.4.3 -- https://github.com/davidmuller/aws-requests-auth Copyright (c) David Muller. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (c) David Muller. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------ ** python-jose; version 3.2.0 -- https://github.com/mpdavis/python-jose Copyright (c) 2015 Michael Davis ** python-simplejson; version 3.17.2 -- https://github.com/simplejson/simplejson Copyright (c) 2006 Bob Ippolito Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: event-engine-assets/initialize-module-sam-template.yaml ================================================ AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Create Cloud9 IDE, create state machine to intall pre-req on cloud9, API Gateway cloudwatch Role Parameters: # Optional parameters passed by the Event Engine to the stack. # EEEventId: # Description: "Unique ID of this Event" # Type: String # EETeamId: # Description: "Unique ID of this Team" # Type: String # EEModuleId: # Description: "Unique ID of this module" # Type: String # EEModuleVersion: # Description: "Version of this module" # Type: String EEAssetsBucket: Description: "Region-specific assets S3 bucket name (e.g. ee-assets-prod-us-east-1)" Type: String Default: "aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx" EEAssetsKeyPrefix: Description: "S3 key prefix where this modules assets are stored. (e.g. modules/my_module/v1/)" Type: String Default: "serverless-saas/" # EEMasterAccountId: # Description: "AWS Account Id of the Master account" # Type: String # EETeamRoleArn: # Description: "ARN of the Team Role" # Type: String # EEKeyPair: # Description: "Name of the EC2 KeyPair generated for the Team" # Type: AWS::EC2::KeyPair::KeyName # Your own parameters for the stack. NOTE: All these parameters need to have a default value. EBSVolumeSize: Description: "Size of EBS Volume (in GB)" Type: Number Default: 50 UserDataScript: Description: "File name for user-data script" Type: String Default: "pre-requisites-event-engine.sh" EC2InstanceType: Default: t3.large Description: EC2 instance type on which IDE runs Type: String AutoHibernateTimeout: Default: 120 Description: How many minutes idle before shutting down the IDE Type: Number OwnerRoleName: Default: "ServerlessSaaS/shaanubh-Isengard" Description: Use this if you are accessing your AWS Account using a cross account role (as is the case with event engine) Type: String Conditions: AddUserData: !Not [!Equals [ !Ref UserDataScript, "NONE" ]] UseRole: !Not [!Equals [ !Ref OwnerRoleName, ""]] Resources: EC2SSMExecutionRole: Type: AWS::IAM::Role Properties: RoleName: ec2-ssm-role Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore Policies: - PolicyName: ec2-ssm-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "ec2:ModifyVolume" - "ec2:DescribeInstances" - "ec2:DescribeVolumesModifications" - "logs:*" Resource: "*" WaitForEC2ToInitialize: Type: AWS::Serverless::Function Properties: InlineCode: | import boto3 import time ec2_client = boto3.client('ec2') def lambda_handler(event, context): instance_id = event['resources'][0].split("/")[1] instance_statuses = ec2_client.describe_instance_status(InstanceIds=[instance_id]) try: state = instance_statuses['InstanceStatuses'][0]['InstanceState']['Name'] instance_status = instance_statuses['InstanceStatuses'][0]['InstanceStatus']['Details'][0]['Status'] system_status = instance_statuses['InstanceStatuses'][0]['SystemStatus']['Details'][0]['Status'] except Exception as _: state = 'UNKNOWN' while (instance_status != 'passed' or system_status != 'passed' or state != 'running'): print("waiting for system to be ready") time.sleep(30) instance_statuses = ec2_client.describe_instance_status(InstanceIds=[instance_id]) print (instance_statuses) try: state = instance_statuses['InstanceStatuses'][0]['InstanceState']['Name'] instance_status = instance_statuses['InstanceStatuses'][0]['InstanceStatus']['Details'][0]['Status'] system_status = instance_statuses['InstanceStatuses'][0]['SystemStatus']['Details'][0]['Status'] except Exception as e: print(e) state = 'UNKNOWN' environment_info = { "instance_id": instance_id } return environment_info Handler: index.lambda_handler Runtime: python3.9 Timeout: 900 Policies: - Statement: - Sid: EC2 Effect: Allow Action: - "ec2:DescribeInstanceStatus" Resource: "*" AttachSSMRoleToEC2: Type: AWS::Serverless::Function Properties: InlineCode: | import boto3 import json import os from time import sleep iam_client= boto3.client('iam') ec2_client = boto3.client('ec2') def lambda_handler(event, context): print(event) instance_id = event["instance_id"] instance_profile_name = 'cloud9-' + instance_id response = ec2_client.describe_iam_instance_profile_associations(Filters=[{'Name': 'instance-id','Values': [instance_id]},{'Name': 'state','Values': ['associated']}]) if (len(response['IamInstanceProfileAssociations']) < 1): try: create_instance_profile_response = iam_client.create_instance_profile(InstanceProfileName=instance_profile_name) print(create_instance_profile_response) sleep(30) except iam_client.exceptions.EntityAlreadyExistsException as _: pass try: response = iam_client.add_role_to_instance_profile( InstanceProfileName=instance_profile_name, RoleName='ec2-ssm-role' ) print(response) response = ec2_client.associate_iam_instance_profile( IamInstanceProfile={'Name': instance_profile_name}, InstanceId=instance_id ) print(response) except iam_client.exceptions.LimitExceededException as e: print(e) response = ec2_client.describe_iam_instance_profile_associations(Filters=[{'Name': 'instance-id','Values': [instance_id]},{'Name': 'state','Values': ['associated']}]) print(response) while len(response['IamInstanceProfileAssociations']) < 1: print("waiting for association to finish") sleep(30) response = ec2_client.describe_iam_instance_profile_associations(Filters=[{'Name': 'instance-id','Values': [instance_id]},{'Name': 'state','Values': ['associated']}]) environment_info = { "instance_id": instance_id, "instance_profile_name": instance_profile_name } return environment_info Handler: index.lambda_handler Runtime: python3.9 Timeout: 900 Policies: - Statement: - Sid: ResizePolicy Effect: Allow Action: - "ec2:DescribeIamInstanceProfileAssociations" - "ec2:AssociateIamInstanceProfile" - "iam:CreateInstanceProfile" - "iam:AddRoleToInstanceProfile" - "iam:PassRole" Resource: "*" SendSSMCommandtoEC2: Type: AWS::Serverless::Function Properties: InlineCode: | import boto3 import json import os from io import BytesIO s3_resource = boto3.resource('s3') ssm_client = boto3.client('ssm') import time def get_preamble(): return f""" REGION=$(curl http://169.254.169.254/latest/meta-data/placement/region) aws configure set profile.default.region $REGION SIZE={int(os.environ.get('VolumeSize', '25'))} # Get the ID of the environment host Amazon EC2 instance. INSTANCEID=$(curl http://169.254.169.254/latest/meta-data//instance-id) # Get the ID of the Amazon EBS volume associated with the instance. VOLUMEID=$(aws ec2 describe-instances \ --instance-id $INSTANCEID \ --query "Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId" \ --output text) # Resize the EBS volume. aws ec2 modify-volume --volume-id $VOLUMEID --size $SIZE # Wait for the resize to finish. while [ \ "$(aws ec2 describe-volumes-modifications \ --volume-id $VOLUMEID \ --filters Name=modification-state,Values="optimizing","completed" \ --query "length(VolumesModifications)"\ --output text)" != "1" ]; do sleep 1 done #Check if we're on an NVMe filesystem if [[ -e "/dev/xvda" && $(readlink -f /dev/xvda) = "/dev/xvda" ]] then # Rewrite the partition table so that the partition takes up all the space that it can. sudo growpart /dev/xvda 1 # Expand the size of the file system. # Check if we're on AL2 STR=$(cat /etc/os-release) SUB='VERSION_ID="2"' if [[ "$STR" == *"$SUB"* ]] then sudo xfs_growfs -d / else sudo resize2fs /dev/xvda1 fi else # Rewrite the partition table so that the partition takes up all the space that it can. sudo growpart /dev/nvme0n1 1 # Expand the size of the file system. # Check if we're on AL2 STR=$(cat /etc/os-release) SUB='VERSION_ID="2"' if [[ "$STR" == *"$SUB"* ]] then sudo xfs_growfs -d / else sudo resize2fs /dev/nvme0n1p1 fi fi """ def ssm_ready(ssm_client, instance_id): try: response = ssm_client.describe_instance_information(Filters=[{'Key': 'InstanceIds', 'Values': [instance_id]}]) return len(response['InstanceInformationList'])>=1 except ssm_client.exceptions.InvalidInstanceId: return False def lambda_handler(event, context): print(event) instance_id = event["instance_id"] while not ssm_ready(ssm_client, instance_id): print("SSM not ready yet") time.sleep(30) try: print(os.environ["S3Bucket"]) print(os.environ["S3Object"]) bucket = s3_resource.Bucket(os.environ["S3Bucket"]) obj = bucket.Object(os.environ["S3Object"]) output = BytesIO() obj.download_fileobj(output) commands = get_preamble() + '\n' + output.getvalue().decode('utf-8') + '\n' except Exception as e: print(e) commands = get_preamble() print (instance_id) send_command_response = ssm_client.send_command( InstanceIds=[instance_id], DocumentName='AWS-RunShellScript', Parameters={'commands': commands.split('\n')}, CloudWatchOutputConfig={ 'CloudWatchLogGroupName': f'ssm-output-{instance_id}', 'CloudWatchOutputEnabled': True } ) environment_info = { "instance_id": instance_id, "command_id": send_command_response['Command']['CommandId'] } return environment_info Handler: index.lambda_handler Runtime: python3.9 Timeout: 900 Environment: Variables: VolumeSize: !Ref EBSVolumeSize S3Bucket: !If - AddUserData - !Ref EEAssetsBucket - !Ref "AWS::NoValue" S3Object: !If - AddUserData - !Sub "${EEAssetsKeyPrefix}${UserDataScript}" - !Ref "AWS::NoValue" Policies: - Statement: - Sid: ResizePolicy Effect: Allow Action: - "s3:*" - "ssm:*" - "cloudwatch:*" Resource: "*" WaitForSSMCommandToComplete: Type: AWS::Serverless::Function Properties: InlineCode: | import boto3 import time def lambda_handler(event, context): command_id = event["command_id"] instance_id = event["instance_id"] ssm_client = boto3.client('ssm') response = ssm_client.get_command_invocation(CommandId=command_id, InstanceId=instance_id) while (response['Status'] in ['Pending', 'InProgress', 'Delayed']): time.sleep(30) print("Command in Progress") response = ssm_client.get_command_invocation(CommandId=command_id, InstanceId=instance_id) Handler: index.lambda_handler Runtime: python3.9 Timeout: 900 Policies: - Statement: - Sid: ResizePolicy Effect: Allow Action: - "ssm:GetCommandInvocation" Resource: "*" BootStrapC9StateMachine: Type: AWS::Serverless::StateMachine Properties: Definition: StartAt: Environment Health Check States: Environment Health Check: Type: Task Resource: ${WaitForEC2ToInitializeArn} Retry: - ErrorEquals: - States.TaskFailed IntervalSeconds: 30 MaxAttempts: 2 BackoffRate: 1.5 Next: Attach SSM Role To EC2 Attach SSM Role To EC2: Type: Task Resource: ${AttachSSMRoleToEC2Arn} Retry: - ErrorEquals: - States.TaskFailed IntervalSeconds: 30 MaxAttempts: 2 BackoffRate: 1.5 Next: Send Command Send Command: Type: Task Resource: ${SendSSMCommandtoEC2Arn} Retry: - ErrorEquals: - States.TaskFailed IntervalSeconds: 30 MaxAttempts: 2 BackoffRate: 1.5 Next: Wait To Stabilize Wait To Stabilize: Type: Task Resource: ${WaitForSSMCommandToCompleteArn} Retry: - ErrorEquals: - States.TaskFailed IntervalSeconds: 30 MaxAttempts: 2 BackoffRate: 1.5 End: true DefinitionSubstitutions: WaitForEC2ToInitializeArn: !GetAtt WaitForEC2ToInitialize.Arn AttachSSMRoleToEC2Arn: !GetAtt AttachSSMRoleToEC2.Arn SendSSMCommandtoEC2Arn: !GetAtt SendSSMCommandtoEC2.Arn WaitForSSMCommandToCompleteArn: !GetAtt WaitForSSMCommandToComplete.Arn Policies: - LambdaInvokePolicy: FunctionName: !Ref WaitForEC2ToInitialize - LambdaInvokePolicy: FunctionName: !Ref AttachSSMRoleToEC2 - LambdaInvokePolicy: FunctionName: !Ref SendSSMCommandtoEC2 - LambdaInvokePolicy: FunctionName: !Ref WaitForSSMCommandToComplete EventBridgeRole: Type: AWS::IAM::Role Properties: Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - events.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: serverless-saas-eventbridge-stepfunction-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - states:StartExecution Resource: - !GetAtt BootStrapC9StateMachine.Arn NewCloud9Event: Type: AWS::Events::Rule Properties: EventPattern: source: - aws.ec2 detail-type: - EC2 Instance State-change Notification detail: state: - running Targets: - Arn : !GetAtt BootStrapC9StateMachine.Arn Id: "BootStrapC9StateMachineTarget" RoleArn: !GetAtt EventBridgeRole.Arn Cloud9IDE: Type: AWS::Cloud9::EnvironmentEC2 DependsOn: NewCloud9Event Properties: Repositories: - RepositoryUrl: https://github.com/aws-samples/aws-serverless-saas-workshop.git PathComponent: /aws-serverless-saas-workshop Description: Cloud9 IDE Tags: - Key: stack-name Value: ServerlessSaaS AutomaticStopTimeMinutes: Ref: AutoHibernateTimeout ImageId: amazonlinux-2-x86_64 InstanceType: Ref: EC2InstanceType Name: Serverless-SaaS OwnerArn: !If [UseRole, !Join [ "", [!Sub "arn:aws:sts::${AWS::AccountId}:assumed-role/", !Ref OwnerRoleName]] , !Ref "AWS::NoValue"] Outputs: BootStrapC9StateMachineArn: Description: "State machine ARN" Value: !Ref BootStrapC9StateMachine ================================================ FILE: event-engine-assets/lab1-module-sam-template.yaml ================================================ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: 'Lab1 - Basic Serverless Application' Parameters: EEAssetsBucket: Description: "Region-specific assets S3 bucket name (e.g. ee-assets-prod-us-east-1)" Type: String Default: "aws-sam-cli-managed-default-samclisourcebucket-8tf6bmi4rdcx" EEAssetsKeyPrefix: Description: "S3 key prefix where this modules assets are stored. (e.g. modules/my_module/v1/)" Type: String Default: "serverless-saas/" StageName: Type: String Default: prod Description: Stage Name for the api Globals: Function: Timeout: 29 Layers: - Fn::Sub: arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:14 Environment: Variables: LOG_LEVEL: DEBUG Resources: ApiGatewayCloudWatchPublishRole: Type: AWS::IAM::Role Properties: Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs AttachCloudWatchRoleToApiGateway: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt ApiGatewayCloudWatchPublishRole.Arn ServerlessSaaSLayers: Type: AWS::Serverless::LayerVersion Properties: LayerName: serverless-saas-workshoplab1 Description: Utilities for project ContentUri: Bucket: !Ref EEAssetsBucket Key: !Sub ${EEAssetsKeyPrefix}1b6d1796b5948e393749602691c77e44 CompatibleRuntimes: - python3.9 LicenseInfo: MIT RetentionPolicy: Retain Metadata: BuildMethod: python3.9 ProductTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: productId AttributeType: S KeySchema: - AttributeName: productId KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: Product-Lab1 OrderTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: orderId AttributeType: S KeySchema: - AttributeName: orderId KeyType: HASH BillingMode: PAY_PER_REQUEST TableName: Order-Lab1 ProductFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: product-function-execution-role-lab1 Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: product-function-policy-lab1 PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query - dynamodb:Scan Resource: - Fn::GetAtt: - ProductTable - Arn GetProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: Bucket: !Ref EEAssetsBucket Key: !Sub ${EEAssetsKeyPrefix}3f9284a8b1fb9547d59e56cc8560b94c Handler: product_service.get_product Runtime: python3.9 Tracing: Active Role: Fn::GetAtt: - ProductFunctionExecutionRole - Arn Layers: - Ref: ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: ProductService PRODUCT_TABLE_NAME: Ref: ProductTable GetProductsFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: Bucket: !Ref EEAssetsBucket Key: !Sub ${EEAssetsKeyPrefix}3f9284a8b1fb9547d59e56cc8560b94c Handler: product_service.get_products Runtime: python3.9 Tracing: Active Role: Fn::GetAtt: - ProductFunctionExecutionRole - Arn Layers: - Ref: ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: ProductService PRODUCT_TABLE_NAME: Ref: ProductTable CreateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: Bucket: !Ref EEAssetsBucket Key: !Sub ${EEAssetsKeyPrefix}3f9284a8b1fb9547d59e56cc8560b94c Handler: product_service.create_product Runtime: python3.9 Tracing: Active Role: Fn::GetAtt: - ProductFunctionExecutionRole - Arn Layers: - Ref: ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: ProductService PRODUCT_TABLE_NAME: Ref: ProductTable UpdateProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: Bucket: !Ref EEAssetsBucket Key: !Sub ${EEAssetsKeyPrefix}3f9284a8b1fb9547d59e56cc8560b94c Handler: product_service.update_product Runtime: python3.9 Tracing: Active Role: Fn::GetAtt: - ProductFunctionExecutionRole - Arn Layers: - Ref: ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: ProductService PRODUCT_TABLE_NAME: Ref: ProductTable DeleteProductFunction: Type: AWS::Serverless::Function DependsOn: ProductFunctionExecutionRole Properties: CodeUri: Bucket: !Ref EEAssetsBucket Key: !Sub ${EEAssetsKeyPrefix}3f9284a8b1fb9547d59e56cc8560b94c Handler: product_service.delete_product Runtime: python3.9 Tracing: Active Role: Fn::GetAtt: - ProductFunctionExecutionRole - Arn Layers: - Ref: ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: ProductService PRODUCT_TABLE_NAME: Ref: ProductTable OrderFunctionExecutionRole: Type: AWS::IAM::Role Properties: RoleName: order-function-execution-role-lab1 Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess Policies: - PolicyName: order-function-policy-lab1 PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:UpdateItem - dynamodb:PutItem - dynamodb:DeleteItem - dynamodb:Query - dynamodb:Scan Resource: - Fn::GetAtt: - OrderTable - Arn GetOrdersFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: Bucket: !Ref EEAssetsBucket Key: !Sub ${EEAssetsKeyPrefix}ac28465dbac64f71e4739d551119c420 Handler: order_service.get_orders Runtime: python3.9 Tracing: Active Role: Fn::GetAtt: - OrderFunctionExecutionRole - Arn Layers: - Ref: ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: OrderService ORDER_TABLE_NAME: Ref: OrderTable GetOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: Bucket: !Ref EEAssetsBucket Key: !Sub ${EEAssetsKeyPrefix}ac28465dbac64f71e4739d551119c420 Handler: order_service.get_order Runtime: python3.9 Tracing: Active Role: Fn::GetAtt: - OrderFunctionExecutionRole - Arn Layers: - Ref: ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: OrderService ORDER_TABLE_NAME: Ref: OrderTable CreateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: Bucket: !Ref EEAssetsBucket Key: !Sub ${EEAssetsKeyPrefix}ac28465dbac64f71e4739d551119c420 Handler: order_service.create_order Runtime: python3.9 Tracing: Active Role: Fn::GetAtt: - OrderFunctionExecutionRole - Arn Layers: - Ref: ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: OrderService ORDER_TABLE_NAME: Ref: OrderTable UpdateOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: Bucket: !Ref EEAssetsBucket Key: !Sub ${EEAssetsKeyPrefix}ac28465dbac64f71e4739d551119c420 Handler: order_service.update_order Runtime: python3.9 Tracing: Active Role: Fn::GetAtt: - OrderFunctionExecutionRole - Arn Layers: - Ref: ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: OrderService ORDER_TABLE_NAME: Ref: OrderTable DeleteOrderFunction: Type: AWS::Serverless::Function DependsOn: OrderFunctionExecutionRole Properties: CodeUri: Bucket: !Ref EEAssetsBucket Key: !Sub ${EEAssetsKeyPrefix}ac28465dbac64f71e4739d551119c420 Handler: order_service.delete_order Runtime: python3.9 Tracing: Active Role: Fn::GetAtt: - OrderFunctionExecutionRole - Arn Layers: - Ref: ServerlessSaaSLayers Environment: Variables: POWERTOOLS_SERVICE_NAME: OrderService ORDER_TABLE_NAME: Ref: OrderTable ApiGatewayAccessLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/api-gateway/access-logs-serverless-saas-workshop-lab1-api RetentionInDays: 30 ApiGatewayApi: Type: AWS::Serverless::Api Properties: MethodSettings: - DataTraceEnabled: true LoggingLevel: INFO MetricsEnabled: true ResourcePath: /* HttpMethod: '*' AccessLogSetting: DestinationArn: Fn::GetAtt: - ApiGatewayAccessLogs - Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' TracingEnabled: true DefinitionBody: openapi: 3.0.1 info: title: serverless-saas-workshop-lab1 basePath: Fn::Join: - '' - - / - Ref: StageName schemes: - https paths: /order/{id}: get: summary: Returns a order description: Return a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} x-amazon-apigateway-integration: uri: Fn::Join: - '' - - Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - Fn::GetAtt: - GetOrderFunction - Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} x-amazon-apigateway-integration: uri: Fn::Join: - '' - - Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - Fn::GetAtt: - UpdateOrderFunction - Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a order description: Deletes a order by a order id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} x-amazon-apigateway-integration: uri: Fn::Join: - '' - - Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - Fn::GetAtt: - DeleteOrderFunction - Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: '#/definitions/Empty' headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: '''DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT''' method.response.header.Access-Control-Allow-Headers: '''Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token''' method.response.header.Access-Control-Allow-Origin: '''*''' passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /orders: get: summary: Returns all orders description: Returns all orders. produces: - application/json responses: {} x-amazon-apigateway-integration: uri: Fn::Join: - '' - - Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - Fn::GetAtt: - GetOrdersFunction - Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: '#/definitions/Empty' headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: '''DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT''' method.response.header.Access-Control-Allow-Headers: '''Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token''' method.response.header.Access-Control-Allow-Origin: '''*''' passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /order: post: produces: - application/json responses: {} x-amazon-apigateway-integration: uri: Fn::Join: - '' - - Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - Fn::GetAtt: - CreateOrderFunction - Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: '#/definitions/Empty' headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: '''DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT''' method.response.header.Access-Control-Allow-Headers: '''Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token''' method.response.header.Access-Control-Allow-Origin: '''*''' passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /product/{id}: get: summary: Returns a product description: Return a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} x-amazon-apigateway-integration: uri: Fn::Join: - '' - - Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - Fn::GetAtt: - GetProductFunction - Arn - /invocations httpMethod: POST type: aws_proxy put: produces: - application/json responses: {} x-amazon-apigateway-integration: uri: Fn::Join: - '' - - Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - Fn::GetAtt: - UpdateProductFunction - Arn - /invocations httpMethod: POST type: aws_proxy delete: summary: Deletes a product description: Deletes a product by a product id. produces: - application/json parameters: - name: id in: path required: true type: string responses: {} x-amazon-apigateway-integration: uri: Fn::Join: - '' - - Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - Fn::GetAtt: - DeleteProductFunction - Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: '#/definitions/Empty' headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: '''DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT''' method.response.header.Access-Control-Allow-Headers: '''Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token''' method.response.header.Access-Control-Allow-Origin: '''*''' passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /products: get: summary: Returns all products description: Returns all products. produces: - application/json responses: {} x-amazon-apigateway-integration: uri: Fn::Join: - '' - - Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - Fn::GetAtt: - GetProductsFunction - Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: '#/definitions/Empty' headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: '''DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT''' method.response.header.Access-Control-Allow-Headers: '''Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token''' method.response.header.Access-Control-Allow-Origin: '''*''' passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock /product: post: produces: - application/json responses: {} x-amazon-apigateway-integration: uri: Fn::Join: - '' - - Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/ - Fn::GetAtt: - CreateProductFunction - Arn - /invocations httpMethod: POST type: aws_proxy options: consumes: - application/json produces: - application/json responses: '200': description: 200 response schema: $ref: '#/definitions/Empty' headers: Access-Control-Allow-Origin: type: string Access-Control-Allow-Methods: type: string Access-Control-Allow-Headers: type: string x-amazon-apigateway-integration: responses: default: statusCode: 200 responseParameters: method.response.header.Access-Control-Allow-Methods: '''DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT''' method.response.header.Access-Control-Allow-Headers: '''Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token''' method.response.header.Access-Control-Allow-Origin: '''*''' passthroughBehavior: when_no_match requestTemplates: application/json: '{"statusCode": 200}' type: mock StageName: Ref: StageName GetProductsLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::GetAtt: - GetProductsFunction - Arn Principal: apigateway.amazonaws.com SourceArn: Fn::Join: - '' - - 'arn:aws:execute-api:' - Ref: AWS::Region - ':' - Ref: AWS::AccountId - ':' - Ref: ApiGatewayApi - /*/*/* GetProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::GetAtt: - GetProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: Fn::Join: - '' - - 'arn:aws:execute-api:' - Ref: AWS::Region - ':' - Ref: AWS::AccountId - ':' - Ref: ApiGatewayApi - /*/*/* CreateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::GetAtt: - CreateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: Fn::Join: - '' - - 'arn:aws:execute-api:' - Ref: AWS::Region - ':' - Ref: AWS::AccountId - ':' - Ref: ApiGatewayApi - /*/*/* UpdateProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::GetAtt: - UpdateProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: Fn::Join: - '' - - 'arn:aws:execute-api:' - Ref: AWS::Region - ':' - Ref: AWS::AccountId - ':' - Ref: ApiGatewayApi - /*/*/* DeleteProductLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::GetAtt: - DeleteProductFunction - Arn Principal: apigateway.amazonaws.com SourceArn: Fn::Join: - '' - - 'arn:aws:execute-api:' - Ref: AWS::Region - ':' - Ref: AWS::AccountId - ':' - Ref: ApiGatewayApi - /*/*/* GetOrdersLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::GetAtt: - GetOrdersFunction - Arn Principal: apigateway.amazonaws.com SourceArn: Fn::Join: - '' - - 'arn:aws:execute-api:' - Ref: AWS::Region - ':' - Ref: AWS::AccountId - ':' - Ref: ApiGatewayApi - /*/*/* GetOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::GetAtt: - GetOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: Fn::Join: - '' - - 'arn:aws:execute-api:' - Ref: AWS::Region - ':' - Ref: AWS::AccountId - ':' - Ref: ApiGatewayApi - /*/*/* CreateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::GetAtt: - CreateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: Fn::Join: - '' - - 'arn:aws:execute-api:' - Ref: AWS::Region - ':' - Ref: AWS::AccountId - ':' - Ref: ApiGatewayApi - /*/*/* UpdateOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::GetAtt: - UpdateOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: Fn::Join: - '' - - 'arn:aws:execute-api:' - Ref: AWS::Region - ':' - Ref: AWS::AccountId - ':' - Ref: ApiGatewayApi - /*/*/* DeleteOrderLambdaApiGatewayExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::GetAtt: - DeleteOrderFunction - Arn Principal: apigateway.amazonaws.com SourceArn: Fn::Join: - '' - - 'arn:aws:execute-api:' - Ref: AWS::Region - ':' - Ref: AWS::AccountId - ':' - Ref: ApiGatewayApi - /*/*/* CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: Origin Access Identity for CloudFront Distribution AppBucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true AppSiteReadPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: AppBucket PolicyDocument: Statement: - Action: s3:GetObject Effect: Allow Resource: Fn::Sub: arn:aws:s3:::${AppBucket}/* Principal: CanonicalUser: Fn::GetAtt: - CloudFrontOriginAccessIdentity - S3CanonicalUserId ApplicationSite: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: CustomErrorResponses: - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: /index.html - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: /index.html DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 MinTTL: 60 TargetOriginId: lab1-tenantapp-s3origin ViewerProtocolPolicy: allow-all DefaultRootObject: index.html Enabled: true HttpVersion: http2 Origins: - DomainName: Fn::GetAtt: - AppBucket - RegionalDomainName Id: lab1-tenantapp-s3origin S3OriginConfig: OriginAccessIdentity: Fn::Join: - '' - - origin-access-identity/cloudfront/ - Ref: CloudFrontOriginAccessIdentity PriceClass: PriceClass_All Outputs: APIGatewayURL: Description: API Gateway endpoint URL for API Value: Fn::Join: - '' - - Fn::Sub: https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/ - Ref: StageName - / ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: Fn::GetAtt: - ApplicationSite - DomainName AppBucket: Description: The name of the bucket for uploading the Tenant Management site to Value: Ref: AppBucket ================================================ FILE: event-engine-assets/pre-requisites-event-engine.sh ================================================ #!/bin/bash -x . /home/ec2-user/.nvm/nvm.sh #Install python3.8 sudo yum install -y amazon-linux-extras sudo amazon-linux-extras enable python3.8 sudo yum install -y python3.8 sudo alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1 sudo alternatives --set python3 /usr/bin/python3.8 # Uninstall aws cli v1 and Install aws cli version-2.3.0 sudo pip2 uninstall awscli -y echo "Installing aws cli version-2.3.0" curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64-2.3.0.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install rm awscliv2.zip rm -rf aws # Install sam cli version 1.64.0 echo "Installing sam cli version 1.64.0" wget https://github.com/aws/aws-sam-cli/releases/download/v1.64.0/aws-sam-cli-linux-x86_64.zip unzip aws-sam-cli-linux-x86_64.zip -d sam-installation sudo ./sam-installation/install if [ $? -ne 0 ]; then echo "Sam cli is already present, so deleting existing version" sudo rm /usr/local/bin/sam sudo rm -rf /usr/local/aws-sam-cli echo "Now installing sam cli version 1.64.0" sudo ./sam-installation/install fi rm aws-sam-cli-linux-x86_64.zip rm -rf sam-installation # Install git-remote-codecommit version 1.15.1 echo "Installing git-remote-codecommit version 1.15.1" curl -O https://bootstrap.pypa.io/get-pip.py python3 get-pip.py --user rm get-pip.py python3 -m pip install git-remote-codecommit==1.15.1 # Install node v14.18.1 echo "Installing node v14.18.1" nvm deactivate nvm uninstall node nvm install v14.18.1 nvm use v14.18.1 nvm alias default v14.18.1 npm set unsafe-perm true # Install cdk cli version ^2.40.0 echo "Installing cdk cli version^2.40.0" npm uninstall -g aws-cdk npm install -g aws-cdk@"^2.40.0" #Install jq version 1.5 sudo yum -y install jq-1.5 #Install pylint version 2.11.1 python3 -m pip install pylint==2.11.1 python3 -m pip install boto3 ================================================ FILE: event-engine-assets/userinterface-module-sam-template.yaml ================================================ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Template to deploy cloudfront and s3 bucket for UI code. This template will be used to pre-provision cloudfront and s3 buckets for UI code using event engine module during AWS hosted events. So that we don't need to provision them in individual labs and it improves individual labs execution time. Resources: CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: "Origin Access Identity for CloudFront Distributions" AdminAppBucket: Type: AWS::S3::Bucket DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AdminAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref AdminAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AdminAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId AdminAppSite: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: #Aliases: # - !Sub 'admin.${CustomDomainName}' CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS CachedMethods: - GET - HEAD - OPTIONS Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: adminapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt AdminAppBucket.RegionalDomainName Id: adminapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' LandingAppBucket: Type: AWS::S3::Bucket DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True LandingAppSiteReadPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref LandingAppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${LandingAppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId LandingApplicationSite: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: landingapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'LandingAppBucket.RegionalDomainName' Id: landingapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' AppBucket: Type: AWS::S3::Bucket DeletionPolicy : Retain Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: 'AES256' PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True AppSiteReadPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref AppBucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${AppBucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId ApplicationSite: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: CustomErrorResponses: # Needed to support angular routing - ErrorCode: 403 ResponseCode: 200 ResponsePagePath: '/index.html' - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: '/index.html' DefaultCacheBehavior: AllowedMethods: - DELETE - GET - HEAD - OPTIONS - PATCH - POST - PUT Compress: true DefaultTTL: 3600 # in seconds ForwardedValues: Cookies: Forward: none QueryString: false MaxTTL: 86400 # in seconds MinTTL: 60 # in seconds TargetOriginId: tenantapp-s3origin ViewerProtocolPolicy: 'allow-all' DefaultRootObject: 'index.html' Enabled: true HttpVersion: http2 Origins: - DomainName: !GetAtt 'AppBucket.RegionalDomainName' Id: tenantapp-s3origin S3OriginConfig: OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]] PriceClass: 'PriceClass_All' Outputs: AdminBucket: Description: The name of the bucket for uploading the Admin Management site to Value: !Ref AdminAppBucket Export: Name: "Serverless-SaaS-AdminSiteBucket" AdminAppSite: Description: The name of the CloudFront url for Admin Management site Value: !GetAtt AdminAppSite.DomainName Export: Name: "Serverless-SaaS-AdminAppSite" LandingAppBucket: Description: The name of the bucket for uploading the Landing site to Value: !Ref LandingAppBucket Export: Name: "Serverless-SaaS-LandingApplicationSiteBucket" LandingApplicationSite: Description: The name of the CloudFront url for Landing site Value: !GetAtt LandingApplicationSite.DomainName Export: Name: "Serverless-SaaS-LandingApplicationSite" AppBucket: Description: The name of the bucket for uploading the Tenant Management site to Value: !Ref AppBucket Export: Name: "Serverless-SaaS-ApplicationSiteBucket" ApplicationSite: Description: The name of the CloudFront url for Tenant Management site Value: !GetAtt ApplicationSite.DomainName Export: Name: "Serverless-SaaS-ApplicationSite" ================================================ FILE: scripts/cleanup.sh ================================================ #!/bin/bash ## ## This script aims to clean up resources created for the ## SaaS Serverless Workshop. This script is based on the guidance ## provided here: ## https://catalog.us-east-1.prod.workshops.aws/workshops/b0c6ad36-0a4b-45d8-856b-8a64f0ac76bb/en-US/cleanup ## ## Note that this script can also be used to clean up resources for the ## Serverless SaaS Reference Solution as outlined here: ## https://github.com/aws-samples/aws-saas-factory-ref-solution-serverless-saas#steps-to-clean-up ## ## # helper function delete_stack_after_confirming() { if [[ -z "${1}" ]]; then echo "$(date) stack name missing..." return fi stack=$(aws cloudformation describe-stacks --stack-name "$1") if [[ -z "${stack}" ]]; then echo "$(date) stack ${1} does not exist..." return fi if [[ -z "${skip_flag}" ]]; then read -p "Delete stack with name $1 [Y/n] " -n 1 -r fi if [[ $REPLY =~ ^[n]$ ]]; then echo "$(date) NOT deleting stack $1." else echo "$(date) deleting stack $1..." aws cloudformation delete-stack --stack-name "$1" echo "$(date) waiting for stack delete operation to complete..." aws cloudformation wait stack-delete-complete --stack-name "$1" fi } # helper function delete_codecommit_repo_after_confirming() { REPO_NAME="$1" repo=$(aws codecommit get-repository --repository-name "$REPO_NAME") if [[ -n "${repo}" ]]; then if [[ -z "${skip_flag}" ]]; then read -p "Delete codecommit repo with name \"$REPO_NAME\" [Y/n] " -n 1 -r fi if [[ $REPLY =~ ^[n]$ ]]; then echo "$(date) NOT deleting $REPO_NAME." else echo "$(date) deleting codecommit repo \"$REPO_NAME\"..." aws codecommit delete-repository --repository-name "$REPO_NAME" fi else echo "$(date) repo \"$REPO_NAME\" does not exist..." fi } skip_flag='' while getopts 's' flag; do case "${flag}" in s) skip_flag='true' ;; *) error "Unexpected option ${flag}!" && exit 1 ;; esac done echo "$(date) Checking for prerequisites..." jq --version || { echo "jq missing! Please install before using this script." exit 1 } aws --version || { echo "ÅWS cli missing! Please install before using this script." exit 1 } echo "$(date) Done checking for prerequisites." echo "$(date) Cleaning up resources..." if [[ -n "${skip_flag}" ]]; then echo "skip_flag enabled. Script will not pause for confirmation before deleting resources!" else echo "skip_flag disabled. Script will pause for confirmation before deleting resources." fi delete_stack_after_confirming "serverless-saas-workshop-lab1" delete_stack_after_confirming "stack-pooled" delete_stack_after_confirming "serverless-saas-cost-per-tenant-lab7" echo "$(date) cleaning up platinum tenants..." next_token="" STACK_STATUS_FILTER="CREATE_COMPLETE ROLLBACK_COMPLETE UPDATE_COMPLETE UPDATE_ROLLBACK_COMPLETE IMPORT_COMPLETE IMPORT_ROLLBACK_COMPLETE" while true; do if [[ "${next_token}" == "" ]]; then echo "$(date) making api call to search for platinum tenants..." # shellcheck disable=SC2086 # ignore shellcheck error for adding a quote as that causes the api call to fail response=$(aws cloudformation list-stacks --stack-status-filter $STACK_STATUS_FILTER) else echo "$(date) making api call to search for platinum tenants..." # shellcheck disable=SC2086 # ignore shellcheck error for adding a quote as that causes the api call to fail response=$(aws cloudformation list-stacks --stack-status-filter $STACK_STATUS_FILTER --starting-token "$next_token") fi tenant_stacks=$(echo "$response" | jq -r '.StackSummaries[].StackName | select(. | test("^stack-*"))') for i in $tenant_stacks; do delete_stack_after_confirming "$i" done next_token=$(echo "$response" | jq '.NextToken') if [[ "${next_token}" == "null" ]]; then echo "$(date) no more platinum tenants left." # no more results left. Exit loop... break fi done delete_stack_after_confirming "serverless-saas" delete_stack_after_confirming "serverless-saas-pipeline" # delete_codecommit_repo_after_confirming "aws-saas-factory-ref-serverless-saas" delete_codecommit_repo_after_confirming "aws-serverless-saas-workshop" echo "$(date) cleaning up buckets..." for i in $(aws s3 ls | awk '{print $3}' | grep -E "^serverless-saas-*|^sam-bootstrap-*"); do if [[ -z "${skip_flag}" ]]; then read -p "Delete bucket with name s3://${i} [Y/n] " -n 1 -r fi if [[ $REPLY =~ ^[n]$ ]]; then echo "$(date) NOT deleting bucket s3://${i}." else echo "$(date) emptying out s3 bucket with name s3://${i}..." aws s3 rm --recursive "s3://${i}" echo "$(date) deleting s3 bucket with name s3://${i}..." aws s3 rb "s3://${i}" fi done echo "$(date) cleaning up log groups..." next_token="" while true; do if [[ "${next_token}" == "" ]]; then response=$(aws logs describe-log-groups) else response=$(aws logs describe-log-groups --starting-token "$next_token") fi log_groups=$(echo "$response" | jq -r '.logGroups[].logGroupName | select(. | test("^/aws/lambda/stack-*|^/aws/lambda/serverless-saas-*"))') for i in $log_groups; do if [[ -z "${skip_flag}" ]]; then read -p "Delete log group with name $i [Y/n] " -n 1 -r fi if [[ $REPLY =~ ^[n]$ ]]; then echo "$(date) NOT deleting log group $i." else echo "$(date) deleting log group with name $i..." aws logs delete-log-group --log-group-name "$i" fi done next_token=$(echo "$response" | jq '.NextToken') if [[ "${next_token}" == "null" ]]; then # no more results left. Exit loop... break fi done echo "$(date) cleaning up user pools..." next_token="" while true; do if [[ "${next_token}" == "" ]]; then response=$(aws cognito-idp list-user-pools --max-results 1) else response=$(aws cognito-idp list-user-pools --max-results 1 --starting-token "$next_token") fi pool_ids=$(echo "$response" | jq -r '.UserPools[] | select(.Name | test("^.*-ServerlessSaaSUserPool$")) |.Id') for i in $pool_ids; do if [[ -z "${skip_flag}" ]]; then read -p "Delete user pool with name $i [Y/n] " -n 1 -r fi if [[ $REPLY =~ ^[n]$ ]]; then echo "$(date) NOT deleting user pool $i." else echo "$(date) deleting user pool with name $i..." echo "getting pool domain..." pool_domain=$(aws cognito-idp describe-user-pool --user-pool-id "$i" | jq -r '.UserPool.Domain') echo "deleting pool domain $pool_domain..." aws cognito-idp delete-user-pool-domain \ --user-pool-id "$i" \ --domain "$pool_domain" echo "deleting pool $i..." aws cognito-idp delete-user-pool --user-pool-id "$i" fi done next_token=$(echo "$response" | jq '.NextToken') if [[ "${next_token}" == "null" ]]; then # no more results left. Exit loop... break fi done echo "$(date) Done cleaning up resources!" ================================================ FILE: scripts/create_tenants.sh ================================================ #!/bin/bash ## ## This script is to help create different kinds of tenants quickly. ## ## To use: ## bash create_tenants.sh myemail mydomain.com ## EMAIL_ALIAS="$1" # ex. test EMAIL_DOMAIN="$2" # ex. test.com echo "$(date) finding saas admin API GW url..." next_token="" while true; do if [[ "${next_token}" == "" ]]; then echo "$(date) making api call to search for saas admin API GWs..." response=$(aws apigateway get-rest-apis) else echo "$(date) making api call to search for saas admin API GWs..." response=$(aws apigateway get-rest-apis --starting-token "$next_token") fi api_gw_id=$(echo "$response" | jq -r '.items[] | select (.name | match("serverless-saas-admin-api")) | .id') if [[ "${api_gw_id}" != "" ]]; then echo "$(date) saas admin API rest id found!" # no need to look any further... break fi next_token=$(echo "$response" | jq '.NextToken') if [[ "${next_token}" == "null" ]]; then echo "$(date) no more API GWs left!" # no more results left. Exit loop... break fi done SAAS_ADMIN_URL_STAGE="prod" CURRENT_REGION=$(aws configure get region || echo "$AWS_DEFAULT_REGION") SAAS_ADMIN_URL="https://${api_gw_id}.execute-api.${CURRENT_REGION}.amazonaws.com/${SAAS_ADMIN_URL_STAGE}" # ex. https://m6slpkzugb.execute-api.us-west-2.amazonaws.com echo "EMAIL_ALIAS=${EMAIL_ALIAS}" echo "EMAIL_DOMAIN=${EMAIL_DOMAIN}" echo "SAAS_ADMIN_URL=${SAAS_ADMIN_URL}" echo "REGION=${CURRENT_REGION}" read -rp "press any key to confirm above parameters and continue..." echo "$(date) Creating a Standard tenant..." curl --location --request POST "${SAAS_ADMIN_URL}/registration" \ --header 'Content-Type: application/json' \ --data-raw "{ \"tenantName\": \"tenantstandardB\", \"tenantAddress\": \"123 street\", \"tenantEmail\": \"${EMAIL_ALIAS}+tenantstandardB@${EMAIL_DOMAIN}\", \"tenantPhone\": \"1234567890\", \"tenantTier\": \"Standard\" }" echo "$(date) Done creating a Standard tenant!" echo "$(date) Creating a Platinum tenant..." curl --location --request POST "${SAAS_ADMIN_URL}/registration" \ --header 'Content-Type: application/json' \ --data-raw "{ \"tenantName\": \"tenantplatinumB\", \"tenantAddress\": \"123 street\", \"tenantEmail\": \"${EMAIL_ALIAS}+tenantplatinumB@${EMAIL_DOMAIN}\", \"tenantPhone\": \"1234567890\", \"tenantTier\": \"Platinum\" }" echo "$(date) Done creating a Platinum tenant!" echo "$(date) Creating a Premium tenant..." curl --location --request POST "${SAAS_ADMIN_URL}/registration" \ --header 'Content-Type: application/json' \ --data-raw "{ \"tenantName\": \"tenantpremiumB\", \"tenantAddress\": \"123 street\", \"tenantEmail\": \"${EMAIL_ALIAS}+tenantpremiumB@${EMAIL_DOMAIN}\", \"tenantPhone\": \"1234567890\", \"tenantTier\": \"Premium\" }" echo "$(date) Done creating a Premium tenant!" ================================================ FILE: scripts/lab2_updates.py ================================================ from replace_function import replace_in_file lab2_tenant_mgmt_original_str = """#TODO: Implement the below method def get_tenant(event, context): pass """ lab2_tenant_mgmt_update_str = """def get_tenant(event, context): tenant_id = event['pathParameters']['tenantid'] logger.info("Request received to get tenant details") tenant_details = table_tenant_details.get_item( Key={ 'tenantId': tenant_id, }, AttributesToGet=[ 'tenantName', 'tenantAddress', 'tenantEmail', 'tenantPhone' ] ) item = tenant_details['Item'] tenant_info = TenantInfo(item['tenantName'], item['tenantAddress'],item['tenantEmail'], item['tenantPhone']) logger.info(tenant_info) logger.info("Request completed to get tenant details") return utils.create_success_response(tenant_info.__dict__) """ replace_in_file(lab2_tenant_mgmt_original_str, lab2_tenant_mgmt_update_str, "../Lab2/server/TenantManagementService/tenant-management.py") lab2_user_mgmt_original_str = """#TODO: Implement the below method def create_user(event, context): pass """ lab2_user_mgmt_update_str = """def create_user(event, context): user_details = json.loads(event['body']) logger.info("Request received to create new user") logger.info(event) tenant_id = user_details['tenantId'] response = client.admin_create_user( Username=user_details['userName'], UserPoolId=user_pool_id, ForceAliasCreation=True, UserAttributes=[ { 'Name': 'email', 'Value': user_details['userEmail'] }, { 'Name': 'custom:userRole', 'Value': user_details['userRole'] }, { 'Name': 'custom:tenantId', 'Value': tenant_id } ] ) logger.info(response) user_mgmt = UserManagement() user_mgmt.add_user_to_group(user_pool_id, user_details['userName'], tenant_id) response_mapping = user_mgmt.create_user_tenant_mapping(user_details['userName'], tenant_id) logger.info("Request completed to create new user ") return utils.create_success_response("New user created") """ replace_in_file(lab2_user_mgmt_original_str, lab2_user_mgmt_update_str, "../Lab2/server/TenantManagementService/user-management.py") lab2_registration_svc_original_str = """#TODO: Implement this method def register_tenant(event, context): pass """ lab2_registration_svc_update_str = """def register_tenant(event, context): try: tenant_id = uuid.uuid1().hex tenant_details = json.loads(event['body']) tenant_details['tenantId'] = tenant_id logger.info(tenant_details) stage_name = event['requestContext']['stage'] host = event['headers']['Host'] auth = utils.get_auth(host, region) headers = utils.get_headers(event) create_user_response = __create_tenant_admin_user(tenant_details, headers, auth, host, stage_name) logger.info (create_user_response) tenant_details['tenantAdminUserName'] = create_user_response['message']['tenantAdminUserName'] create_tenant_response = __create_tenant(tenant_details, headers, auth, host, stage_name) logger.info (create_tenant_response) except Exception as e: logger.error('Error registering a new tenant') raise Exception('Error registering a new tenant', e) else: return utils.create_success_response("You have been registered in our system") """ replace_in_file(lab2_registration_svc_original_str, lab2_registration_svc_update_str, "../Lab2/server/TenantManagementService/tenant-registration.py") ================================================ FILE: scripts/lab3_updates.py ================================================ from replace_function import replace_in_file lab3_tenant_auth_original_str = """ # TODO: Add tenant context to authResponse""" lab3_tenant_auth_update_str = """ context = { 'userName': user_name, 'tenantId': tenant_id } authResponse['context'] = context """ replace_in_file(lab3_tenant_auth_original_str, lab3_tenant_auth_update_str, "../Lab3/server/Resources/tenant_authorizer.py") lab3_shared_svc_original_str = """ #TODO: Add policy so that only tenant and SaaS admins can add/modify tenant information""" lab3_shared_svc_update_str = """ if (auth_manager.isTenantAdmin(user_role) or auth_manager.isSystemAdmin(user_role)): policy.allowAllMethods() if (auth_manager.isTenantAdmin(user_role)): policy.denyMethod(HttpVerb.POST, "tenant-activation") policy.denyMethod(HttpVerb.GET, "tenants") else: #if not tenant admin or system admin then only allow to get info and update info policy.allowMethod(HttpVerb.GET, "user/*") policy.allowMethod(HttpVerb.PUT, "user/*") """ replace_in_file(lab3_shared_svc_original_str, lab3_shared_svc_update_str, "../Lab3/server/Resources/shared_service_authorizer.py") lab3_layer_original_str = """#TODO: Implement the below method def record_metric(event, metric_name, metric_unit, metric_value): pass """ lab3_layer_update_str = """def record_metric(event, metric_name, metric_unit, metric_value): \"\"\"Record the metric in Cloudwatch using EMF format Args: event ([type]): [description] metric_name ([type]): [description] metric_unit ([type]): [description] metric_value ([type]): [description] \"\"\" metrics.add_dimension(name="tenant_id", value=event['requestContext']['authorizer']['tenantId']) metrics.add_metric(name=metric_name, unit=metric_unit, value=metric_value) metrics_object = metrics.serialize_metric_set() metrics.clear_metrics() print(json.dumps(metrics_object)) """ replace_in_file(lab3_layer_original_str, lab3_layer_update_str, "../Lab3/server/layers/metrics_manager.py") lab3_product_original_str = """#TODO: Implement this method def create_product(event, payload): pass """ lab3_product_update_str = """def create_product(event, payload): tenantId = event['requestContext']['authorizer']['tenantId'] suffix = random.randrange(suffix_start, suffix_end) shardId = tenantId+"-"+str(suffix) product = Product(shardId, str(uuid.uuid4()), payload.sku,payload.name, payload.price, payload.category) try: response = table.put_item( Item= { 'shardId': shardId, 'productId': product.productId, 'sku': product.sku, 'name': product.name, 'price': product.price, 'category': product.category } ) except ClientError as e: logger.error(e.response['Error']['Message']) raise Exception('Error adding a product', e) else: logger.info("PutItem succeeded:") return product """ replace_in_file(lab3_product_original_str, lab3_product_update_str, "../Lab3/server/ProductService/product_service_dal.py") ================================================ FILE: scripts/lab4_updates.py ================================================ from replace_function import replace_in_file lab4_tenant_auth_original_str = """ #TODO : Add code for Fine-Grained-Access-Control""" lab4_tenant_auth_update_str = """ iam_policy = auth_manager.getPolicyForUser(user_role, utils.Service_Identifier.BUSINESS_SERVICES.value, tenant_id, region, aws_account_id) logger.info(iam_policy) role_arn = "arn:aws:iam::{}:role/authorizer-access-role".format(aws_account_id) assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName="tenant-aware-session", Policy=iam_policy, ) credentials = assumed_role["Credentials"] #pass sts credentials to lambda context = { 'accesskey': credentials['AccessKeyId'], # $context.authorizer.key -> value 'secretkey' : credentials['SecretAccessKey'], 'sessiontoken' : credentials["SessionToken"], 'userName': user_name, 'tenantId': tenant_id, 'userPoolId': userpool_id, 'userRole': user_role } authResponse['context'] = context """ replace_in_file(lab4_tenant_auth_original_str, lab4_tenant_auth_update_str, "../Lab4/server/Resources/tenant_authorizer.py") lab4_product_original_str = """ #TODO: Implement this method""" lab4_product_update_str = """ accesskey = event['requestContext']['authorizer']['accesskey'] secretkey = event['requestContext']['authorizer']['secretkey'] sessiontoken = event['requestContext']['authorizer']['sessiontoken'] dynamodb = boto3.resource('dynamodb', aws_access_key_id=accesskey, aws_secret_access_key=secretkey, aws_session_token=sessiontoken ) return dynamodb.Table(table_name) """ replace_in_file(lab4_product_original_str, lab4_product_update_str, "../Lab4/server/ProductService/product_service_dal.py") replace_in_file(lab4_product_original_str, lab4_product_update_str, "../Lab4/server/OrderService/order_service_dal.py") ================================================ FILE: scripts/lab5_updates.py ================================================ from replace_function import replace_in_file lab5_user_mgmt_original_str = """ #TODO: add code to provision new user pool pass """ lab5_user_mgmt_update_str = """ user_pool_response = user_mgmt.create_user_pool(tenant_id) user_pool_id = user_pool_response['UserPool']['Id'] logger.info (user_pool_id) app_client_response = user_mgmt.create_user_pool_client(user_pool_id) logger.info(app_client_response) app_client_id = app_client_response['UserPoolClient']['ClientId'] user_pool_domain_response = user_mgmt.create_user_pool_domain(user_pool_id, tenant_id) logger.info ("New Tenant Created") """ replace_in_file(lab5_user_mgmt_original_str, lab5_user_mgmt_update_str, "../Lab5/server/TenantManagementService/user-management.py") lab5_tenant_prov_original_str = """ #TODO: Add missing code to kick off the pipeline pass """ lab5_tenant_prov_update_str = """ response_ddb = table_tenant_stack_mapping.put_item( Item={ 'tenantId': tenant_details['tenantId'], 'stackName': stack_name.format(tenant_details['tenantId']), 'applyLatestRelease': True, 'codeCommitId': '' } ) logger.info(response_ddb) response_codepipeline = codepipeline.start_pipeline_execution( name='serverless-saas-pipeline' ) logger.info(response_ddb) """ replace_in_file(lab5_tenant_prov_original_str, lab5_tenant_prov_update_str, "../Lab5/server/TenantManagementService/tenant-provisioning.py") ================================================ FILE: scripts/lab6_updates.py ================================================ from replace_function import replace_in_file lab6_tenant_reg_original_str = """ #TODO: Pass relevant apikey to tenant_details object based upon tenant tier if (tenant_details['tenantTier'].upper() == utils.TenantTier.PLATINUM.value.upper()): tenant_details['dedicatedTenancy'] = 'true' """ lab6_tenant_reg_update_str = """ if (tenant_details['tenantTier'].upper() == utils.TenantTier.PLATINUM.value.upper()): tenant_details['dedicatedTenancy'] = 'true' api_key = platinum_tier_api_key elif (tenant_details['tenantTier'].upper() == utils.TenantTier.PREMIUM.value.upper()): api_key = premium_tier_api_key elif (tenant_details['tenantTier'].upper() == utils.TenantTier.STANDARD.value.upper()): api_key = standard_tier_api_key elif (tenant_details['tenantTier'].upper() == utils.TenantTier.BASIC.value.upper()): api_key = basic_tier_api_key tenant_details['apiKey'] = api_key """ replace_in_file(lab6_tenant_reg_original_str, lab6_tenant_reg_update_str, "../Lab6/server/TenantManagementService/tenant-registration.py") lab6_tenant_mgmt_original_str = """#'apiKey': tenant_details['apiKey'],""" lab6_tenant_mgmt_update_str = """'apiKey': tenant_details['apiKey'],""" replace_in_file(lab6_tenant_mgmt_original_str, lab6_tenant_mgmt_update_str, "../Lab6/server/TenantManagementService/tenant-management.py") lab6_tenant_auth_original_str = """ #TODO: Get API Key from tenant management table #api_key = tenant_details['Item']['apiKey'] """ lab6_tenant_auth_update_str = """ api_key = tenant_details['Item']['apiKey']""" replace_in_file(lab6_tenant_auth_original_str, lab6_tenant_auth_update_str, "../Lab6/server/Resources/tenant_authorizer.py") lab6_tenant_auth_original_str_2 = """ #TODO: Assign API Key to authorizer response #'apiKey': api_key, """ lab6_tenant_auth_update_str_2 = """ 'apiKey': api_key,""" replace_in_file(lab6_tenant_auth_original_str_2, lab6_tenant_auth_update_str_2, "../Lab6/server/Resources/tenant_authorizer.py") ================================================ FILE: scripts/replace_function.py ================================================ def replace_in_file(str_to_find, replacement_str, file_name): fh = open(file_name, "r") txt = fh.read() ts = txt.split(str_to_find) fh.close() fh = open(file_name, "w") fh.write(ts[0] + replacement_str + ts[1]) fh.close() ================================================ FILE: scripts/run_all_labs.sh ================================================ #!/bin/bash ## ## This script aims to automatically deploy the labs ## using the completed labs found in the Solutions folder. ## echo "################ Running pre-req script... ################" cd ../Cloud9Setup/ ./increase-disk-size.sh # ./pre-requisites.sh cd ../scripts/ echo "################ Done running pre-req script... ################" # echo "################ Running labs... ################" # #### Note that deploying lab1 is not a requirement #### # # echo "################ Running lab1... ################" # # cd ../Solution/Lab1/scripts # # ./deployment.sh -s -c --stack-name serverless-saas-workshop-lab1 # # cd ../../../scripts/ # # echo "################ Done running lab1. ################" # ####################################################### echo "################ Running lab2... ################" cd ../Solution/Lab2/scripts ./deployment.sh -s -c --email syeduh+serverlesslab@amazon.com ./deployment.sh -s cd ../../../scripts/ echo "################ Done running lab2. ################" echo "################ Sleeping for a minute before moving to next lab... ################" sleep 60 echo "################ Running lab3... ################" cd ../Solution/Lab3/scripts ./deployment.sh -s -c ./deployment.sh -s cd ../../../scripts/ echo "################ Done running lab3. ################" echo "################ Sleeping for a minute before moving to next lab... ################" sleep 60 echo "################ Running lab4... ################" cd ../Solution/Lab4/scripts ./deployment.sh -s cd ../../../scripts/ echo "################ Done running lab4. ################" echo "################ Sleeping for a minute before moving to next lab... ################" sleep 60 echo "################ Running lab5... ################" cd ../Solution/Lab5/scripts/ ./deployment.sh -s -c ./deployment.sh -s cd ../../../scripts/ echo "################ Done running lab5. ################" echo "################ Sleeping for a minute before moving to next lab... ################" sleep 60 echo "################ Running lab6... ################" cd ../Solution/Lab6/scripts/ ./deployment.sh cd ../../../scripts/ echo "################ Done running lab6. ################" ================================================ FILE: scripts/run_workshop.sh ================================================ #!/bin/bash ## ## This script aims to automatically deploy the labs ## as outlined in this workshop here: ## https://catalog.us-east-1.prod.workshops.aws/workshops/b0c6ad36-0a4b-45d8-856b-8a64f0ac76bb/en-US ## echo "################ Running pre-req script... ################" cd ../Cloud9Setup/ ./increase-disk-size.sh # ./pre-requisites.sh cd ../scripts/ echo "################ Done running pre-req script... ################" # #### Note that deploying lab1 is not a requirement #### # ####################################################### echo "################ Running lab2... ################" cd ../Lab2/scripts ./deployment.sh -s -c --email syeduh+serverlesslab@amazon.com cd ../../scripts/ python3 lab2_updates.py cd ../Lab2/scripts ./deployment.sh -s cd ../../scripts/ echo "################ Done running lab2. ################" echo "################ Sleeping for a minute before moving to next lab... ################" sleep 60 echo "################ Running lab3... ################" cd ../Lab3/scripts ./deployment.sh -s -c cd ../../scripts/ python3 lab3_updates.py cd ../Lab3/scripts ./deployment.sh -s cd ../../scripts/ echo "################ Done running lab3. ################" echo "################ Sleeping for a minute before moving to next lab... ################" sleep 60 echo "################ Running lab4... ################" python3 lab4_updates.py cd ../Lab4/scripts ./deployment.sh -s cd ../../scripts/ echo "################ Done running lab4. ################" echo "################ Sleeping for a minute before moving to next lab... ################" sleep 60 echo "################ Running lab5... ################" cd ../Lab5/scripts/ ./deployment.sh -s -c cd ../../scripts/ python3 lab5_updates.py cd ../Lab5/scripts/ ./deployment.sh -s cd ../../scripts/ echo "################ Done running lab5. ################" echo "################ Sleeping for a minute before moving to next lab... ################" sleep 60 echo "################ Running lab6... ################" python3 lab6_updates.py cd ../Lab6/scripts/ ./deployment.sh cd ../../scripts/ echo "################ Done running lab6. ################"