├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPER_GUIDE.md ├── LICENSE ├── README.kr.md ├── README.md ├── client ├── AdminWeb │ ├── .editorconfig │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── angular.json │ ├── config-asset-loader.ts │ ├── nginx.conf │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── _nav.ts │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── auth-config.module.ts │ │ │ ├── auth-guard.guard.ts │ │ │ ├── auth.interceptor.ts │ │ │ ├── models │ │ │ │ ├── index.ts │ │ │ │ └── interfaces.ts │ │ │ ├── nav │ │ │ │ ├── nav.component.css │ │ │ │ ├── nav.component.html │ │ │ │ └── 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 │ │ │ │ ├── detail │ │ │ │ ├── detail.component.html │ │ │ │ └── detail.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 │ │ ├── assets │ │ │ └── logo.svg │ │ ├── custom-theme.scss │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── styles │ │ │ ├── _variables.scss │ │ │ └── reset.scss │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── yarn.lock └── Application │ ├── .browserslistrc │ ├── .editorconfig │ ├── angular.json │ ├── 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 │ │ └── logo.svg │ ├── custom-theme.scss │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ └── styles │ │ ├── _variables.scss │ │ └── reset.scss │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── yarn.lock ├── images ├── advanced-tier.png ├── archi-base-infra.png ├── archi-high-level.png ├── basic-tier.png ├── premium-tier.png ├── routing-parallel-albs.png ├── routing-premium-tier.png ├── service-connect.png └── solution-architecture-tiers.png ├── package.json ├── scripts ├── build-application.sh ├── cleanup.sh ├── del-secrets.sh ├── deprovision-tenant.sh ├── get-adv-network.sh ├── init-install.sh ├── install.sh ├── provision-tenant.sh ├── resize-cloud9.sh ├── sbt-install.sh ├── update-provision-source.sh └── update-tenants.sh └── server ├── .npmignore ├── application ├── .gitignore ├── Dockerfile.order ├── Dockerfile.product ├── Dockerfile.rproxy ├── Dockerfile.user ├── libs │ ├── auth │ │ ├── src │ │ │ ├── auth-config.ts │ │ │ ├── auth.decorator.ts │ │ │ ├── auth.module.ts │ │ │ ├── credential-vendor.ts │ │ │ ├── index.ts │ │ │ ├── jwt-auth.guard.ts │ │ │ ├── jwt.strategy.ts │ │ │ └── policies.json │ │ └── tsconfig.lib.json │ └── client-factory │ │ ├── src │ │ ├── client-factory.module.ts │ │ ├── client-factory.service.ts │ │ └── index.ts │ │ └── tsconfig.lib.json ├── microservices │ ├── order │ │ ├── src │ │ │ ├── main.ts │ │ │ └── orders │ │ │ │ ├── dto │ │ │ │ ├── create-order.dto.ts │ │ │ │ ├── order-product.dto.ts │ │ │ │ └── update-order.dto.ts │ │ │ │ ├── entities │ │ │ │ └── order.entity.ts │ │ │ │ ├── orders.controller.ts │ │ │ │ ├── orders.module.ts │ │ │ │ └── orders.service.ts │ │ └── tsconfig.app.json │ ├── product_dynamodb │ │ ├── src │ │ │ ├── main.ts │ │ │ └── products │ │ │ │ ├── dto │ │ │ │ ├── create-product.dto.ts │ │ │ │ └── update-product.dto.ts │ │ │ │ ├── entities │ │ │ │ └── product.entity.ts │ │ │ │ ├── products.controller.ts │ │ │ │ ├── products.module.ts │ │ │ │ └── products.service.ts │ │ └── tsconfig.app.json │ ├── product_mysql │ │ ├── src │ │ │ ├── main.ts │ │ │ └── products │ │ │ │ ├── dto │ │ │ │ ├── create-product.dto.ts │ │ │ │ └── update-product.dto.ts │ │ │ │ ├── entities │ │ │ │ └── product.entity.ts │ │ │ │ ├── products.controller.ts │ │ │ │ ├── products.module.ts │ │ │ │ └── products.service.ts │ │ └── tsconfig.app.json │ └── user │ │ ├── src │ │ ├── main.ts │ │ └── users │ │ │ ├── dto │ │ │ ├── identity.dto.ts │ │ │ ├── update-user.dto.ts │ │ │ └── user.dto.ts │ │ │ ├── entities │ │ │ └── user.entity.ts │ │ │ ├── users.controller.ts │ │ │ ├── users.module.ts │ │ │ └── users.service.ts │ │ └── tsconfig.app.json ├── nest-cli.json ├── package.json ├── reverseproxy │ ├── index.html │ └── nginx.template ├── tsconfig.json └── yarn.lock ├── bin └── ecs-saas-ref-template.ts ├── cdk.json ├── lib ├── bootstrap-template │ ├── control-plane-stack.ts │ ├── core-appplane-stack.ts │ └── static-site.ts ├── cdknag │ ├── control-plane-nag.ts │ ├── core-app-plane-nag.ts │ ├── ecs-saas-pipeline-nag.ts │ ├── shared-infra-nag.ts │ ├── tenant-service-nag.ts │ └── tenant-template-nag.ts ├── interfaces │ ├── api-key-ssm-parameter-names.ts │ ├── container-info.ts │ ├── custom-api-key.ts │ ├── identity-details.ts │ └── rproxy-info.ts ├── shared-infra │ ├── Resources │ │ ├── requirements.txt │ │ └── tenant_authorizer.py │ ├── api-gateway.ts │ ├── layers │ │ ├── Dockerfile │ │ ├── abstract_classes │ │ │ ├── idp_authorizer_abstract_class.py │ │ │ └── idp_user_management_abstract_class.py │ │ ├── auth_manager.py │ │ ├── cognito │ │ │ ├── cognito_authorizer.py │ │ │ ├── cognito_user_management_service.py │ │ │ └── user_management_util.py │ │ ├── idp_object_factory.py │ │ ├── logger.py │ │ ├── metrics_manager.py │ │ ├── requirements.txt │ │ └── utils.py │ ├── mysql-database │ │ ├── SSLCA.pem │ │ ├── mysql_database.py │ │ └── requirements.txt │ ├── rds-cluster.ts │ ├── shared-infra-stack.ts │ ├── static-site-distro.ts │ ├── tenant-api-key.ts │ └── usage-plans.ts ├── tenant-api-prod.json ├── tenant-template │ ├── ecs-cluster.ts │ ├── ecs-dynamodb.ts │ ├── eni-trunking.ts │ ├── identity-provider.ts │ ├── services.ts │ ├── tenant-template-stack.ts │ └── tenant-update-stack.ts └── utilities │ ├── destroy-policy-setter.ts │ ├── ecs-utils.ts │ └── helper-functions.ts ├── package-lock.json ├── package.json ├── service-info.txt ├── service-info_mysql.txt └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | application-plane/ 6 | 7 | .vscode/ 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | 12 | *service-info.json 13 | cdk.context.json 14 | # ../server/lib/service-info.json 15 | .DS_Store 16 | .idea/ 17 | # RDS SSL certification 18 | *.pem 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /client/AdminWeb/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /client/AdminWeb/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /client/AdminWeb/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/bitnami/node:16.16.0 AS build 2 | WORKDIR /usr/src/app 3 | COPY package.json ./ 4 | RUN yarn 5 | COPY . . 6 | RUN yarn build --configuration production 7 | 8 | FROM public.ecr.aws/nginx/nginx:1.23 9 | COPY nginx.conf /etc/nginx/conf.d/default.conf 10 | COPY --from=build /usr/src/app/dist /usr/share/nginx/html/admin 11 | -------------------------------------------------------------------------------- /client/AdminWeb/config-asset-loader.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { shareReplay } from 'rxjs/operators'; 5 | 6 | interface Configuration { 7 | apiUrl: string; 8 | stage: string; 9 | } 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class ConfigAssetLoaderService { 13 | private readonly CONFIG_URL = './assets/config/config.json'; 14 | private configuration$: Observable | undefined; 15 | 16 | constructor(private http: HttpClient) {} 17 | 18 | public loadConfigurations(): Observable { 19 | console.log('IN LOADCONFIGURATIONS'); 20 | if (!this.configuration$) { 21 | this.configuration$ = this.http 22 | .get(this.CONFIG_URL) 23 | .pipe(shareReplay(1)); 24 | } 25 | return this.configuration$; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/AdminWeb/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | location /admin { 5 | alias /usr/share/nginx/html/admin/; 6 | index index.html; 7 | try_files $uri $uri/ index.html =404; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/AdminWeb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashboard", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "upgrade": "ng update", 9 | "watch": "ng build --watch --configuration development" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^15.2.9", 14 | "@angular/cdk": "14.2.7", 15 | "@angular/common": "^15.2.9", 16 | "@angular/compiler": "^15.2.9", 17 | "@angular/core": "^15.2.9", 18 | "@angular/flex-layout": "^14.0.0-beta.41", 19 | "@angular/forms": "^15.2.9", 20 | "@angular/material": "14.2.7", 21 | "@angular/platform-browser": "^15.2.9", 22 | "@angular/platform-browser-dynamic": "^15.2.9", 23 | "@angular/router": "^15.2.9", 24 | "angular-auth-oidc-client": "^16.0.0", 25 | "bootstrap": "5.3.1", 26 | "chart.js": "^3.9.1", 27 | "chartjs-plugin-datalabels": "^2.2.0", 28 | "ng2-charts": "^4.1.1", 29 | "rxjs": "~7.8.1", 30 | "tslib": "^2.6.1", 31 | "uuid": "^9.0.0", 32 | "zone.js": "~0.13.1" 33 | }, 34 | "devDependencies": { 35 | "@angular-devkit/build-angular": "^15.2.9", 36 | "@angular/cli": "~15.2.9", 37 | "@angular/compiler-cli": "^15.2.9", 38 | "@types/jasmine": "~4.3.5", 39 | "@types/uuid": "^9.0.2", 40 | "typescript": "~4.9.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/_nav.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { INavData } from './models'; 6 | 7 | export const navItems: INavData[] = [ 8 | /* { 9 | name: 'Dashboard', 10 | url: '/dashboard', 11 | icon: 'insights', 12 | },*/ 13 | { 14 | name: 'Tenants', 15 | url: '/tenants', 16 | icon: 'groups', 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { NavComponent } from './nav/nav.component'; 4 | import { AuthComponent } from './views/auth/auth.component'; 5 | import { canActivate } from './auth-guard.guard'; 6 | 7 | export const routes: Routes = [ 8 | { 9 | path: '', 10 | redirectTo: 'tenants', 11 | pathMatch: 'full', 12 | }, 13 | { 14 | path: '', 15 | component: NavComponent, 16 | data: { 17 | title: 'Home', 18 | }, 19 | canActivate: [canActivate], 20 | children: [ 21 | { 22 | path: 'auth/info', 23 | component: AuthComponent, 24 | }, 25 | { 26 | path: 'dashboard', 27 | loadChildren: () => 28 | import('./views/dashboard/dashboard.module').then( 29 | (m) => m.DashboardModule, 30 | ), 31 | }, 32 | { 33 | path: 'tenants', 34 | loadChildren: () => 35 | import('./views/tenants/tenants.module').then((m) => m.TenantsModule), 36 | }, 37 | ], 38 | }, 39 | ]; 40 | 41 | @NgModule({ 42 | imports: [RouterModule.forRoot(routes)], 43 | exports: [RouterModule], 44 | }) 45 | export class AppRoutingModule {} 46 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatIconRegistry } from '@angular/material/icon'; 3 | import { DomSanitizer } from '@angular/platform-browser'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | template: ` `, 8 | }) 9 | export class AppComponent { 10 | constructor( 11 | private matIconRegistry: MatIconRegistry, 12 | private domSanitizer: DomSanitizer, 13 | ) { 14 | this.matIconRegistry.addSvgIcon( 15 | 'saas-commerce', 16 | this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg'), 17 | ); 18 | } 19 | title = 'dashboard'; 20 | } 21 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { 5 | HTTP_INTERCEPTORS, 6 | HttpClient, 7 | HttpClientModule, 8 | } from '@angular/common/http'; 9 | import { LayoutModule } from '@angular/cdk/layout'; 10 | import { HashLocationStrategy, LocationStrategy } from '@angular/common'; 11 | 12 | import { MatButtonModule } from '@angular/material/button'; 13 | import { MatCardModule } from '@angular/material/card'; 14 | import { MatGridListModule } from '@angular/material/grid-list'; 15 | import { MatNativeDateModule } from '@angular/material/core'; 16 | import { MatIconModule } from '@angular/material/icon'; 17 | import { MatListModule } from '@angular/material/list'; 18 | import { MatMenuModule } from '@angular/material/menu'; 19 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 20 | import { MatSidenavModule } from '@angular/material/sidenav'; 21 | import { MatToolbarModule } from '@angular/material/toolbar'; 22 | 23 | import { AppComponent } from './app.component'; 24 | import { AppRoutingModule } from './app-routing.module'; 25 | import { NavComponent } from './nav/nav.component'; 26 | import { AuthComponent } from './views/auth/auth.component'; 27 | import { AuthConfigModule } from './auth-config.module'; 28 | import { AuthInterceptor } from './auth.interceptor'; 29 | 30 | @NgModule({ 31 | declarations: [AppComponent, NavComponent, AuthComponent], 32 | imports: [ 33 | AppRoutingModule, 34 | AuthConfigModule, 35 | BrowserAnimationsModule, 36 | BrowserModule, 37 | HttpClientModule, 38 | LayoutModule, 39 | MatButtonModule, 40 | MatCardModule, 41 | MatGridListModule, 42 | MatIconModule, 43 | MatListModule, 44 | MatMenuModule, 45 | MatNativeDateModule, 46 | MatProgressSpinnerModule, 47 | MatSidenavModule, 48 | MatToolbarModule, 49 | ], 50 | providers: [ 51 | HttpClientModule, 52 | { 53 | provide: LocationStrategy, 54 | useClass: HashLocationStrategy, 55 | }, 56 | { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, 57 | ], 58 | bootstrap: [AppComponent], 59 | }) 60 | export class AppModule {} 61 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/auth-config.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { AuthModule, LogLevel } from 'angular-auth-oidc-client'; 3 | import { environment } from 'src/environments/environment'; 4 | 5 | @NgModule({ 6 | imports: [ 7 | AuthModule.forRoot({ 8 | config: { 9 | authority: environment.issuer, 10 | authWellknownEndpointUrl: environment.wellKnownEndpointUrl, 11 | clientId: environment.clientId, 12 | logLevel: LogLevel.Debug, 13 | postLogoutRedirectUri: window.location.origin, 14 | redirectUrl: window.location.origin, 15 | responseType: 'code', 16 | scope: 'openid profile email tenant/tenant_read tenant/tenant_write user/user_read user/user_write', 17 | }, 18 | }), 19 | ], 20 | exports: [AuthModule], 21 | }) 22 | export class AuthConfigModule {} 23 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/auth-guard.guard.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivateFn, 5 | RouterStateSnapshot, 6 | } from '@angular/router'; 7 | import { OidcSecurityService } from 'angular-auth-oidc-client'; 8 | import { catchError, map, of } from 'rxjs'; 9 | 10 | export const canActivate: CanActivateFn = ( 11 | route: ActivatedRouteSnapshot, 12 | state: RouterStateSnapshot, 13 | ) => { 14 | const authService = inject(OidcSecurityService); 15 | 16 | return authService.checkAuth().pipe( 17 | map((res) => { 18 | if (!res.isAuthenticated) { 19 | authService.authorize(); 20 | } 21 | return res.isAuthenticated; 22 | }), 23 | catchError(() => { 24 | authService.authorize(); 25 | return of(false); 26 | }), 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Injectable } from '@angular/core'; 6 | import { 7 | HttpRequest, 8 | HttpHandler, 9 | HttpEvent, 10 | HttpInterceptor, 11 | } from '@angular/common/http'; 12 | import { Observable } from 'rxjs'; 13 | import { switchMap } from 'rxjs/operators'; 14 | import { OidcSecurityService } from 'angular-auth-oidc-client'; 15 | import { environment } from 'src/environments/environment'; 16 | 17 | @Injectable() 18 | export class AuthInterceptor implements HttpInterceptor { 19 | constructor(public service: OidcSecurityService) {} 20 | 21 | intercept( 22 | req: HttpRequest, 23 | next: HttpHandler, 24 | ): Observable> { 25 | if ( 26 | req.url.startsWith(environment.issuer) || 27 | req.url.includes('auth-info') 28 | ) { 29 | return next.handle(req); 30 | } 31 | 32 | return this.service.getAccessToken().pipe( 33 | switchMap((tok) => { 34 | req = req.clone({ 35 | headers: req.headers.set('Authorization', 'Bearer ' + tok), 36 | }); 37 | return next.handle(req); 38 | }), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/models/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface INavData { 2 | name?: string; 3 | url?: string | any[]; 4 | href?: string; 5 | icon?: string; 6 | title?: boolean; 7 | children?: INavData[]; 8 | variant?: string; 9 | divider?: boolean; 10 | class?: string; 11 | } 12 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/nav/nav.component.css: -------------------------------------------------------------------------------- 1 | .sidenav-container { 2 | height: 100%; 3 | } 4 | 5 | .sidenav-content-container { 6 | background-color: lightgray; 7 | } 8 | 9 | .sidenav { 10 | width: 200px; 11 | background-color: #2f353a; 12 | } 13 | 14 | .mat-list-item { 15 | color: whitesmoke; 16 | } 17 | 18 | .mat-toolbar.mat-primary { 19 | position: sticky; 20 | top: 0; 21 | z-index: 1; 22 | } 23 | 24 | .spacer { 25 | flex: 1 1 auto; 26 | } 27 | 28 | .material-symbols-outlined { 29 | font-variation-settings: 30 | "FILL" 0, 31 | "wght" 400, 32 | "GRAD" 0, 33 | "opsz" 48; 34 | } 35 | 36 | .logo { 37 | width: 100px; 38 | height: auto; 39 | } 40 | 41 | .nav-icon { 42 | color: #20a8d8; 43 | } 44 | 45 | .spinner-container { 46 | height: 80%; 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | box-sizing: border-box; 51 | } 52 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/nav/nav.component.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 13 | 14 | 15 | {{ 16 | navItem.icon 17 | }} 18 | {{ navItem.name }} 19 | 20 | 21 | 22 | 23 | 24 | 32 | 33 | 40 | 41 | 42 | 43 | 47 | 48 | {{ username$ | async }} 49 | 50 | 54 | 58 | 62 | 63 |
64 | 65 |
66 | 67 |
68 |
69 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/nav/nav.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; 3 | import { Observable, of } from 'rxjs'; 4 | import { filter, map, shareReplay } from 'rxjs/operators'; 5 | import { 6 | NavigationCancel, 7 | NavigationEnd, 8 | NavigationError, 9 | NavigationStart, 10 | Router, 11 | } from '@angular/router'; 12 | 13 | import { navItems } from '../_nav'; 14 | import { 15 | AuthenticatedResult, 16 | OidcSecurityService, 17 | } from 'angular-auth-oidc-client'; 18 | 19 | @Component({ 20 | selector: 'app-nav', 21 | templateUrl: './nav.component.html', 22 | styleUrls: ['./nav.component.css'], 23 | }) 24 | export class NavComponent implements OnInit { 25 | tenantName = ''; 26 | loading$: Observable = of(false); 27 | isAuthenticated$: Observable | undefined; 28 | username$: Observable | undefined; 29 | companyName$: Observable | undefined; 30 | public navItems = navItems; 31 | isHandset$: Observable = this.breakpointObserver 32 | .observe(Breakpoints.Handset) 33 | .pipe( 34 | map((result) => result.matches), 35 | shareReplay(), 36 | ); 37 | 38 | constructor( 39 | private breakpointObserver: BreakpointObserver, 40 | private router: Router, 41 | public oidcSecurityService: OidcSecurityService, 42 | ) { 43 | this.loading$ = this.router.events.pipe( 44 | filter( 45 | (e) => 46 | e instanceof NavigationStart || 47 | e instanceof NavigationEnd || 48 | e instanceof NavigationCancel || 49 | e instanceof NavigationError, 50 | ), 51 | map((e) => e instanceof NavigationStart), 52 | ); 53 | } 54 | 55 | ngOnInit(): void { 56 | this.isAuthenticated$ = this.oidcSecurityService.isAuthenticated$; 57 | } 58 | 59 | async login() { 60 | this.oidcSecurityService.authorize(); 61 | } 62 | 63 | async logout() { 64 | // TODO This isn't working 65 | this.oidcSecurityService.logoffAndRevokeTokens().subscribe(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/auth/auth.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 8 |
9 |
10 |
11 |
12 | 13 | Access Token 14 |
15 |
16 |               
17 |                 {{accessToken$ | async}}
18 |               
19 |             
20 |
21 |
22 |
23 |
24 | 25 | ID Token 26 |
27 |
28 |               
29 |                 {{idToken$ | async}}
30 |               
31 |             
32 |
33 |
34 |
35 |
36 |
37 | 38 |
39 | 40 |
41 |
42 | 43 |

User Data

44 | Is Authenticated: {{ isAuthenticated$ | async }} 45 |
{{ userData$ | async | json }}
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/auth/auth.component.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | * software and associated documentation files (the "Software"), to deal in the Software 6 | * without restriction, including without limitation the rights to use, copy, modify, 7 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | * permit persons to whom the Software is furnished to do so. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | pre, 18 | code { 19 | font-family: monospace, monospace; 20 | } 21 | pre { 22 | white-space: pre-wrap; /* css-3 */ 23 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 24 | white-space: -pre-wrap; /* Opera 4-6 */ 25 | white-space: -o-pre-wrap; /* Opera 7 */ 26 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 27 | overflow: auto; 28 | } 29 | 30 | .card { 31 | margin: 20px; 32 | } 33 | 34 | .card-header { 35 | justify-content: center; 36 | font-size: larger; 37 | font-weight: bold; 38 | } 39 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/auth/auth.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { OidcSecurityService } from 'angular-auth-oidc-client'; 3 | import { from, Observable, pipe } from 'rxjs'; 4 | // import { Auth } from 'aws-amplify'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | @Component({ 8 | templateUrl: './auth.component.html', 9 | styleUrls: ['./auth.component.scss'], 10 | }) 11 | export class AuthComponent implements OnInit { 12 | session$: Observable | undefined; 13 | userData$: Observable | undefined; 14 | isAuthenticated$: Observable | undefined; 15 | checkSessionChanged$: Observable | undefined; 16 | idToken$: Observable | undefined; 17 | accessToken$: Observable | undefined; 18 | checkSessionChanged: any; 19 | 20 | constructor(private service: OidcSecurityService) {} 21 | 22 | ngOnInit(): void { 23 | this.service.getUserData().subscribe((res) => console.log(res)); 24 | this.accessToken$ = this.service.getAccessToken(); 25 | this.idToken$ = this.service.getIdToken(); 26 | this.isAuthenticated$ = this.service.isAuthenticated$.pipe( 27 | map((res) => res.isAuthenticated), 28 | ); 29 | this.userData$ = this.service.getUserData(); 30 | } 31 | 32 | async logout() { 33 | await this.service.logoffLocal(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { Routes, RouterModule } from '@angular/router'; 7 | 8 | import { DashboardComponent } from './dashboard.component'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: DashboardComponent, 14 | data: { 15 | title: 'Dashboard', 16 | }, 17 | }, 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [RouterModule.forChild(routes)], 22 | exports: [RouterModule], 23 | }) 24 | export class DashboardRoutingModule {} 25 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |

Dashboard

6 |
7 |
8 | 9 |
10 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | margin: 20px; 3 | } 4 | 5 | .dashboard-card { 6 | position: absolute; 7 | top: 15px; 8 | left: 15px; 9 | right: 15px; 10 | bottom: 15px; 11 | } 12 | 13 | .more-button { 14 | position: absolute; 15 | top: 5px; 16 | right: 10px; 17 | border: none; 18 | } 19 | 20 | .dashboard-card-content { 21 | text-align: center; 22 | } 23 | 24 | .chart-wrapper { 25 | padding-right: 20px; 26 | position: relative; 27 | margin: auto; 28 | height: 80vh; 29 | width: 80vw; 30 | } 31 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Component, OnInit } from '@angular/core'; 6 | import { TenantsService } from '../tenants/tenants.service'; 7 | 8 | @Component({ 9 | templateUrl: 'dashboard.component.html', 10 | styleUrls: ['./dashboard.component.scss'], 11 | selector: 'app-dashboard', 12 | }) 13 | export class DashboardComponent implements OnInit { 14 | constructor(private tenantSvc: TenantsService) {} 15 | 16 | ngOnInit(): void {} 17 | } 18 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { DashboardComponent } from './dashboard.component'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { MatGridListModule } from '@angular/material/grid-list'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatListModule } from '@angular/material/list'; 9 | import { MatMenuModule } from '@angular/material/menu'; 10 | 11 | import { NgChartsModule } from 'ng2-charts'; 12 | 13 | import { DashboardRoutingModule } from './dashboard-routing.module'; 14 | 15 | @NgModule({ 16 | declarations: [DashboardComponent], 17 | imports: [ 18 | CommonModule, 19 | DashboardRoutingModule, 20 | MatButtonModule, 21 | MatCardModule, 22 | MatGridListModule, 23 | MatIconModule, 24 | MatListModule, 25 | MatMenuModule, 26 | NgChartsModule, 27 | ], 28 | }) 29 | export class DashboardModule {} 30 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/tenants/create/create.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Provision tenant 5 | 6 | 7 | Enter name 8 | 14 | Name is required 15 | Must start with a lowercase, and only lowercase, numbers, and hyphen 16 | 17 | 18 | Enter email 19 | 25 | Email is required 26 | 27 | 28 | Enter tenant tier 29 | 39 | 40 | 41 |
42 | 50 | 58 |
59 |
60 |
61 |
62 |
63 |
64 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/tenants/create/create.component.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | margin: 20px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: flex-start; 6 | width: fit-content; 7 | } 8 | 9 | .product-form { 10 | display: flex; 11 | flex-direction: column; 12 | align-items: flex-start; 13 | } 14 | 15 | .mat-card-content { 16 | width: 100%; 17 | } 18 | 19 | .mat-form-field { 20 | flex-flow: column; 21 | display: flex; 22 | } 23 | 24 | .button-panel { 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | margin-bottom: 8px; 29 | } 30 | 31 | button { 32 | margin: 4px; 33 | } 34 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/tenants/create/create.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormControl, FormGroup, Validators, ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { TenantsService } from '../tenants.service'; 5 | import { v4 as guid } from 'uuid'; 6 | 7 | @Component({ 8 | selector: 'app-create', 9 | templateUrl: './create.component.html', 10 | styleUrls: ['./create.component.scss'], 11 | }) 12 | export class CreateComponent implements OnInit { 13 | submitting = false; 14 | tenantForm = new FormGroup({ 15 | tenantName: new FormControl('', [Validators.required, this.lowercaseAndNumberValidator() ]), 16 | email: new FormControl('', [Validators.email, Validators.required]), 17 | tier: new FormControl('', [Validators.required]), 18 | prices: new FormControl([]) 19 | }); 20 | constructor( 21 | private tenantSvc: TenantsService, 22 | private router: Router, 23 | ) {} 24 | 25 | ngOnInit(): void {} 26 | 27 | submit() { 28 | this.submitting = true; 29 | 30 | const tenant = { 31 | tenantId: guid(), 32 | tenantData: { 33 | ...this.tenantForm.value, 34 | prices: this.tenantForm.value.prices || [], 35 | }, 36 | tenantRegistrationData: { 37 | registrationStatus: "In progress" 38 | } 39 | }; 40 | 41 | // const tenant = { 42 | // ...this.tenantForm.value, 43 | // tenantId: guid(), 44 | // tenantStatus: 'In progress', 45 | // }; 46 | 47 | this.tenantSvc.post(tenant).subscribe({ 48 | next: () => { 49 | this.submitting = false; 50 | this.router.navigate(['tenants']); 51 | }, 52 | error: (err: any) => { 53 | console.error(err); 54 | this.submitting = false; 55 | }, 56 | }); 57 | } 58 | 59 | lowercaseAndNumberValidator(): ValidatorFn { 60 | return (control: AbstractControl): ValidationErrors | null => { 61 | const value = control.value; 62 | if (value && !/^[a-z][a-z0-9-]*$/.test(value)) { 63 | return { lowercaseAndNumbers: 'Must start with a lowercase, and only lowercase, numbers, and hyphen' }; 64 | } 65 | return null; 66 | } 67 | } 68 | 69 | public get tenantName() { 70 | return this.tenantForm.get('tenantName'); 71 | } 72 | 73 | public get email() { 74 | return this.tenantForm.get('email'); 75 | } 76 | 77 | public get tier() { 78 | return this.tenantForm.get('tier'); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/tenants/detail/detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { TenantsService } from '../tenants.service'; 3 | import { Observable, map, of, pipe, switchMap } from 'rxjs'; 4 | import { Tenant } from '../models/tenant'; 5 | import { ActivatedRoute, Router } from '@angular/router'; 6 | 7 | @Component({ 8 | selector: 'app-detail', 9 | templateUrl: './detail.component.html', 10 | }) 11 | export class DetailComponent implements OnInit { 12 | // tenant$: Observable = of(null); 13 | tenant$: Observable = of({ 14 | tenantData: { 15 | tenantName: '', 16 | email: '', 17 | tier: 'basic' 18 | }, 19 | tenantRegistrationData: { 20 | tenantRegistrationId: '', 21 | registrationStatus: 'In progress' 22 | }, 23 | 24 | }); 25 | 26 | constructor( 27 | private tenantsSvc: TenantsService, 28 | private route: ActivatedRoute, 29 | private router: Router, 30 | ) {} 31 | 32 | ngOnInit(): void { 33 | this.tenant$ = this.route.params.pipe( 34 | switchMap(() => this.route.queryParams), 35 | switchMap((queryParams) => { 36 | return this.tenantsSvc.get(queryParams['tenantRegistrationId']).pipe( 37 | map(registrationInfo => ({ 38 | tenantId: this.route.snapshot.params['tenantId'], 39 | tenantRegistrationData: { 40 | tenantRegistrationId: registrationInfo.tenantRegistrationId, 41 | registrationStatus: registrationInfo.registrationStatus 42 | }, 43 | tenantData: { 44 | tenantName: queryParams['tenantName'], 45 | email: queryParams['email'], 46 | tier: queryParams['tier'] 47 | }, 48 | })) 49 | ); 50 | }) 51 | 52 | // map((p) => p['tenantId']), 53 | // switchMap((tenantId) => this.tenantsSvc.get(tenantId)), 54 | // map(tenant => ({ 55 | // ...tenant, 56 | // tenantRegistrationId: this.route.snapshot.queryParams['tenantRegistrationId'] 57 | // })) 58 | ); 59 | } 60 | 61 | delete() { 62 | this.tenant$ 63 | .pipe(switchMap((t) => this.tenantsSvc.delete(t))) 64 | .subscribe((_) => this.router.navigate(['tenants'])); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/tenants/list/list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Tenant List

3 |
4 | 13 | 14 | 25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
Id 33 | 35 | {{ element.tenantId }} 36 | 37 | Name{{ element.tenantName }}E-Mail{{ element.email }}Tier{{ element.tier }}
58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/tenants/list/list.component.scss: -------------------------------------------------------------------------------- 1 | .tenant-list { 2 | margin: 20px; 3 | } 4 | table { 5 | width: 100%; 6 | } 7 | 8 | .button-panel { 9 | margin-top: 20px; 10 | } 11 | 12 | .mat-cell { 13 | margin: 20px 5px; 14 | } 15 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/tenants/list/list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Tenant } from '../models/tenant'; 4 | import { TenantsService } from '../tenants.service'; 5 | 6 | @Component({ 7 | selector: 'app-list', 8 | templateUrl: './list.component.html', 9 | styleUrls: ['./list.component.scss'], 10 | }) 11 | export class ListComponent implements OnInit { 12 | tenants$ = new Observable(); 13 | displayedColumns = [ 14 | 'tenantId', 15 | 'tenantName', 16 | 'email', 17 | 'tier', 18 | // 'tenantStatus', 19 | ]; 20 | constructor(private tenantSvc: TenantsService) {} 21 | 22 | ngOnInit(): void { 23 | this.tenants$ = this.tenantSvc.fetch(); 24 | } 25 | 26 | refresh(): void { 27 | this.ngOnInit(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/tenants/models/tenant.ts: -------------------------------------------------------------------------------- 1 | // export interface Tenant { 2 | // tenantId?: string; 3 | // tenantName?: string | null; 4 | // email?: string | undefined | null; 5 | // tier?: string | undefined | null; 6 | // tenantStatus?: string; 7 | // isActive?: boolean; 8 | // [key: string]: any; 9 | // } 10 | 11 | interface Price { 12 | id: string; 13 | metricName: string; 14 | } 15 | 16 | interface TenantData { 17 | tenantName?: string | null; 18 | email?: string | undefined | null; 19 | tier?: string | undefined | null; 20 | prices?: Price[]; 21 | } 22 | 23 | export interface TenantRegistrationData { 24 | tenantRegistrationId?: string; 25 | registrationStatus?: string; 26 | } 27 | 28 | export interface Tenant { 29 | tenantId?: string; 30 | tenantData: TenantData; 31 | tenantRegistrationData: TenantRegistrationData; 32 | } -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/tenants/tenants-routing.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { Routes, RouterModule } from '@angular/router'; 7 | import { CreateComponent } from './create/create.component'; 8 | 9 | import { ListComponent } from './list/list.component'; 10 | import { DetailComponent } from './detail/detail.component'; 11 | 12 | const routes: Routes = [ 13 | { 14 | path: '', 15 | redirectTo: 'list', 16 | pathMatch: 'full', 17 | }, 18 | { 19 | path: 'list', 20 | component: ListComponent, 21 | data: { 22 | title: 'Tenant List', 23 | }, 24 | }, 25 | { 26 | path: 'create', 27 | component: CreateComponent, 28 | data: { 29 | title: 'Provision new tenant', 30 | }, 31 | }, 32 | { 33 | path: 'detail/:tenantId', 34 | component: DetailComponent, 35 | data: { 36 | title: 'Tenant Detail', 37 | }, 38 | }, 39 | ]; 40 | 41 | @NgModule({ 42 | imports: [RouterModule.forChild(routes)], 43 | exports: [RouterModule], 44 | }) 45 | export class TenantsRoutingModule {} 46 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/tenants/tenants.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatCardModule } from '@angular/material/card'; 7 | import { MatDatepickerModule } from '@angular/material/datepicker'; 8 | import { MatDialogModule } from '@angular/material/dialog'; 9 | import { 10 | MatFormFieldModule, 11 | MAT_FORM_FIELD_DEFAULT_OPTIONS, 12 | } from '@angular/material/form-field'; 13 | import { MatIconModule } from '@angular/material/icon'; 14 | import { MatInputModule } from '@angular/material/input'; 15 | import { MatRadioModule } from '@angular/material/radio'; 16 | import { MatSelectModule } from '@angular/material/select'; 17 | import { MatTableModule } from '@angular/material/table'; 18 | 19 | import { DetailComponent } from './detail/detail.component'; 20 | import { ListComponent } from './list/list.component'; 21 | import { TenantsRoutingModule } from './tenants-routing.module'; 22 | import { CreateComponent } from './create/create.component'; 23 | import { FlexLayoutModule } from '@angular/flex-layout'; 24 | 25 | @NgModule({ 26 | declarations: [DetailComponent, ListComponent, CreateComponent], 27 | imports: [ 28 | CommonModule, 29 | FlexLayoutModule, 30 | TenantsRoutingModule, 31 | MatButtonModule, 32 | MatCardModule, 33 | MatDatepickerModule, 34 | MatDialogModule, 35 | MatFormFieldModule, 36 | MatIconModule, 37 | MatInputModule, 38 | MatRadioModule, 39 | MatSelectModule, 40 | MatTableModule, 41 | ReactiveFormsModule, 42 | ], 43 | providers: [ 44 | { 45 | provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, 46 | useValue: { appearance: 'outline' }, 47 | }, 48 | ], 49 | }) 50 | export class TenantsModule {} 51 | -------------------------------------------------------------------------------- /client/AdminWeb/src/app/views/tenants/tenants.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { HttpClient } from '@angular/common/http'; 6 | import { Injectable } from '@angular/core'; 7 | import { Observable } from 'rxjs'; 8 | import { environment } from 'src/environments/environment'; 9 | import { map } from 'rxjs/operators'; 10 | 11 | import { Tenant, TenantRegistrationData } from './models/tenant'; 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class TenantsService { 17 | constructor(private http: HttpClient) {} 18 | baseUrl = `${environment.apiUrl}`; 19 | tenantsApiUrl = `${this.baseUrl}/tenant-registrations`; 20 | tenantsMgmtApiUrl = `${this.baseUrl}/tenants`; 21 | 22 | fetch(): Observable { 23 | return this.http 24 | .get(this.tenantsMgmtApiUrl)//.get(this.tenantsApiUrl) 25 | .pipe(map((response: any) => response.data)); 26 | } 27 | 28 | post(tenant: Tenant): Observable { 29 | return this.http.post(this.tenantsApiUrl, tenant); 30 | } 31 | 32 | tenantUrl = (id: any) => { 33 | return `${this.tenantsApiUrl}/${id}`; 34 | }; 35 | 36 | tenantMgmtUrl = (id: any) => { 37 | return `${this.tenantsMgmtApiUrl}/${id}`; 38 | }; 39 | 40 | get(id: string): Observable { 41 | return this.http 42 | .get(this.tenantUrl(id)) 43 | .pipe(map((response: any) => response.data)); 44 | } 45 | 46 | // get(id: string): Observable { 47 | // return this.http 48 | // .get(this.tenantMgmtUrl(id)) 49 | // .pipe(map((response: any) => response.data)); 50 | // } 51 | // delete(tenant: Tenant): Observable { 52 | // return this.http.delete(this.tenantUrl(tenant.tenantId), { 53 | // body: tenant, 54 | // }); 55 | // } 56 | 57 | delete(tenant: any): Observable { 58 | return this.http.delete(this.tenantUrl(tenant.tenantRegistrationData.tenantRegistrationId), { 59 | body: tenant, 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /client/AdminWeb/src/custom-theme.scss: -------------------------------------------------------------------------------- 1 | // Custom Theming for Angular Material 2 | // For more information: https://material.angular.io/guide/theming 3 | @use "@angular/material" as mat; 4 | // Plus imports for other components in your app. 5 | 6 | // Include the common styles for Angular Material. We include this here so that you only 7 | // have to load a single css file for Angular Material in your app. 8 | // Be sure that you only ever include this mixin once! 9 | @include mat.core(); 10 | 11 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 12 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 13 | // hue. Available color palettes: https://material.io/design/color/ 14 | $dashboard-primary: mat.define-palette(mat.$indigo-palette); 15 | $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); 16 | 17 | // The warn palette is optional (defaults to red). 18 | $dashboard-warn: mat.define-palette(mat.$red-palette); 19 | 20 | // Create the theme object. A theme consists of configurations for individual 21 | // theming systems such as "color" or "typography". 22 | $dashboard-theme: mat.define-light-theme( 23 | ( 24 | color: ( 25 | primary: $dashboard-primary, 26 | accent: $dashboard-accent, 27 | warn: $dashboard-warn, 28 | ), 29 | ) 30 | ); 31 | 32 | // Include theme styles for core and each component used in your app. 33 | // Alternatively, you can import and @include the theme mixins for each component 34 | // that you are using. 35 | @include mat.all-component-themes($dashboard-theme); 36 | 37 | html, 38 | body { 39 | height: 100%; 40 | } 41 | body { 42 | margin: 0; 43 | font-family: Roboto, "Helvetica Neue", sans-serif; 44 | } 45 | -------------------------------------------------------------------------------- /client/AdminWeb/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | clientId: '', 8 | issuer: '', 9 | apiUrl: '', 10 | wellKnownEndpointUrl: '', 11 | }; 12 | /* 13 | * For easier debugging in development mode, you can import the following file 14 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 15 | * 16 | * This import should be commented out in production mode because it will have a negative impact 17 | * on performance if an error is thrown. 18 | */ 19 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 20 | -------------------------------------------------------------------------------- /client/AdminWeb/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | clientId: '', 8 | issuer: '', 9 | apiUrl: '', 10 | wellKnownEndpointUrl: '', 11 | }; 12 | /* 13 | * For easier debugging in development mode, you can import the following file 14 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 15 | * 16 | * This import should be commented out in production mode because it will have a negative impact 17 | * on performance if an error is thrown. 18 | */ 19 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 20 | -------------------------------------------------------------------------------- /client/AdminWeb/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | Dashboard 13 | 14 | 15 | 16 | 17 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/AdminWeb/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /client/AdminWeb/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /client/AdminWeb/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); 3 | 4 | @import "styles/variables"; 5 | // Custom.scss 6 | // Option A: Include all of Bootstrap 7 | 8 | // Include any default variable overrides here (though functions won't be available) 9 | 10 | @import "../node_modules/bootstrap/scss/bootstrap"; 11 | 12 | // Then add additional custom code here 13 | // Import functions, variables, and mixins needed by other Bootstrap files 14 | @import "styles/reset"; 15 | -------------------------------------------------------------------------------- /client/AdminWeb/src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | a { 2 | &.mat-button, 3 | &.mat-raised-button, 4 | &.mat-fab, 5 | &.mat-mini-fab, 6 | &.mat-list-item { 7 | &:hover { 8 | color: currentColor; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/AdminWeb/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /client/AdminWeb/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "es2020", 20 | "module": "es2020", 21 | "strictPropertyInitialization": false, 22 | "lib": ["es2020", "dom"] 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/Application/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /client/Application/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /client/Application/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development" 9 | }, 10 | "private": true, 11 | "dependencies": { 12 | "@angular/animations": "~14.0.0", 13 | "@angular/cdk": "~14.0.4", 14 | "@angular/common": "~14.0.0", 15 | "@angular/compiler": "~14.0.0", 16 | "@angular/core": "~14.0.0", 17 | "@angular/forms": "~14.0.0", 18 | "@angular/material": "~14.0.4", 19 | "@angular/platform-browser": "~14.0.0", 20 | "@angular/platform-browser-dynamic": "~14.0.0", 21 | "@angular/router": "~14.0.0", 22 | "@aws-amplify/ui-angular": "~2.4.14", 23 | "aws-amplify": "~4.3.27", 24 | "bootstrap": "~5.2.0", 25 | "chart.js": "~3.8.0", 26 | "chartjs-plugin-datalabels": "~2.0.0", 27 | "ng2-charts": "~4.0.0", 28 | "rxjs": "~7.5.0", 29 | "tslib": "~2.3.0", 30 | "zone.js": "~0.11.4" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "~14.2.13", 34 | "@angular/cli": "~14.0.5", 35 | "@angular/compiler-cli": "~14.0.0", 36 | "@types/jasmine": "~4.0.0", 37 | "cypress": "^12.5.1", 38 | "typescript": "~4.7.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/Application/src/app/_nav.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { INavData } from './models'; 6 | 7 | export const navItems: INavData[] = [ 8 | { 9 | name: 'Dashboard', 10 | url: '/dashboard', 11 | icon: 'insights', 12 | }, 13 | { 14 | name: 'Products', 15 | url: '/products', 16 | icon: 'sell', 17 | }, 18 | { 19 | name: 'Orders', 20 | url: '/orders', 21 | icon: 'shopping_cart', 22 | }, 23 | { 24 | name: 'Users', 25 | url: '/users', 26 | icon: 'supervisor_account', 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /client/Application/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { NavComponent } from './nav/nav.component'; 4 | import { AuthComponent } from './views/auth/auth.component'; 5 | import { UnauthorizedComponent } from './views/error/unauthorized.component'; 6 | import { CognitoGuard } from './cognito.guard'; 7 | 8 | export const routes: Routes = [ 9 | { 10 | path: '', 11 | redirectTo: 'unauthorized', 12 | pathMatch: 'full', 13 | }, 14 | { 15 | path: '', 16 | component: NavComponent, 17 | canActivate: [CognitoGuard], 18 | data: { 19 | title: 'Home', 20 | }, 21 | children: [ 22 | { 23 | path: 'auth/info', 24 | component: AuthComponent, 25 | }, 26 | { 27 | path: 'dashboard', 28 | loadChildren: () => 29 | import('./views/dashboard/dashboard.module').then( 30 | (m) => m.DashboardModule, 31 | ), 32 | }, 33 | { 34 | path: 'orders', 35 | loadChildren: () => 36 | import('./views/orders/orders.module').then((m) => m.OrdersModule), 37 | }, 38 | { 39 | path: 'products', 40 | loadChildren: () => 41 | import('./views/products/products.module').then( 42 | (m) => m.ProductsModule, 43 | ), 44 | }, 45 | { 46 | path: 'users', 47 | loadChildren: () => 48 | import('./views/users/users.module').then((m) => m.UsersModule), 49 | }, 50 | ], 51 | }, 52 | { 53 | path: 'unauthorized', 54 | component: UnauthorizedComponent, 55 | }, 56 | ]; 57 | 58 | @NgModule({ 59 | imports: [RouterModule.forRoot(routes)], 60 | exports: [RouterModule], 61 | }) 62 | export class AppRoutingModule {} 63 | -------------------------------------------------------------------------------- /client/Application/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | // styles 2 | -------------------------------------------------------------------------------- /client/Application/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatIconRegistry } from '@angular/material/icon'; 3 | import { DomSanitizer } from '@angular/platform-browser'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | template: ` `, 8 | styleUrls: ['./app.component.scss'], 9 | }) 10 | export class AppComponent { 11 | constructor( 12 | private matIconRegistry: MatIconRegistry, 13 | private domSanitizer: DomSanitizer, 14 | ) { 15 | this.matIconRegistry.addSvgIcon( 16 | 'saas-commerce', 17 | this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg'), 18 | ); 19 | } 20 | title = 'application'; 21 | } 22 | -------------------------------------------------------------------------------- /client/Application/src/app/cognito.guard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Injectable } from '@angular/core'; 6 | import { 7 | ActivatedRouteSnapshot, 8 | CanActivate, 9 | Router, 10 | RouterStateSnapshot, 11 | } from '@angular/router'; 12 | import { Auth } from 'aws-amplify'; 13 | import { AuthConfigurationService } from './views/auth/auth-configuration.service'; 14 | 15 | @Injectable({ providedIn: 'root' }) 16 | export class CognitoGuard implements CanActivate { 17 | constructor( 18 | private router: Router, 19 | private authConfigService: AuthConfigurationService, 20 | ) {} 21 | 22 | canActivate( 23 | route: ActivatedRouteSnapshot, 24 | state: RouterStateSnapshot, 25 | ): Promise { 26 | if (!this.authConfigService.configureAmplifyAuth()) { 27 | this.authConfigService.cleanLocalStorage(); 28 | this.router.navigate(['/unauthorized']); 29 | return new Promise((res, rej) => { 30 | res(false); 31 | }); 32 | } 33 | 34 | return Auth.currentSession() 35 | .then((u) => { 36 | if (u.isValid()) { 37 | return true; 38 | } else { 39 | this.authConfigService.cleanLocalStorage(); 40 | this.router.navigate(['/unauthorized']); 41 | return false; 42 | } 43 | }) 44 | .catch((e) => { 45 | if (state.url === '/dashboard') { 46 | // if we're going to the dashboard and we're not logged in, 47 | // don't stop the flow as the amplify-authenticator will 48 | // route requests going to the dashboard to the sign-in page. 49 | return true; 50 | } 51 | 52 | console.log('Error getting current session', e); 53 | this.router.navigate(['/unauthorized']); 54 | return false; 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/Application/src/app/interceptors/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Injectable } from '@angular/core'; 6 | import { 7 | HttpRequest, 8 | HttpHandler, 9 | HttpEvent, 10 | HttpInterceptor, 11 | } from '@angular/common/http'; 12 | import { from, Observable } from 'rxjs'; 13 | import { Auth } from 'aws-amplify'; 14 | import { filter, map, switchMap } from 'rxjs/operators'; 15 | 16 | @Injectable() 17 | export class AuthInterceptor implements HttpInterceptor { 18 | constructor() {} 19 | idToken = ''; 20 | 21 | intercept( 22 | req: HttpRequest, 23 | next: HttpHandler, 24 | ): Observable> { 25 | if (req.url.includes('tenant-config')) { 26 | return next.handle(req); 27 | } 28 | 29 | const s = Auth.currentSession().catch((err) => console.log(err)); 30 | const session$ = from(s); 31 | 32 | return session$.pipe( 33 | filter((sesh) => !!sesh), 34 | map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), 35 | switchMap((tok) => { 36 | req = req.clone({ 37 | headers: req.headers.set('Authorization', 'Bearer ' + tok), 38 | }); 39 | return next.handle(req); 40 | }), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/Application/src/app/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 6 | 7 | import { AuthInterceptor } from './auth.interceptor'; 8 | 9 | /** Http interceptor providers in outside-in order */ 10 | export const httpInterceptorProviders = [ 11 | { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, 12 | ]; 13 | -------------------------------------------------------------------------------- /client/Application/src/app/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | -------------------------------------------------------------------------------- /client/Application/src/app/models/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface INavData { 2 | name?: string; 3 | url?: string | any[]; 4 | href?: string; 5 | icon?: string; 6 | title?: boolean; 7 | children?: INavData[]; 8 | variant?: string; 9 | divider?: boolean; 10 | class?: string; 11 | } 12 | -------------------------------------------------------------------------------- /client/Application/src/app/nav/nav.component.scss: -------------------------------------------------------------------------------- 1 | .sidenav { 2 | width: 200px; 3 | background-color: #2f353a; 4 | } 5 | 6 | .mat-list-item { 7 | color: whitesmoke; 8 | } 9 | 10 | .sidebar-icon-container { 11 | height: 64px; 12 | background-color: whitesmoke; 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | } 17 | 18 | .spacer { 19 | flex: 1 1 auto; 20 | } 21 | 22 | .material-symbols-outlined { 23 | font-variation-settings: 24 | "FILL" 0, 25 | "wght" 400, 26 | "GRAD" 0, 27 | "opsz" 48; 28 | } 29 | 30 | .logo { 31 | width: 100px; 32 | height: auto; 33 | } 34 | 35 | .nav-icon { 36 | color: #20a8d8; 37 | } 38 | 39 | .footer { 40 | position: fixed; 41 | bottom: 0; 42 | width: 100%; 43 | height: 40px; 44 | background-color: whitesmoke; 45 | color: #2d3337; 46 | // text-align: center; 47 | margin: 0px; 48 | } 49 | 50 | .footer-text { 51 | display: flex; 52 | } 53 | 54 | .content { 55 | position: absolute; 56 | width: 100%; 57 | height: 90%; 58 | max-height: 90%; 59 | overflow: auto; 60 | background-color: lightgray; 61 | } 62 | -------------------------------------------------------------------------------- /client/Application/src/app/views/auth/auth.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 8 |
9 |
10 |
11 |
12 | 13 | Access Token 14 |
15 |
16 |               
17 |                 {{accessToken$ | async}}
18 |               
19 |             
20 |
21 |
22 |
23 |
24 | 25 | ID Token 26 |
27 |
28 |               
29 |                 {{idToken$ | async}}
30 |               
31 |             
32 |
33 |
34 |
35 |
36 |
37 | 38 |
39 | 40 |
41 |
42 | 43 |

User Data

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

Dashboard

3 | 4 | 9 | 10 | 11 | 12 | {{ card.title }} 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /client/Application/src/app/views/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | margin: 20px; 3 | } 4 | 5 | .dashboard-card { 6 | position: absolute; 7 | top: 15px; 8 | left: 15px; 9 | right: 15px; 10 | bottom: 15px; 11 | } 12 | 13 | .more-button { 14 | position: absolute; 15 | top: 5px; 16 | right: 10px; 17 | border: none; 18 | } 19 | 20 | .dashboard-card-content { 21 | text-align: center; 22 | } 23 | -------------------------------------------------------------------------------- /client/Application/src/app/views/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { DashboardComponent } from './dashboard.component'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { MatGridListModule } from '@angular/material/grid-list'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatListModule } from '@angular/material/list'; 9 | import { MatMenuModule } from '@angular/material/menu'; 10 | 11 | import { NgChartsModule } from 'ng2-charts'; 12 | 13 | import { DashboardRoutingModule } from './dashboard-routing.module'; 14 | 15 | @NgModule({ 16 | declarations: [DashboardComponent], 17 | imports: [ 18 | CommonModule, 19 | DashboardRoutingModule, 20 | MatButtonModule, 21 | MatCardModule, 22 | MatGridListModule, 23 | MatIconModule, 24 | MatListModule, 25 | MatMenuModule, 26 | NgChartsModule, 27 | ], 28 | }) 29 | export class DashboardModule {} 30 | -------------------------------------------------------------------------------- /client/Application/src/app/views/error/404.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

404

7 |

Oops! You're lost.

8 |

The page you are looking for was not found.

9 |
10 |
11 |
12 | 13 |
14 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /client/Application/src/app/views/error/404.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Component } from '@angular/core'; 6 | 7 | @Component({ 8 | templateUrl: '404.component.html', 9 | }) 10 | export class P404Component { 11 | constructor() {} 12 | } 13 | -------------------------------------------------------------------------------- /client/Application/src/app/views/error/500.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

500

7 |

Houston, we have a problem!

8 |

9 | The page you are looking for is temporarily unavailable. 10 |

11 |
12 |
13 |
14 | 15 |
16 | 23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /client/Application/src/app/views/error/500.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Component } from '@angular/core'; 6 | 7 | @Component({ 8 | templateUrl: '500.component.html', 9 | }) 10 | export class P500Component { 11 | constructor() {} 12 | } 13 | -------------------------------------------------------------------------------- /client/Application/src/app/views/error/unauthorized.component.html: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 |
7 | 8 | Unauthorized 9 | Enter your tenant name and click submit below 12 | 13 | 14 | Tenant Name 15 | 26 | home 27 | 28 | 29 |
30 | 38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /client/Application/src/app/views/error/unauthorized.component.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | * software and associated documentation files (the "Software"), to deal in the Software 6 | * without restriction, including without limitation the rights to use, copy, modify, 7 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | * permit persons to whom the Software is furnished to do so. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | .center-screen { 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | align-items: center; 22 | text-align: center; 23 | min-height: 100vh; 24 | } 25 | -------------------------------------------------------------------------------- /client/Application/src/app/views/orders/create/create.component.scss: -------------------------------------------------------------------------------- 1 | // .card { 2 | // margin: 20px; 3 | // display: flex; 4 | // flex-direction: column; 5 | // align-items: flex-start; 6 | // } 7 | 8 | .order-form { 9 | margin: 20px; 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | 14 | .mat-card-title { 15 | display: flex; 16 | justify-content: center; 17 | } 18 | 19 | .button-panel { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | margin-bottom: 8px; 24 | } 25 | 26 | .row { 27 | width: 100%; 28 | align-items: center; 29 | } 30 | 31 | button { 32 | margin: 4px; 33 | } 34 | -------------------------------------------------------------------------------- /client/Application/src/app/views/orders/detail/detail.component.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | * software and associated documentation files (the "Software"), to deal in the Software 6 | * without restriction, including without limitation the rights to use, copy, modify, 7 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | * permit persons to whom the Software is furnished to do so. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | .no-bottom-margin { 18 | margin-bottom: 0; 19 | } 20 | 21 | .nowrap { 22 | white-space: nowrap; 23 | } 24 | 25 | .invoice { 26 | font-family: Arial, Helvetica, sans-serif; 27 | width: 970px !important; 28 | margin: 50px auto; 29 | .invoice-header { 30 | padding: 25px 25px 15px; 31 | h1 { 32 | margin: 0; 33 | } 34 | .media { 35 | .media-body { 36 | font-size: 0.9em; 37 | margin: 0; 38 | } 39 | } 40 | } 41 | .invoice-body { 42 | border-radius: 10px; 43 | padding: 25px; 44 | background: #fff; 45 | } 46 | .invoice-footer { 47 | padding: 15px; 48 | font-size: 0.9em; 49 | text-align: center; 50 | color: #999; 51 | } 52 | } 53 | .logo { 54 | max-height: 70px; 55 | border-radius: 10px; 56 | } 57 | .dl-horizontal { 58 | margin: 0; 59 | dt { 60 | float: left; 61 | width: 80px; 62 | overflow: hidden; 63 | clear: left; 64 | text-align: right; 65 | text-overflow: ellipsis; 66 | white-space: nowrap; 67 | } 68 | dd { 69 | margin-left: 90px; 70 | } 71 | } 72 | .rowamount { 73 | padding-top: 15px !important; 74 | } 75 | .rowtotal { 76 | font-size: 1.3em; 77 | } 78 | .colfix { 79 | width: 12%; 80 | } 81 | .mono { 82 | font-family: monospace; 83 | } 84 | -------------------------------------------------------------------------------- /client/Application/src/app/views/orders/detail/detail.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Component, OnInit } from '@angular/core'; 6 | import { ActivatedRoute } from '@angular/router'; 7 | import { Observable } from 'rxjs'; 8 | import { map, switchMap } from 'rxjs/operators'; 9 | import { Order } from '../models/order.interface'; 10 | import { OrderProduct } from '../models/orderproduct.interface'; 11 | import { OrdersService } from '../orders.service'; 12 | @Component({ 13 | selector: 'app-detail', 14 | templateUrl: './detail.component.html', 15 | styleUrls: ['./detail.component.scss'], 16 | }) 17 | export class DetailComponent implements OnInit { 18 | orderId$: Observable | undefined; 19 | order$: Observable | undefined; 20 | orderProducts$: Observable | undefined; 21 | taxRate = 0.0899; 22 | constructor( 23 | private route: ActivatedRoute, 24 | private orderSvc: OrdersService, 25 | ) {} 26 | 27 | ngOnInit(): void { 28 | this.orderId$ = this.route.params.pipe(map((o) => o['orderId'])); 29 | this.order$ = this.orderId$.pipe(switchMap((o) => this.orderSvc.get(o))); 30 | this.orderProducts$ = this.order$.pipe(map((o) => o.orderProducts)); 31 | } 32 | 33 | today() { 34 | return new Date(); 35 | } 36 | 37 | tenantName() { 38 | return ''; 39 | } 40 | 41 | sum(op: OrderProduct) { 42 | return op.price * op.quantity; 43 | } 44 | 45 | tax(op: OrderProduct) { 46 | return this.sum(op) * this.taxRate; 47 | } 48 | 49 | total(op: OrderProduct) { 50 | return this.sum(op) + this.tax(op); 51 | } 52 | 53 | subTotal(order: Order) { 54 | return order.orderProducts 55 | .map((op) => op.price * op.quantity) 56 | .reduce((acc, curr) => acc + curr); 57 | } 58 | 59 | calcTax(order: Order) { 60 | return this.subTotal(order) * this.taxRate; 61 | } 62 | 63 | final(order: Order) { 64 | return this.subTotal(order) + this.calcTax(order); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client/Application/src/app/views/orders/list/list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Order List

3 |
4 |
5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 |
Name 10 | 16 | {{ element.orderName }} 17 | 18 | Line Items 24 | {{ element.orderProducts?.length }} 25 | Total 31 | {{ sum(element) | currency }} 32 |
38 | 42 | 47 | 48 | 49 |
50 | 58 |
59 |
60 |
61 |
62 | -------------------------------------------------------------------------------- /client/Application/src/app/views/orders/list/list.component.scss: -------------------------------------------------------------------------------- 1 | .order-list { 2 | margin: 20px; 3 | } 4 | table { 5 | width: 100%; 6 | } 7 | 8 | .button-panel { 9 | margin-top: 20px; 10 | } 11 | -------------------------------------------------------------------------------- /client/Application/src/app/views/orders/list/list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Order } from '../models/order.interface'; 4 | import { OrdersService } from '../orders.service'; 5 | 6 | @Component({ 7 | selector: 'app-list', 8 | templateUrl: './list.component.html', 9 | styleUrls: ['./list.component.scss'], 10 | }) 11 | export class ListComponent implements OnInit { 12 | displayedColumns: string[] = ['name', 'lineItems', 'total']; 13 | orderData: Order[] = []; 14 | isLoading: boolean = true; 15 | constructor( 16 | private orderSvc: OrdersService, 17 | private router: Router, 18 | ) {} 19 | 20 | ngOnInit(): void { 21 | this.orderSvc.fetch().subscribe((data) => { 22 | this.isLoading = false; 23 | this.orderData = data; 24 | }); 25 | } 26 | 27 | sum(order: Order): number { 28 | return order.orderProducts 29 | .map((p) => p.price * p.quantity) 30 | .reduce((acc, curr) => acc + curr); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/Application/src/app/views/orders/models/order.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { OrderProduct } from './orderproduct.interface'; 6 | 7 | export interface Order { 8 | key: string; 9 | tenantId: string; 10 | orderId: string; 11 | orderName: string; 12 | orderProducts: OrderProduct[]; 13 | } 14 | -------------------------------------------------------------------------------- /client/Application/src/app/views/orders/models/orderproduct.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export interface OrderProduct { 6 | productId: string; 7 | price: number; 8 | quantity: number; 9 | } 10 | -------------------------------------------------------------------------------- /client/Application/src/app/views/orders/orders-routing.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { Routes, RouterModule } from '@angular/router'; 7 | import { CreateComponent } from './create/create.component'; 8 | import { DetailComponent } from './detail/detail.component'; 9 | import { ListComponent } from './list/list.component'; 10 | 11 | const routes: Routes = [ 12 | { 13 | path: '', 14 | redirectTo: 'list', 15 | pathMatch: 'full', 16 | }, 17 | { 18 | path: 'list', 19 | data: { 20 | title: 'All Orders', 21 | }, 22 | component: ListComponent, 23 | }, 24 | { 25 | path: 'create', 26 | data: { 27 | title: 'Create Order', 28 | }, 29 | component: CreateComponent, 30 | }, 31 | { 32 | path: 'detail/:orderId', 33 | data: { 34 | title: 'View Order Detail', 35 | }, 36 | component: DetailComponent, 37 | }, 38 | ]; 39 | 40 | @NgModule({ 41 | imports: [RouterModule.forChild(routes)], 42 | exports: [RouterModule], 43 | }) 44 | export class OrdersRoutingModule {} 45 | -------------------------------------------------------------------------------- /client/Application/src/app/views/orders/orders.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatCardModule } from '@angular/material/card'; 7 | import { MatDatepickerModule } from '@angular/material/datepicker'; 8 | import { MatDialogModule } from '@angular/material/dialog'; 9 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 10 | 11 | import { 12 | MatFormFieldModule, 13 | MAT_FORM_FIELD_DEFAULT_OPTIONS, 14 | } from '@angular/material/form-field'; 15 | import { MatIconModule } from '@angular/material/icon'; 16 | import { MatInputModule } from '@angular/material/input'; 17 | import { MatRadioModule } from '@angular/material/radio'; 18 | import { MatSelectModule } from '@angular/material/select'; 19 | import { MatTableModule } from '@angular/material/table'; 20 | 21 | import { CreateComponent } from './create/create.component'; 22 | import { DetailComponent } from './detail/detail.component'; 23 | import { ListComponent } from './list/list.component'; 24 | import { OrdersRoutingModule } from './orders-routing.module'; 25 | 26 | @NgModule({ 27 | declarations: [CreateComponent, ListComponent, DetailComponent], 28 | imports: [ 29 | CommonModule, 30 | OrdersRoutingModule, 31 | ReactiveFormsModule, 32 | MatButtonModule, 33 | MatCardModule, 34 | MatDatepickerModule, 35 | MatDialogModule, 36 | MatFormFieldModule, 37 | MatIconModule, 38 | MatInputModule, 39 | MatRadioModule, 40 | MatSelectModule, 41 | MatTableModule, 42 | MatProgressSpinnerModule, 43 | ], 44 | providers: [ 45 | { 46 | provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, 47 | useValue: { appearance: 'outline' }, 48 | }, 49 | ], 50 | }) 51 | export class OrdersModule {} 52 | -------------------------------------------------------------------------------- /client/Application/src/app/views/orders/orders.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { HttpClient } from '@angular/common/http'; 6 | import { Injectable } from '@angular/core'; 7 | import { Observable, of } from 'rxjs'; 8 | import { Order } from './models/order.interface'; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class OrdersService { 14 | orders: Order[] = []; 15 | baseUrl = `${localStorage.getItem('apiGatewayUrl')}/orders`; 16 | constructor(private http: HttpClient) {} 17 | 18 | fetch(): Observable { 19 | return this.http.get(`${this.baseUrl}`); 20 | } 21 | 22 | get(orderId: string): Observable { 23 | const url = `${this.baseUrl}/${orderId}`; 24 | return this.http.get(url); 25 | } 26 | 27 | create(order: Order): Observable { 28 | return this.http.post(`${this.baseUrl}`, order); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/create/create.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Create new Product 5 | 6 | 7 | Enter product name 8 | 14 | Name is required 15 | 16 | 17 | Enter product price 18 | 25 | Price is required 26 | 27 | 28 | SKU 29 | 35 | 36 | 37 | Category 38 | 39 | 40 | {{ category }} 41 | 42 | 43 | 44 | 45 |
46 | 54 | 57 |
58 |
59 |
60 |
61 |
62 |
63 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/create/create.component.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | margin: 20px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: flex-start; 6 | } 7 | 8 | .product-form { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: flex-start; 12 | } 13 | 14 | .mat-form-field { 15 | display: flex; 16 | } 17 | 18 | .button-panel { 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | margin-bottom: 8px; 23 | } 24 | 25 | button { 26 | margin: 4px; 27 | } 28 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/create/create.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | 5 | import { ProductService } from '../product.service'; 6 | 7 | @Component({ 8 | selector: 'app-create', 9 | templateUrl: './create.component.html', 10 | styleUrls: ['./create.component.scss'], 11 | }) 12 | export class CreateComponent implements OnInit { 13 | productForm: FormGroup; 14 | categories: string[] = ['category1', 'category2', 'category3', 'category4']; 15 | constructor( 16 | private fb: FormBuilder, 17 | private productSvc: ProductService, 18 | private router: Router, 19 | ) { 20 | this.productForm = this.fb.group({}); 21 | } 22 | 23 | ngOnInit(): void { 24 | this.productForm = this.fb.group({ 25 | name: ['', Validators.required], 26 | price: ['', Validators.required], 27 | sku: '', 28 | category: '', 29 | }); 30 | } 31 | 32 | get name() { 33 | return this.productForm.get('name'); 34 | } 35 | 36 | get price() { 37 | return this.productForm.get('price'); 38 | } 39 | 40 | submit() { 41 | this.productSvc.post(this.productForm.value).subscribe({ 42 | next: () => this.router.navigate(['products']), 43 | error: (err) => { 44 | alert(err.message); 45 | console.error(err); 46 | }, 47 | }); 48 | } 49 | 50 | cancel() { 51 | this.router.navigate(['products']); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/edit/edit.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Edit Product 5 | 6 | 7 | Product ID 8 | 9 | 10 | 11 | Enter product name 12 | 18 | Name is required 19 | 20 | 21 | Enter product price 22 | 29 | Price is required 30 | 31 | 32 | SKU 33 | 39 | 40 | 41 | Category 42 | 43 | 44 | {{ category }} 45 | 46 | 47 | 48 | 49 |
50 | 58 | 61 |
62 |
63 |
64 |
65 |
66 |
67 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/edit/edit.component.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | margin: 20px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: flex-start; 6 | } 7 | 8 | .product-form { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: flex-start; 12 | } 13 | 14 | .mat-form-field { 15 | display: flex; 16 | } 17 | 18 | .button-panel { 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | margin-bottom: 8px; 23 | } 24 | 25 | button { 26 | margin: 4px; 27 | } 28 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/edit/edit.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Component, OnInit } from '@angular/core'; 6 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 7 | import { ActivatedRoute, Router } from '@angular/router'; 8 | import { Observable, of } from 'rxjs'; 9 | import { map, switchMap } from 'rxjs/operators'; 10 | import { Product } from '../models/product.interface'; 11 | import { ProductService } from '../product.service'; 12 | 13 | @Component({ 14 | selector: 'app-edit', 15 | templateUrl: './edit.component.html', 16 | styleUrls: ['./edit.component.scss'], 17 | }) 18 | export class EditComponent implements OnInit { 19 | productForm: FormGroup; 20 | categories: string[] = ['category1', 'category2', 'category3', 'category4']; 21 | product$: Observable | undefined; 22 | productId$: Observable | undefined; 23 | productName$: Observable | undefined; 24 | 25 | constructor( 26 | private fb: FormBuilder, 27 | private productSvc: ProductService, 28 | private router: Router, 29 | private route: ActivatedRoute, 30 | ) { 31 | this.productForm = this.fb.group({}); 32 | } 33 | 34 | ngOnInit(): void { 35 | this.productForm = this.fb.group({ 36 | tenantId: [], 37 | productId: [], 38 | name: ['', Validators.required], 39 | price: ['', Validators.required], 40 | sku: '', 41 | category: '', 42 | }); 43 | 44 | this.productId$ = this.route.params.pipe(map((p) => p['productId'])); 45 | this.product$ = this.productId$.pipe( 46 | switchMap((p) => this.productSvc.get(p)), 47 | ); 48 | this.productName$ = this.product$.pipe(map((p) => p?.name)); 49 | 50 | this.product$.subscribe((val) => { 51 | this.productForm?.patchValue({ 52 | ...val, 53 | }); 54 | }); 55 | } 56 | 57 | get name() { 58 | return this.productForm?.get('name'); 59 | } 60 | 61 | get price() { 62 | return this.productForm?.get('price'); 63 | } 64 | 65 | submit() { 66 | this.productSvc.put(this.productForm?.value).subscribe({ 67 | next: () => this.router.navigate(['products']), 68 | error: (err) => console.error(err), 69 | }); 70 | } 71 | 72 | delete() { 73 | this.productSvc.delete(this.productForm?.value).subscribe({ 74 | next: () => this.router.navigate(['products']), 75 | error: (err) => { 76 | alert(err); 77 | console.error(err); 78 | }, 79 | }); 80 | } 81 | 82 | cancel() { 83 | this.router.navigate(['products']); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/list/list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Product List

3 |
4 |
5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
Name 10 | 16 | {{ element.name }} 17 | 18 | Price 25 | {{ element.price | currency }} 26 | SKU{{ element.sku }}
38 | 42 | 47 | 48 | 49 |
50 | 53 |
54 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/list/list.component.scss: -------------------------------------------------------------------------------- 1 | .product-list { 2 | margin: 20px; 3 | } 4 | table { 5 | width: 100%; 6 | } 7 | 8 | .button-panel { 9 | margin-top: 20px; 10 | } 11 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/list/list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Product } from '../models/product.interface'; 4 | import { ProductService } from '../product.service'; 5 | 6 | @Component({ 7 | selector: 'app-list', 8 | templateUrl: './list.component.html', 9 | styleUrls: ['./list.component.scss'], 10 | }) 11 | export class ListComponent implements OnInit { 12 | productData: Product[] = []; 13 | isLoading: boolean = true; 14 | displayedColumns: string[] = ['name', 'price', 'sku']; 15 | 16 | constructor( 17 | private productSvc: ProductService, 18 | private router: Router, 19 | ) {} 20 | 21 | ngOnInit(): void { 22 | this.productSvc.fetch().subscribe((data) => { 23 | this.productData = data; 24 | this.isLoading = false; 25 | }); 26 | } 27 | 28 | onEdit(product: Product) { 29 | this.router.navigate(['products', 'edit', product.productId]); 30 | return false; 31 | } 32 | 33 | onRemove(product: Product) { 34 | this.productSvc.delete(product); 35 | this.isLoading = true; 36 | this.productSvc.fetch().subscribe((data) => { 37 | this.productData = data; 38 | this.isLoading = false; 39 | }); 40 | } 41 | 42 | onCreate() { 43 | this.router.navigate(['products', 'create']); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/models/product.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export interface Product { 6 | key: string; 7 | tenantId: string; 8 | productId: string; 9 | name: string; 10 | price: number; 11 | sku: string; 12 | category: string; 13 | pictureUrl?: string; 14 | } 15 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/product.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { HttpClient } from '@angular/common/http'; 6 | import { Injectable } from '@angular/core'; 7 | import { Observable, of } from 'rxjs'; 8 | import { Product } from './models/product.interface'; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class ProductService { 14 | constructor(private http: HttpClient) {} 15 | baseUrl = `${localStorage.getItem('apiGatewayUrl')}/products`; 16 | 17 | fetch(): Observable { 18 | return this.http.get(`${this.baseUrl}`); 19 | } 20 | 21 | get(productId: string): Observable { 22 | const url = `${this.baseUrl}/${productId}`; 23 | return this.http.get(url); 24 | } 25 | 26 | delete(product: Product) { 27 | const url = `${this.baseUrl}/${product.tenantId}:${product.productId}`; 28 | return this.http.delete(url); 29 | } 30 | 31 | put(product: Product) { 32 | const url = `${this.baseUrl}/${product.tenantId}:${product.productId}`; 33 | return this.http.put(url, product); 34 | } 35 | 36 | post(product: Product) { 37 | return this.http.post(`${this.baseUrl}`, product); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/products-routing.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { Routes, RouterModule } from '@angular/router'; 7 | import { CreateComponent } from './create/create.component'; 8 | import { EditComponent } from './edit/edit.component'; 9 | import { ListComponent } from './list/list.component'; 10 | 11 | const routes: Routes = [ 12 | { 13 | path: '', 14 | redirectTo: 'list', 15 | pathMatch: 'prefix', 16 | }, 17 | { 18 | path: 'list', 19 | data: { 20 | title: 'Product List', 21 | }, 22 | component: ListComponent, 23 | }, 24 | { 25 | path: 'create', 26 | data: { 27 | title: 'Create new Product', 28 | }, 29 | component: CreateComponent, 30 | }, 31 | { 32 | path: 'edit/:productId', 33 | data: { 34 | title: 'Edit Product', 35 | }, 36 | component: EditComponent, 37 | }, 38 | ]; 39 | 40 | @NgModule({ 41 | imports: [RouterModule.forChild(routes)], 42 | exports: [RouterModule], 43 | }) 44 | export class ProductsRoutingModule {} 45 | -------------------------------------------------------------------------------- /client/Application/src/app/views/products/products.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { MatCardModule } from '@angular/material/card'; 5 | import { MatDatepickerModule } from '@angular/material/datepicker'; 6 | import { 7 | MatFormFieldModule, 8 | MAT_FORM_FIELD_DEFAULT_OPTIONS, 9 | } from '@angular/material/form-field'; 10 | import { MatButtonModule } from '@angular/material/button'; 11 | import { MatInputModule } from '@angular/material/input'; 12 | import { MatSelectModule } from '@angular/material/select'; 13 | import { MatRadioModule } from '@angular/material/radio'; 14 | import { MatDialogModule } from '@angular/material/dialog'; 15 | import { MatTableModule } from '@angular/material/table'; 16 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 17 | 18 | import { ProductsRoutingModule } from './products-routing.module'; 19 | import { CreateComponent } from './create/create.component'; 20 | import { EditComponent } from './edit/edit.component'; 21 | import { ListComponent } from './list/list.component'; 22 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 23 | 24 | @NgModule({ 25 | declarations: [CreateComponent, EditComponent, ListComponent], 26 | imports: [ 27 | CommonModule, 28 | ReactiveFormsModule, 29 | ProductsRoutingModule, 30 | MatButtonModule, 31 | MatCardModule, 32 | MatDatepickerModule, 33 | MatDialogModule, 34 | MatFormFieldModule, 35 | MatInputModule, 36 | MatRadioModule, 37 | MatSelectModule, 38 | MatTableModule, 39 | MatProgressSpinnerModule, 40 | ], 41 | providers: [ 42 | { 43 | provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, 44 | useValue: { appearance: 'outline' }, 45 | }, 46 | ], 47 | }) 48 | export class ProductsModule {} 49 | -------------------------------------------------------------------------------- /client/Application/src/app/views/users/create/create.component.scss: -------------------------------------------------------------------------------- 1 | .user-form { 2 | margin: 20px; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .mat-card-title { 8 | display: flex; 9 | justify-content: center; 10 | } 11 | 12 | .button-panel { 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | margin-bottom: 8px; 17 | } 18 | 19 | .row { 20 | width: 100%; 21 | align-items: center; 22 | } 23 | 24 | button { 25 | margin: 4px; 26 | } 27 | -------------------------------------------------------------------------------- /client/Application/src/app/views/users/create/create.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Component, OnInit } from '@angular/core'; 6 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 7 | import { UsersService } from '../users.service'; 8 | import { MatSnackBar } from '@angular/material/snack-bar'; 9 | 10 | @Component({ 11 | selector: 'app-create', 12 | templateUrl: './create.component.html', 13 | styleUrls: ['./create.component.scss'], 14 | }) 15 | export class CreateComponent implements OnInit { 16 | userForm: FormGroup; 17 | error: boolean = false; 18 | success: boolean = false; 19 | 20 | constructor( 21 | private fb: FormBuilder, 22 | private userSvc: UsersService, 23 | private _snackBar: MatSnackBar, 24 | ) { 25 | this.userForm = this.fb.group({ 26 | userName: [null, [Validators.required]], 27 | userEmail: [null, [Validators.email, Validators.required]], 28 | userRole: [null, [Validators.required]], 29 | }); 30 | } 31 | 32 | ngOnInit(): void {} 33 | 34 | openErrorMessageSnackBar(errorMessage: string) { 35 | this._snackBar.open(errorMessage, 'Dismiss', { 36 | duration: 4 * 1000, // seconds 37 | }); 38 | } 39 | 40 | onSubmit() { 41 | const user = this.userForm.value; 42 | this.userSvc.create(user).subscribe( 43 | () => { 44 | this.success = true; 45 | this.openErrorMessageSnackBar('Successfully created new user!'); 46 | }, 47 | (err) => { 48 | this.error = true; 49 | this.openErrorMessageSnackBar('An unexpected error occurred!'); 50 | }, 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/Application/src/app/views/users/list/list.component.scss: -------------------------------------------------------------------------------- 1 | .user-list { 2 | margin: 20px; 3 | } 4 | table { 5 | width: 100%; 6 | } 7 | 8 | .button-panel { 9 | margin-top: 20px; 10 | } 11 | 12 | .mat-cell { 13 | margin: 20px 5px; 14 | } 15 | -------------------------------------------------------------------------------- /client/Application/src/app/views/users/list/list.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Component, OnInit } from '@angular/core'; 6 | import { User } from '../models/user'; 7 | import { UsersService } from '../users.service'; 8 | 9 | @Component({ 10 | selector: 'app-user', 11 | templateUrl: './list.component.html', 12 | styleUrls: ['./list.component.scss'], 13 | }) 14 | export class ListComponent implements OnInit { 15 | userData: User[] = []; 16 | isLoading: boolean = true; 17 | displayedColumns: string[] = [ 18 | 'email', 19 | 'created', 20 | 'modified', 21 | 'status', 22 | 'enabled', 23 | ]; 24 | 25 | constructor(private userSvc: UsersService) {} 26 | 27 | ngOnInit(): void { 28 | this.userSvc.fetch().subscribe((data) => { 29 | this.userData = data; 30 | this.isLoading = false; 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/Application/src/app/views/users/models/user.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export interface User { 6 | email: string; 7 | created?: string; 8 | modified?: string; 9 | enabled?: boolean; 10 | status?: string; 11 | verified?: boolean; 12 | role?: string; 13 | username?: string; 14 | } 15 | -------------------------------------------------------------------------------- /client/Application/src/app/views/users/users-routing.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { Routes, RouterModule } from '@angular/router'; 7 | import { CreateComponent } from './create/create.component'; 8 | import { ListComponent } from './list/list.component'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | redirectTo: 'list', 14 | pathMatch: 'prefix', 15 | }, 16 | { 17 | path: 'list', 18 | data: { 19 | title: 'All Users', 20 | }, 21 | component: ListComponent, 22 | }, 23 | { 24 | path: 'create', 25 | data: { 26 | title: 'Create User', 27 | }, 28 | component: CreateComponent, 29 | }, 30 | ]; 31 | 32 | @NgModule({ 33 | imports: [RouterModule.forChild(routes)], 34 | exports: [RouterModule], 35 | }) 36 | export class UsersRoutingModule {} 37 | -------------------------------------------------------------------------------- /client/Application/src/app/views/users/users.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { CommonModule } from '@angular/common'; 7 | 8 | import { UsersRoutingModule } from './users-routing.module'; 9 | import { ListComponent } from './list/list.component'; 10 | import { CreateComponent } from './create/create.component'; 11 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 12 | import { MatCardModule } from '@angular/material/card'; 13 | import { MatDatepickerModule } from '@angular/material/datepicker'; 14 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 15 | 16 | import { 17 | MatFormFieldModule, 18 | MAT_FORM_FIELD_DEFAULT_OPTIONS, 19 | } from '@angular/material/form-field'; 20 | import { MatButtonModule } from '@angular/material/button'; 21 | import { MatInputModule } from '@angular/material/input'; 22 | import { MatSelectModule } from '@angular/material/select'; 23 | import { MatRadioModule } from '@angular/material/radio'; 24 | import { MatDialogModule } from '@angular/material/dialog'; 25 | import { MatTableModule } from '@angular/material/table'; 26 | import { MatIconModule } from '@angular/material/icon'; 27 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 28 | 29 | @NgModule({ 30 | declarations: [ListComponent, CreateComponent], 31 | imports: [ 32 | CommonModule, 33 | UsersRoutingModule, 34 | FormsModule, 35 | ReactiveFormsModule, 36 | MatButtonModule, 37 | MatCardModule, 38 | MatDatepickerModule, 39 | MatDialogModule, 40 | MatFormFieldModule, 41 | MatInputModule, 42 | MatRadioModule, 43 | MatSelectModule, 44 | MatTableModule, 45 | MatIconModule, 46 | MatProgressSpinnerModule, 47 | MatSnackBarModule, 48 | ], 49 | }) 50 | export class UsersModule {} 51 | -------------------------------------------------------------------------------- /client/Application/src/app/views/users/users.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | * software and associated documentation files (the "Software"), to deal in the Software 6 | * without restriction, including without limitation the rights to use, copy, modify, 7 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | * permit persons to whom the Software is furnished to do so. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 18 | import { Injectable } from '@angular/core'; 19 | import { Observable, of } from 'rxjs'; 20 | import { find, mergeMap, defaultIfEmpty } from 'rxjs/operators'; 21 | import { User } from './models/user'; 22 | import { environment } from '../../../environments/environment'; 23 | 24 | @Injectable({ 25 | providedIn: 'root', 26 | }) 27 | export class UsersService { 28 | constructor(private http: HttpClient) {} 29 | baseUrl = `${localStorage.getItem('apiGatewayUrl')}/users`; 30 | 31 | fetch(): Observable { 32 | return this.http.get(`${this.baseUrl}`); 33 | } 34 | 35 | create(user: User): Observable { 36 | return this.http.post(`${this.baseUrl}`, user); 37 | } 38 | 39 | update(email: string, user: User) {} 40 | } 41 | -------------------------------------------------------------------------------- /client/Application/src/custom-theme.scss: -------------------------------------------------------------------------------- 1 | // Custom Theming for Angular Material 2 | // For more information: https://material.angular.io/guide/theming 3 | @use "@angular/material" as mat; 4 | // Plus imports for other components in your app. 5 | 6 | // Include the common styles for Angular Material. We include this here so that you only 7 | // have to load a single css file for Angular Material in your app. 8 | // Be sure that you only ever include this mixin once! 9 | @include mat.core(); 10 | 11 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 12 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 13 | // hue. Available color palettes: https://material.io/design/color/ 14 | $dashboard-primary: mat.define-palette(mat.$indigo-palette); 15 | $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); 16 | 17 | // The warn palette is optional (defaults to red). 18 | $dashboard-warn: mat.define-palette(mat.$red-palette); 19 | 20 | // Create the theme object. A theme consists of configurations for individual 21 | // theming systems such as "color" or "typography". 22 | $dashboard-theme: mat.define-light-theme( 23 | ( 24 | color: ( 25 | primary: $dashboard-primary, 26 | accent: $dashboard-accent, 27 | warn: $dashboard-warn, 28 | ), 29 | ) 30 | ); 31 | 32 | // Include theme styles for core and each component used in your app. 33 | // Alternatively, you can import and @include the theme mixins for each component 34 | // that you are using. 35 | @include mat.all-component-themes($dashboard-theme); 36 | 37 | html, 38 | body { 39 | height: 100%; 40 | } 41 | body { 42 | margin: 0; 43 | font-family: Roboto, "Helvetica Neue", sans-serif; 44 | } 45 | -------------------------------------------------------------------------------- /client/Application/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | apiUrl: 'https://4zp9zi5kgl.execute-api.us-west-2.amazonaws.com/prod/', 4 | }; 5 | -------------------------------------------------------------------------------- /client/Application/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | apiUrl: 'https://4zp9zi5kgl.execute-api.us-west-2.amazonaws.com/prod/', 4 | }; 5 | -------------------------------------------------------------------------------- /client/Application/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/saas-reference-architecture-ecs/861e71270b0fc9876600a4bd7f0a4c79b4a2fd68/client/Application/src/favicon.ico -------------------------------------------------------------------------------- /client/Application/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | Application 13 | 14 | 15 | 16 | 17 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/Application/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /client/Application/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /client/Application/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); 3 | @import "~@aws-amplify/ui-angular/theme.css"; 4 | 5 | @import "~bootstrap/scss/functions"; 6 | @import "styles/variables"; 7 | 8 | // Import functions, variables, and mixins needed by other Bootstrap files 9 | @import "~bootstrap/scss/variables"; 10 | @import "~bootstrap/scss/maps"; 11 | @import "~bootstrap/scss/mixins"; 12 | 13 | // Import Bootstrap Reboot 14 | @import "~bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files 15 | @import "~bootstrap/scss/reboot"; 16 | 17 | @import "~bootstrap/scss/containers"; // Add .container and .container-fluid classes 18 | @import "~bootstrap/scss/grid"; // Add the grid system 19 | 20 | @import "~bootstrap/scss/utilities"; // Configures the utility classes that should be generated 21 | @import "~bootstrap/scss/utilities/api"; // Generates the actual utility classes 22 | 23 | @import "styles/reset"; 24 | 25 | .chart-container canvas { 26 | max-height: 250px; 27 | width: auto; 28 | } 29 | 30 | .chart-container { 31 | height: 100%; 32 | display: flex; 33 | flex-direction: column; 34 | align-items: center; 35 | justify-content: center; 36 | } 37 | -------------------------------------------------------------------------------- /client/Application/src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $link-color: #673ab7; // 1 2 | $label-margin-bottom: 0; // 2 3 | $grid-breakpoints: ( 4 | xs: 0, 5 | // handset portrait (small, medium, large) | handset landscape (small) 6 | sm: 600px, 7 | // handset landscape (medium, large) | tablet portrait (small, large) 8 | md: 960px, 9 | // tablet landscape (small, large) 10 | lg: 1280px, 11 | // laptops and desktops 12 | xl: 1600px // large desktops,,, 13 | ); 14 | 15 | $container-max-widths: ( 16 | sm: 600px, 17 | md: 960px, 18 | lg: 1280px, 19 | xl: 1600px, 20 | ); 21 | -------------------------------------------------------------------------------- /client/Application/src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | a { 2 | &.mat-button, 3 | &.mat-raised-button, 4 | &.mat-fab, 5 | &.mat-mini-fab, 6 | &.mat-list-item { 7 | &:hover { 8 | color: currentColor; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/Application/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /client/Application/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "es2020", 20 | "module": "es2020", 21 | "strictPropertyInitialization": false, 22 | "lib": ["es2020", "dom"] 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /images/advanced-tier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/saas-reference-architecture-ecs/861e71270b0fc9876600a4bd7f0a4c79b4a2fd68/images/advanced-tier.png -------------------------------------------------------------------------------- /images/archi-base-infra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/saas-reference-architecture-ecs/861e71270b0fc9876600a4bd7f0a4c79b4a2fd68/images/archi-base-infra.png -------------------------------------------------------------------------------- /images/archi-high-level.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/saas-reference-architecture-ecs/861e71270b0fc9876600a4bd7f0a4c79b4a2fd68/images/archi-high-level.png -------------------------------------------------------------------------------- /images/basic-tier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/saas-reference-architecture-ecs/861e71270b0fc9876600a4bd7f0a4c79b4a2fd68/images/basic-tier.png -------------------------------------------------------------------------------- /images/premium-tier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/saas-reference-architecture-ecs/861e71270b0fc9876600a4bd7f0a4c79b4a2fd68/images/premium-tier.png -------------------------------------------------------------------------------- /images/routing-parallel-albs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/saas-reference-architecture-ecs/861e71270b0fc9876600a4bd7f0a4c79b4a2fd68/images/routing-parallel-albs.png -------------------------------------------------------------------------------- /images/routing-premium-tier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/saas-reference-architecture-ecs/861e71270b0fc9876600a4bd7f0a4c79b4a2fd68/images/routing-premium-tier.png -------------------------------------------------------------------------------- /images/service-connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/saas-reference-architecture-ecs/861e71270b0fc9876600a4bd7f0a4c79b4a2fd68/images/service-connect.png -------------------------------------------------------------------------------- /images/solution-architecture-tiers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/saas-reference-architecture-ecs/861e71270b0fc9876600a4bd7f0a4c79b4a2fd68/images/solution-architecture-tiers.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@aws-sdk/client-lambda": "^3.656.0", 4 | "@aws-sdk/client-rds-data": "^3.654.0" 5 | }, 6 | "devDependencies": { 7 | "@types/aws-lambda": "^8.10.145" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/del-secrets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get Secrets from AWS Secrets Manager 4 | SECRET_IDS=$(aws secretsmanager list-secrets \ 5 | --query "SecretList[?contains(Name, 'rds_proxy_multitenant')].ARN" \ 6 | --output text) 7 | 8 | # Check if there are any secrets 9 | if [ -z "$SECRET_IDS" ]; then 10 | echo "No secrets found." 11 | exit 0 12 | fi 13 | 14 | echo "The following secrets will be deleted:" 15 | echo "$SECRET_IDS" 16 | 17 | # Delete secrets 18 | for SECRET_ID in $SECRET_IDS; do 19 | echo "Deleting secret: $SECRET_ID" 20 | aws secretsmanager delete-secret --secret-id "$SECRET_ID" --force-delete-without-recovery | cat 21 | done 22 | 23 | echo "Deletion complete." -------------------------------------------------------------------------------- /scripts/get-adv-network.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export IMAGE_NAME="$1" 4 | export TENANT="$2" 5 | 6 | if [ "$#" -ne 2 ]; then 7 | echo "Usage: $0 " 8 | exit 1 9 | fi 10 | 11 | SERVICE_NAME="${IMAGE_NAME}${TENANT}" 12 | 13 | CLUSTER_NAME=$(aws ecs list-clusters --query 'clusterArns[*]' --output json | jq -r '.[] | select(contains("/prod-advanced-")) | split("/") | .[1]') 14 | 15 | # Step 1: Get the Task ARN 16 | TASK_ARN=$(aws ecs list-tasks --cluster $CLUSTER_NAME --service-name $SERVICE_NAME --query 'taskArns[0]' --output text) 17 | TASK_ID=$(echo "$TASK_ARN" | awk -F'/' '{print $NF}' ) 18 | # Check if TASK_ARN is empty 19 | if [ -z "$TASK_ARN" ]; then 20 | echo "No tasks found for service $SERVICE_NAME in cluster $CLUSTER_NAME" 21 | exit 1 22 | fi 23 | 24 | export SECURITY_GROUP=$(aws ecs describe-services --cluster $CLUSTER_NAME --services $SERVICE_NAME --query 'services[0].networkConfiguration.awsvpcConfiguration.securityGroups' --output text) 25 | 26 | # Step 2: Get the ENI ID 27 | export PRIVATE_IP=$(aws ecs describe-tasks --cluster $CLUSTER_NAME --tasks $TASK_ARN --query 'tasks[0].attachments[?type==`ElasticNetworkInterface`][].details[?name==`privateIPv4Address`].value | [0]' --output text) 28 | 29 | # Output the Private IP 30 | echo "CLUSTER_NAME : $CLUSTER_NAME" 31 | echo "TASK ID : $TASK_ID" 32 | echo "Security Group: $SECURITY_GROUP" 33 | echo "Private IP : $PRIVATE_IP" 34 | echo "curl http://$PRIVATE_IP:3010/${IMAGE_NAME}" 35 | -------------------------------------------------------------------------------- /scripts/resize-cloud9.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | 4 | # Specify the desired volume size in GiB as a command line argument. If not specified, default to 20 GiB. 5 | SIZE=${1:-50} 6 | 7 | # Get the ID of the environment host Amazon EC2 instance. 8 | TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60") 9 | INSTANCEID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/instance-id 2> /dev/null) 10 | REGION=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/placement/region 2> /dev/null) 11 | 12 | # Get the ID of the Amazon EBS volume associated with the instance. 13 | VOLUMEID=$(aws ec2 describe-instances \ 14 | --instance-id $INSTANCEID \ 15 | --query "Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId" \ 16 | --output text \ 17 | --region $REGION) 18 | 19 | # Resize the EBS volume. 20 | aws ec2 modify-volume --volume-id $VOLUMEID --size $SIZE 21 | 22 | # Wait for the resize to finish. 23 | while [ \ 24 | "$(aws ec2 describe-volumes-modifications \ 25 | --volume-id $VOLUMEID \ 26 | --filters Name=modification-state,Values="optimizing","completed" \ 27 | --query "length(VolumesModifications)"\ 28 | --output text)" != "1" ]; do 29 | sleep 1 30 | done 31 | 32 | # Check if we're on an NVMe filesystem 33 | if [[ -e "/dev/xvda" && $(readlink -f /dev/xvda) = "/dev/xvda" ]] 34 | then 35 | # Rewrite the partition table so that the partition takes up all the space that it can. 36 | sudo growpart /dev/xvda 1 37 | # Expand the size of the file system. 38 | # Check if we're on AL2 or AL2023 39 | STR=$(cat /etc/os-release) 40 | SUBAL2="VERSION_ID=\"2\"" 41 | SUBAL2023="VERSION_ID=\"2023\"" 42 | if [[ "$STR" == *"$SUBAL2"* || "$STR" == *"$SUBAL2023"* ]] 43 | then 44 | sudo xfs_growfs -d / 45 | else 46 | sudo resize2fs /dev/xvda1 47 | fi 48 | 49 | else 50 | # Rewrite the partition table so that the partition takes up all the space that it can. 51 | sudo growpart /dev/nvme0n1 1 52 | 53 | # Expand the size of the file system. 54 | # Check if we're on AL2 or AL2023 55 | STR=$(cat /etc/os-release) 56 | SUBAL2="VERSION_ID=\"2\"" 57 | SUBAL2023="VERSION_ID=\"2023\"" 58 | if [[ "$STR" == *"$SUBAL2"* || "$STR" == *"$SUBAL2023"* ]] 59 | then 60 | sudo xfs_growfs -d / 61 | else 62 | sudo resize2fs /dev/nvme0n1p1 63 | fi 64 | fi 65 | -------------------------------------------------------------------------------- /scripts/sbt-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | export CDK_PARAM_SYSTEM_ADMIN_EMAIL="$1" 4 | 5 | if [[ -z "$CDK_PARAM_SYSTEM_ADMIN_EMAIL" ]]; then 6 | echo "Please provide system admin email" 7 | exit 1 8 | fi 9 | 10 | REGION=$(aws ec2 describe-availability-zones --output text --query 'AvailabilityZones[0].[RegionName]') # Region setting 11 | ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) 12 | 13 | export CDK_PARAM_S3_BUCKET_NAME="saas-reference-architecture-ecs-$ACCOUNT_ID-$REGION" 14 | CDK_SOURCE_NAME="source.zip" 15 | 16 | VERSIONS=$(aws s3api list-object-versions --bucket "$CDK_PARAM_S3_BUCKET_NAME" --prefix "$CDK_SOURCE_NAME" --query 'Versions[?IsLatest==`true`].{VersionId:VersionId}' --output text 2>&1) 17 | export CDK_PARAM_COMMIT_ID=$(echo "$VERSIONS" | awk 'NR==1{print $1}') 18 | 19 | cd ../server 20 | 21 | # npx cdk bootstrap 22 | export CDK_PARAM_TIER='basic' 23 | RDS_RESOURCES=$(aws cloudformation describe-stack-resources --stack-name 'shared-infra-stack' --query "StackResources[?ResourceType=='AWS::RDS::DBInstance']" --output text) 24 | if [ -z "$RDS_RESOURCES" ] 25 | then 26 | export CDK_USE_DB='dynamodb' 27 | else 28 | export CDK_USE_DB='mysql' 29 | fi 30 | echo "DB_TYPE:$CDK_USE_DB" 31 | 32 | #npx cdk deploy --all --require-approval=never 33 | npx cdk deploy \ 34 | controlplane-stack \ 35 | core-appplane-stack --require-approval=any-change 36 | 37 | # Get SaaS application url 38 | ADMIN_SITE_URL=$(aws cloudformation describe-stacks --stack-name shared-infra-stack --query "Stacks[0].Outputs[?OutputKey=='adminSiteUrl'].OutputValue" --output text) 39 | APP_SITE_URL=$(aws cloudformation describe-stacks --stack-name shared-infra-stack --query "Stacks[0].Outputs[?OutputKey=='appSiteUrl'].OutputValue" --output text) 40 | echo "Admin site url: $ADMIN_SITE_URL" 41 | echo "Application site url: $APP_SITE_URL" -------------------------------------------------------------------------------- /scripts/update-provision-source.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | 4 | REGION=$(aws ec2 describe-availability-zones --output text --query 'AvailabilityZones[0].[RegionName]') # Region setting 5 | ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) 6 | 7 | # Create S3 Bucket for provision source. 8 | export CDK_PARAM_S3_BUCKET_NAME="saas-reference-architecture-ecs-$ACCOUNT_ID-$REGION" 9 | 10 | if aws s3api head-bucket --bucket $CDK_PARAM_S3_BUCKET_NAME >/dev/null 2>&1; then 11 | echo "Bucket $CDK_PARAM_S3_BUCKET_NAME already exists." 12 | else 13 | echo "Bucket $CDK_PARAM_S3_BUCKET_NAME does not exist. Creating a new bucket in $REGION region in $ACCOUNT_ID" 14 | 15 | if [ "$REGION" == "us-east-1" ]; then 16 | aws s3api create-bucket --bucket $CDK_PARAM_S3_BUCKET_NAME | cat 17 | else 18 | aws s3api create-bucket \ 19 | --bucket $CDK_PARAM_S3_BUCKET_NAME \ 20 | --region "$REGION" \ 21 | --create-bucket-configuration LocationConstraint="$REGION" | cat 22 | fi 23 | 24 | aws s3api put-bucket-versioning \ 25 | --bucket $CDK_PARAM_S3_BUCKET_NAME \ 26 | --versioning-configuration Status=Enabled 27 | 28 | aws s3api put-public-access-block \ 29 | --bucket $CDK_PARAM_S3_BUCKET_NAME \ 30 | --public-access-block-configuration \ 31 | BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true 32 | 33 | if [ $? -eq 0 ]; then 34 | echo "Bucket $CDK_PARAM_S3_BUCKET_NAME created with versioning enabled." 35 | else 36 | echo "Error creating bucket $CDK_PARAM_S3_BUCKET_NAME with versioning enabled." 37 | exit 1 38 | fi 39 | fi 40 | 41 | echo "Bucket exists: $CDK_PARAM_S3_BUCKET_NAME" 42 | 43 | cd ../ 44 | zip -rq source.zip . -x ".git/*" -x "**/node_modules/*" -x "**/cdk.out/*" -x "**/.aws-sam/*" 45 | export CDK_PARAM_COMMIT_ID=$(aws s3api put-object --bucket "$CDK_PARAM_S3_BUCKET_NAME" --key "source.zip" --body "./source.zip" | jq -r '.VersionId') 46 | echo $CDK_PARAM_COMMIT_ID 47 | rm source.zip 48 | cd ./scripts -------------------------------------------------------------------------------- /scripts/update-tenants.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | # todo: consider setting these env-vars in CDK 4 | export CDK_PARAM_CONTROL_PLANE_SOURCE='sbt-control-plane-api' 5 | export CDK_PARAM_ONBOARDING_DETAIL_TYPE='Onboarding' 6 | export CDK_PARAM_PROVISIONING_DETAIL_TYPE=$CDK_PARAM_ONBOARDING_DETAIL_TYPE 7 | export CDK_PARAM_APPLICATION_NAME_PLANE_SOURCE="sbt-application-plane-api" 8 | export CDK_PARAM_OFFBOARDING_DETAIL_TYPE='Offboarding' 9 | export CDK_PARAM_DEPROVISIONING_DETAIL_TYPE=$CDK_PARAM_OFFBOARDING_DETAIL_TYPE 10 | export CDK_PARAM_PROVISIONING_EVENT_SOURCE="sbt-application-plane-api" 11 | 12 | cd server 13 | npm install 14 | 15 | # Define the DynamoDB table name and initial parameters 16 | page_size=10 17 | query_parameters="" 18 | 19 | while true; do 20 | scan_result=$(aws dynamodb scan --table-name $tenantMappingTableName --max-items $page_size $query_parameters) 21 | 22 | items=$(echo $scan_result | jq '.Items') 23 | 24 | for item in $(echo "$items" | jq -c '.[]'); do 25 | echo "Item:" 26 | echo "$item" | jq . 27 | COMMIT_ID=$(echo "$item" | jq -r '.codeCommitId.S') 28 | 29 | if [ "$COMMIT_ID" == "$CDK_PARAM_COMMIT_ID" ]; then 30 | echo "already updated" 31 | else 32 | STACK_NAME=$(echo "$item" | jq -r '.stackName.S') 33 | export CDK_PARAM_TENANT_ID=$(echo "$item" | jq -r '.tenantId.S') 34 | npx cdk deploy $STACK_NAME --require-approval never 35 | fi 36 | 37 | done 38 | 39 | next_token=$(echo $scan_result | jq -r '.NextToken') 40 | 41 | if [ "$next_token" == "null" ]; then 42 | break 43 | fi 44 | 45 | query_parameters="--starting-token $next_token" 46 | done 47 | -------------------------------------------------------------------------------- /server/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /server/application/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /server/application/Dockerfile.order: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/bitnami/node:20 AS build 2 | WORKDIR /app 3 | COPY package.json ./ 4 | COPY yarn.lock ./ 5 | RUN yarn 6 | COPY . . 7 | RUN yarn build 8 | 9 | FROM public.ecr.aws/bitnami/node:20 10 | WORKDIR /app 11 | COPY --from=build /app ./ 12 | EXPOSE 3010 13 | CMD ["npm", "run", "start:order"] 14 | -------------------------------------------------------------------------------- /server/application/Dockerfile.product: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/bitnami/node:20 AS build 2 | WORKDIR /app 3 | COPY package.json ./ 4 | # COPY yarn.lock ./ 5 | RUN yarn 6 | COPY . . 7 | RUN yarn build product 8 | 9 | FROM public.ecr.aws/bitnami/node:20 10 | WORKDIR /app 11 | COPY --from=build /app ./ 12 | 13 | ########################WORKSHOP-TEST############################## 14 | # RUN apt-get update && \ 15 | # apt-get install -y \ 16 | # unzip \ 17 | # && curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" \ 18 | # && unzip awscliv2.zip \ 19 | # && ./aws/install \ 20 | # && rm -rf awscliv2.zip 21 | ########################WORKSHOP-TEST############################## 22 | 23 | EXPOSE 3010 24 | CMD ["npm", "run", "start:product"] 25 | -------------------------------------------------------------------------------- /server/application/Dockerfile.rproxy: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/nginx/nginx:mainline-alpine AS build 2 | 3 | COPY ./reverseproxy/index.html /etc/nginx/html/index.html 4 | COPY ./reverseproxy/nginx.template /etc/nginx/nginx.template 5 | 6 | RUN chmod 644 /etc/nginx/html/index.html /etc/nginx/nginx.template 7 | 8 | ENTRYPOINT [ "sh", "-c", "envsubst '${NAMESPACE}' < /etc/nginx/nginx.template > /etc/nginx/nginx.conf && nginx -g 'daemon off;'" ] 9 | 10 | EXPOSE 80 -------------------------------------------------------------------------------- /server/application/Dockerfile.user: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/bitnami/node:20 AS build 2 | WORKDIR /app 3 | COPY package.json ./ 4 | COPY yarn.lock ./ 5 | RUN yarn 6 | COPY . . 7 | RUN yarn build user 8 | 9 | FROM public.ecr.aws/bitnami/node:20 10 | WORKDIR /app 11 | COPY --from=build /app ./ 12 | EXPOSE 3010 13 | CMD ["npm", "run", "start:user"] 14 | -------------------------------------------------------------------------------- /server/application/libs/auth/src/auth-config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Injectable } from '@nestjs/common'; 6 | 7 | @Injectable() 8 | export class AuthConfig { 9 | public userPoolId: string = process.env.COGNITO_USER_POOL_ID; 10 | public clientId: string = process.env.COGNITO_CLIENT_ID; 11 | public region: string = process.env.COGNITO_REGION; 12 | public authority = `https://cognito-idp.${process.env.COGNITO_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`; 13 | } 14 | -------------------------------------------------------------------------------- /server/application/libs/auth/src/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { createParamDecorator, type ExecutionContext } from '@nestjs/common'; 6 | 7 | export const TenantCredentials = createParamDecorator( 8 | (data: unknown, ctx: ExecutionContext) => { 9 | const request = ctx.switchToHttp().getRequest(); 10 | return request.user; 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /server/application/libs/auth/src/auth.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { ConfigModule } from '@nestjs/config'; 6 | import { PassportModule } from '@nestjs/passport'; 7 | import { Module } from '@nestjs/common'; 8 | 9 | import { AuthConfig } from './auth-config'; 10 | import { JwtStrategy } from './jwt.strategy'; 11 | 12 | @Module({ 13 | imports: [ 14 | ConfigModule.forRoot({ isGlobal: true }), 15 | PassportModule.register({ defaultStrategy: 'jwt' }) 16 | ], 17 | providers: [JwtStrategy, AuthConfig], 18 | exports: [JwtStrategy] 19 | }) 20 | export class AuthModule {} 21 | -------------------------------------------------------------------------------- /server/application/libs/auth/src/credential-vendor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { STSClient, AssumeRoleCommand } from '@aws-sdk/client-sts'; 6 | import * as Mustache from 'mustache'; 7 | import * as policies from './policies.json'; 8 | 9 | export enum PolicyType { 10 | DynamoDBLeadingKey = 'DYNAMOLEADINGKEY' 11 | } 12 | 13 | export interface CredentialConfig { 14 | policyType: PolicyType 15 | attributes: ClientAttributes 16 | duration?: number 17 | roleSessionName?: string 18 | } 19 | 20 | export type ClientAttributes = Record; 21 | 22 | export class CredentialVendor { 23 | constructor (private readonly tenantId: string) {} 24 | 25 | async getCredentials (config: CredentialConfig): Promise { 26 | let policy: string; 27 | switch (config.policyType) { 28 | case PolicyType.DynamoDBLeadingKey: 29 | const template = JSON.stringify(policies.dynamodbLeadingKey); 30 | const vals = { 31 | ...config.attributes, 32 | tenant: this.tenantId 33 | }; 34 | policy = Mustache.render(template, vals); 35 | console.log('POLICY:', policy); 36 | default: 37 | break; 38 | } 39 | const sts = new STSClient({ region: process.env.AWS_REGION }); 40 | const cmd = new AssumeRoleCommand({ 41 | DurationSeconds: config.duration || 900, 42 | Policy: policy, 43 | RoleArn: process.env.IAM_ROLE_ARN, 44 | RoleSessionName: config.roleSessionName || this.tenantId 45 | }); 46 | const response = await sts.send(cmd); 47 | console.log('Successfully assumed role: ', process.env.IAM_ROLE_ARN); 48 | return response.Credentials; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /server/application/libs/auth/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export * from './auth.module'; 6 | -------------------------------------------------------------------------------- /server/application/libs/auth/src/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Injectable, type ExecutionContext } from '@nestjs/common'; 6 | import { AuthGuard } from '@nestjs/passport'; 7 | import { Reflector } from '@nestjs/core'; 8 | 9 | @Injectable() 10 | export class JwtAuthGuard extends AuthGuard('jwt') { 11 | constructor (private readonly reflector: Reflector) { 12 | super(); 13 | } 14 | 15 | canActivate (context: ExecutionContext) { 16 | const isPublic = this.reflector.getAllAndOverride('isPublic', [ 17 | context.getHandler(), 18 | context.getClass() 19 | ]); 20 | if (isPublic) { 21 | return true; 22 | } 23 | return super.canActivate(context); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/application/libs/auth/src/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { ExtractJwt, Strategy } from 'passport-jwt'; 6 | import { PassportStrategy } from '@nestjs/passport'; 7 | import { Injectable } from '@nestjs/common'; 8 | import { passportJwtSecret } from 'jwks-rsa'; 9 | import { AuthConfig } from './auth-config'; 10 | 11 | @Injectable() 12 | export class JwtStrategy extends PassportStrategy(Strategy) { 13 | constructor (private readonly authConfig: AuthConfig) { 14 | super({ 15 | secretOrKeyProvider: passportJwtSecret({ 16 | cache: true, 17 | rateLimit: true, 18 | jwksRequestsPerMinute: 5, 19 | jwksUri: `${authConfig.authority}/.well-known/jwks.json` 20 | }), 21 | 22 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 23 | audience: authConfig.clientId, 24 | issuer: authConfig.authority, 25 | algorithms: ['RS256'] 26 | }); 27 | 28 | console.log(authConfig.authority); 29 | } 30 | 31 | async validate (payload: any) { 32 | const match = payload.iss.match(/([a-z\d\_\-]+)(\/*|)$/gi); 33 | return { 34 | userId: payload.sub, 35 | username: payload['cognito:username'], 36 | tenantId: payload['custom:tenantId'], 37 | tenantTier: payload['custom:tenantTier'], 38 | tenantName: payload['custom:tenantName'], 39 | email: payload.email, 40 | userPoolId: match?.[0], 41 | appClientId: payload.aud 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /server/application/libs/auth/src/policies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dynamodbLeadingKey": { 3 | "Version": "2012-10-17", 4 | "Statement": [ 5 | { 6 | "Effect": "Allow", 7 | "Action": ["dynamodb:*"], 8 | "Resource": ["arn:aws:dynamodb:*:*:table/{{table}}"], 9 | "Condition": { 10 | "ForAllValues:StringEquals": { 11 | "dynamodb:LeadingKeys": ["{{tenant}}"] 12 | } 13 | } 14 | } 15 | ] 16 | }, 17 | "s3": { 18 | "Version": "2012-10-17", 19 | "Statement": [ 20 | { 21 | "Effect": "Allow", 22 | "Action": ["dynamodb:*"], 23 | "Resource": ["arn:aws:dynamodb:*:*:table/{{table}}"], 24 | "Condition": { 25 | "ForAllValues:StringEquals": { 26 | "dynamodb:LeadingKeys": ["{{tenant}}"] 27 | } 28 | } 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/application/libs/auth/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/auth", 6 | "resolveJsonModule": true 7 | }, 8 | "include": ["src/**/*"], 9 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /server/application/libs/client-factory/src/client-factory.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Module } from '@nestjs/common'; 6 | import { ClientFactoryService } from './client-factory.service'; 7 | 8 | @Module({ 9 | providers: [ClientFactoryService], 10 | exports: [ClientFactoryService] 11 | }) 12 | export class ClientFactoryModule {} 13 | -------------------------------------------------------------------------------- /server/application/libs/client-factory/src/client-factory.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { 6 | type CredentialConfig, 7 | CredentialVendor, 8 | PolicyType 9 | } from '@app/auth/credential-vendor'; 10 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 11 | import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; 12 | import { Injectable } from '@nestjs/common'; 13 | 14 | @Injectable() 15 | export class ClientFactoryService { 16 | public async getClient ( 17 | tenantId: string, 18 | credentialConfig?: CredentialConfig 19 | ) { 20 | const credentialVendor = new CredentialVendor(tenantId); 21 | const creds = await credentialVendor.getCredentials( 22 | credentialConfig || { 23 | policyType: PolicyType.DynamoDBLeadingKey, 24 | attributes: { 25 | tenant: tenantId 26 | } 27 | } 28 | ); 29 | return DynamoDBDocumentClient.from( 30 | new DynamoDBClient({ 31 | credentials: { 32 | accessKeyId: creds.AccessKeyId, 33 | secretAccessKey: creds.SecretAccessKey, 34 | sessionToken: creds.SessionToken, 35 | expiration: creds.Expiration 36 | } 37 | }) 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server/application/libs/client-factory/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export * from './client-factory.module'; 6 | export * from './client-factory.service'; 7 | -------------------------------------------------------------------------------- /server/application/libs/client-factory/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/client-factory" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /server/application/microservices/order/src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import { NestFactory } from '@nestjs/core'; 7 | import { OrdersModule } from './orders/orders.module'; 8 | 9 | async function bootstrap () { 10 | const app = await NestFactory.create(OrdersModule); 11 | app.setGlobalPrefix('/'); 12 | app.enableCors({ 13 | allowedHeaders: '*', 14 | origin: '*', 15 | methods: '*' 16 | }); 17 | await app.listen(3010); 18 | } 19 | bootstrap(); 20 | -------------------------------------------------------------------------------- /server/application/microservices/order/src/orders/dto/create-order.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { type OrderProductDto } from './order-product.dto'; 6 | 7 | export class CreateOrderDto { 8 | orderName: string; 9 | orderProducts: OrderProductDto[]; 10 | } 11 | -------------------------------------------------------------------------------- /server/application/microservices/order/src/orders/dto/order-product.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class OrderProductDto { 6 | productId: string; 7 | price: number; 8 | quantity: number; 9 | } 10 | -------------------------------------------------------------------------------- /server/application/microservices/order/src/orders/dto/update-order.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { PartialType } from '@nestjs/mapped-types'; 6 | import { CreateOrderDto } from './create-order.dto'; 7 | 8 | export class UpdateOrderDto extends PartialType(CreateOrderDto) {} 9 | -------------------------------------------------------------------------------- /server/application/microservices/order/src/orders/entities/order.entity.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class Order {} 6 | -------------------------------------------------------------------------------- /server/application/microservices/order/src/orders/orders.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { 6 | Controller, 7 | Get, 8 | Post, 9 | Body, 10 | Param, 11 | UseGuards, 12 | SetMetadata 13 | } from '@nestjs/common'; 14 | import { OrdersService } from './orders.service'; 15 | import { CreateOrderDto } from './dto/create-order.dto'; 16 | import { TenantCredentials } from '@app/auth/auth.decorator'; 17 | import { JwtAuthGuard } from '@app/auth/jwt-auth.guard'; 18 | 19 | @Controller('orders') 20 | export class OrdersController { 21 | constructor (private readonly ordersService: OrdersService) {} 22 | 23 | @Post() 24 | @UseGuards(JwtAuthGuard) 25 | async create (@Body() createOrderDto: CreateOrderDto, @TenantCredentials() tenant) { 26 | await this.ordersService.create(createOrderDto, tenant.tenantId); 27 | } 28 | 29 | @Get('/health') 30 | @UseGuards(JwtAuthGuard) 31 | @SetMetadata('isPublic', true) 32 | health () { 33 | return { status: 'ok' }; 34 | } 35 | 36 | @Get() 37 | @UseGuards(JwtAuthGuard) 38 | async findAll (@TenantCredentials() tenant) { 39 | return await this.ordersService.findAll(tenant?.tenantId); 40 | } 41 | 42 | @Get(':id') 43 | @UseGuards(JwtAuthGuard) 44 | async findOne (@Param('id') id: string, @TenantCredentials() tenant) { 45 | return await this.ordersService.findOne(id, tenant?.tenantId); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/application/microservices/order/src/orders/orders.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Module } from '@nestjs/common'; 6 | import { OrdersService } from './orders.service'; 7 | import { OrdersController } from './orders.controller'; 8 | import { AuthModule } from '@app/auth'; 9 | import { ConfigModule } from '@nestjs/config'; 10 | import { ClientFactoryModule } from '@app/client-factory'; 11 | 12 | @Module({ 13 | imports: [ 14 | AuthModule, 15 | ConfigModule.forRoot({ isGlobal: true }), 16 | ClientFactoryModule 17 | ], 18 | controllers: [OrdersController], 19 | providers: [OrdersService] 20 | }) 21 | export class OrdersModule {} 22 | -------------------------------------------------------------------------------- /server/application/microservices/order/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/microservices/order" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /server/application/microservices/product_dynamodb/src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NestFactory } from '@nestjs/core'; 6 | import { ProductsModule } from './products/products.module'; 7 | 8 | async function bootstrap () { 9 | const app = await NestFactory.create(ProductsModule); 10 | app.setGlobalPrefix('/'); 11 | app.enableCors({ 12 | allowedHeaders: '*', 13 | origin: '*', 14 | methods: '*' 15 | }); 16 | await app.listen(3010); 17 | } 18 | bootstrap(); 19 | -------------------------------------------------------------------------------- /server/application/microservices/product_dynamodb/src/products/dto/create-product.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class CreateProductDto { 6 | name: string; 7 | price: number; 8 | sku: number; 9 | category: string; 10 | } 11 | -------------------------------------------------------------------------------- /server/application/microservices/product_dynamodb/src/products/dto/update-product.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { PartialType } from '@nestjs/mapped-types'; 6 | import { CreateProductDto } from './create-product.dto'; 7 | 8 | export class UpdateProductDto extends PartialType(CreateProductDto) {} 9 | -------------------------------------------------------------------------------- /server/application/microservices/product_dynamodb/src/products/entities/product.entity.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class Product { 6 | constructor ( 7 | private readonly id: string, 8 | private readonly price: number, 9 | private readonly name: string, 10 | private readonly category: string 11 | ) {} 12 | } 13 | -------------------------------------------------------------------------------- /server/application/microservices/product_dynamodb/src/products/products.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { 6 | Controller, 7 | Get, 8 | Post, 9 | Body, 10 | Put, 11 | Param, 12 | UseGuards, 13 | SetMetadata 14 | } from '@nestjs/common'; 15 | import { ProductsService } from './products.service'; 16 | import { CreateProductDto } from './dto/create-product.dto'; 17 | import { UpdateProductDto } from './dto/update-product.dto'; 18 | import { JwtAuthGuard } from '@app/auth/jwt-auth.guard'; 19 | import { TenantCredentials } from '@app/auth/auth.decorator'; 20 | 21 | @Controller('products') 22 | export class ProductsController { 23 | constructor (private readonly productsService: ProductsService) {} 24 | 25 | @Post() 26 | @UseGuards(JwtAuthGuard) 27 | async create ( 28 | @Body() createProductDto: CreateProductDto, 29 | @TenantCredentials() tenant 30 | ) { 31 | console.log('Create product', tenant); 32 | await this.productsService.create(createProductDto, tenant.tenantId); 33 | } 34 | 35 | @Get() 36 | @UseGuards(JwtAuthGuard) 37 | async findAll (@TenantCredentials() tenant) { 38 | console.log('Get products', tenant); 39 | const tenantId = tenant.tenantId; 40 | return await this.productsService.findAll(tenantId); 41 | } 42 | 43 | @Get('/health') 44 | @UseGuards(JwtAuthGuard) 45 | @SetMetadata('isPublic', true) 46 | health () { 47 | return { status: 'ok' }; 48 | } 49 | 50 | @Get(':id') 51 | @UseGuards(JwtAuthGuard) 52 | async findOne (@Param('id') id: string, @TenantCredentials() tenant) { 53 | console.log('Get One product', tenant); 54 | return await this.productsService.findOne(id, tenant.tenantId); 55 | } 56 | 57 | @Put(':id') 58 | @UseGuards(JwtAuthGuard) 59 | async update ( 60 | @Param('id') id: string, 61 | @Body() updateProductDto: UpdateProductDto, 62 | @TenantCredentials() tenant 63 | ) { 64 | console.log(tenant); 65 | return await this.productsService.update(id, tenant.tenantId, updateProductDto); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /server/application/microservices/product_dynamodb/src/products/products.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Module } from '@nestjs/common'; 6 | import { ProductsService } from './products.service'; 7 | import { ProductsController } from './products.controller'; 8 | import { AuthModule } from '@app/auth'; 9 | import { ConfigModule } from '@nestjs/config'; 10 | import { ClientFactoryModule } from '@app/client-factory'; 11 | 12 | @Module({ 13 | imports: [ 14 | AuthModule, 15 | ConfigModule.forRoot({ isGlobal: true }), 16 | ClientFactoryModule 17 | ], 18 | controllers: [ProductsController], 19 | providers: [ProductsService] 20 | }) 21 | export class ProductsModule {} 22 | -------------------------------------------------------------------------------- /server/application/microservices/product_dynamodb/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/microservices/product" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /server/application/microservices/product_mysql/src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NestFactory } from '@nestjs/core'; 6 | import { ProductsModule } from './products/products.module'; 7 | 8 | async function bootstrap () { 9 | const app = await NestFactory.create(ProductsModule); 10 | app.setGlobalPrefix('/'); 11 | app.enableCors({ 12 | allowedHeaders: '*', 13 | origin: '*', 14 | methods: '*' 15 | }); 16 | await app.listen(3010); 17 | } 18 | bootstrap(); 19 | -------------------------------------------------------------------------------- /server/application/microservices/product_mysql/src/products/dto/create-product.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class CreateProductDto { 6 | name: string; 7 | price: number; 8 | sku: number; 9 | category: string; 10 | } 11 | -------------------------------------------------------------------------------- /server/application/microservices/product_mysql/src/products/dto/update-product.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { PartialType } from '@nestjs/mapped-types'; 6 | import { CreateProductDto } from './create-product.dto'; 7 | 8 | export class UpdateProductDto extends PartialType(CreateProductDto) {} 9 | -------------------------------------------------------------------------------- /server/application/microservices/product_mysql/src/products/entities/product.entity.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class Product { 6 | constructor ( 7 | private readonly id: string, 8 | private readonly price: number, 9 | private readonly name: string, 10 | private readonly category: string 11 | ) {} 12 | } 13 | -------------------------------------------------------------------------------- /server/application/microservices/product_mysql/src/products/products.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { 6 | Controller, 7 | Get, 8 | Post, 9 | Body, 10 | Put, 11 | Param, 12 | UseGuards, 13 | SetMetadata 14 | } from '@nestjs/common'; 15 | import { ProductsService } from './products.service'; 16 | import { CreateProductDto } from './dto/create-product.dto'; 17 | import { UpdateProductDto } from './dto/update-product.dto'; 18 | import { JwtAuthGuard } from '@app/auth/jwt-auth.guard'; 19 | import { TenantCredentials } from '@app/auth/auth.decorator'; 20 | 21 | @Controller('products') 22 | export class ProductsController { 23 | constructor (private readonly productsService: ProductsService) {} 24 | 25 | @Post() 26 | @UseGuards(JwtAuthGuard) 27 | async create ( 28 | @Body() createProductDto: CreateProductDto, 29 | @TenantCredentials() tenant 30 | ) { 31 | console.log('Create product', tenant); 32 | await this.productsService.create(createProductDto, tenant.tenantId, tenant.tenantName); 33 | } 34 | 35 | @Get() 36 | @UseGuards(JwtAuthGuard) 37 | async findAll (@TenantCredentials() tenant) { 38 | console.log('Get products', tenant); 39 | const tenantId = tenant.tenantId; 40 | return await this.productsService.findAll(tenantId, tenant.tenantName); 41 | } 42 | 43 | @Get('/health') 44 | @UseGuards(JwtAuthGuard) 45 | @SetMetadata('isPublic', true) 46 | health () { 47 | return { status: 'ok' }; 48 | } 49 | 50 | @Get(':id') 51 | @UseGuards(JwtAuthGuard) 52 | async findOne (@Param('id') id: string, @TenantCredentials() tenant) { 53 | console.log('Get One product', tenant); 54 | return await this.productsService.findOne(id, tenant.tenantId, tenant.tenantName); 55 | } 56 | 57 | @Put(':id') 58 | @UseGuards(JwtAuthGuard) 59 | async update ( 60 | @Param('id') id: string, 61 | @Body() updateProductDto: UpdateProductDto, 62 | @TenantCredentials() tenant 63 | ) { 64 | console.log(tenant); 65 | return await this.productsService.update(id, tenant.tenantId, tenant.tenantName, updateProductDto); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /server/application/microservices/product_mysql/src/products/products.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Module } from '@nestjs/common'; 6 | import { ProductsService } from './products.service'; 7 | import { ProductsController } from './products.controller'; 8 | import { AuthModule } from '@app/auth'; 9 | import { ConfigModule } from '@nestjs/config'; 10 | import { ClientFactoryModule } from '@app/client-factory'; 11 | 12 | @Module({ 13 | imports: [ 14 | AuthModule, 15 | ConfigModule.forRoot({ isGlobal: true }), 16 | ClientFactoryModule 17 | ], 18 | controllers: [ProductsController], 19 | providers: [ProductsService] 20 | }) 21 | export class ProductsModule {} 22 | -------------------------------------------------------------------------------- /server/application/microservices/product_mysql/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/microservices/product" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /server/application/microservices/user/src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NestFactory } from '@nestjs/core'; 6 | import { UsersModule } from './users/users.module'; 7 | 8 | async function bootstrap () { 9 | const app = await NestFactory.create(UsersModule); 10 | app.setGlobalPrefix('/'); 11 | app.enableCors({ 12 | allowedHeaders: '*', 13 | origin: '*', 14 | methods: '*' 15 | }); 16 | await app.listen(3010); 17 | } 18 | bootstrap(); 19 | -------------------------------------------------------------------------------- /server/application/microservices/user/src/users/dto/identity.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class IdpDetails { 6 | name: string; 7 | public details: { 8 | userPoolId: string 9 | appClientId: string 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /server/application/microservices/user/src/users/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { PartialType } from '@nestjs/mapped-types'; 6 | import { UserDto } from './user.dto'; 7 | 8 | export class UpdateUserDto extends PartialType(UserDto) {} 9 | -------------------------------------------------------------------------------- /server/application/microservices/user/src/users/dto/user.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class UserDto { 6 | userEmail: string; 7 | userRole: string; 8 | userName: string; 9 | } 10 | -------------------------------------------------------------------------------- /server/application/microservices/user/src/users/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class UserInfo { 6 | username: string; 7 | email: string; 8 | user_role: string; 9 | status: string; 10 | enabled: boolean; 11 | created: Date; 12 | modified: Date; 13 | } 14 | -------------------------------------------------------------------------------- /server/application/microservices/user/src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { 6 | Controller, 7 | Get, 8 | Post, 9 | Body, 10 | Put, 11 | Param, 12 | Delete, 13 | UseGuards, 14 | SetMetadata 15 | } from '@nestjs/common'; 16 | import { UsersService } from './users.service'; 17 | import { UserDto } from './dto/user.dto'; 18 | import { UpdateUserDto } from './dto/update-user.dto'; 19 | import { JwtAuthGuard } from '@app/auth/jwt-auth.guard'; 20 | import { TenantCredentials } from '@app/auth/auth.decorator'; 21 | 22 | @Controller('users') 23 | export class UsersController { 24 | constructor (private readonly usersService: UsersService) {} 25 | 26 | @Post() 27 | @UseGuards(JwtAuthGuard) 28 | async create (@Body() userDto: UserDto, @TenantCredentials() tenant) { 29 | console.log('Request received to create new user', tenant.tenantTier); 30 | return await this.usersService.create(userDto, tenant); 31 | } 32 | 33 | @Get() 34 | @UseGuards(JwtAuthGuard) 35 | async findAll (@TenantCredentials() tenant) { 36 | return await this.usersService.findAll(tenant.tenantId); 37 | } 38 | 39 | @Get('/health') 40 | @UseGuards(JwtAuthGuard) 41 | @SetMetadata('isPublic', true) 42 | health () { 43 | return { status: 'ok' }; 44 | } 45 | 46 | @Get(':id') 47 | @UseGuards(JwtAuthGuard) 48 | async findOne (@Param('id') username: string, @TenantCredentials() tenant) { 49 | console.log('Get a user', tenant); 50 | return await this.usersService.findOne(username); 51 | } 52 | 53 | @Put(':id') 54 | @UseGuards(JwtAuthGuard) 55 | async update ( 56 | @Param('id') username: string, 57 | @Body() updateUserDto: UpdateUserDto, 58 | @TenantCredentials() tenant 59 | ) { 60 | console.log(tenant); 61 | return await this.usersService.update(username, updateUserDto); 62 | } 63 | 64 | @Delete(':id') 65 | async remove (@Param('id') username: string) { 66 | return await this.usersService.delete(username); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server/application/microservices/user/src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Module } from '@nestjs/common'; 6 | import { UsersService } from './users.service'; 7 | import { UsersController } from './users.controller'; 8 | import { AuthModule } from '@app/auth'; 9 | import { ConfigModule } from '@nestjs/config'; 10 | import { ClientFactoryModule } from '@app/client-factory'; 11 | 12 | @Module({ 13 | imports: [ 14 | AuthModule, 15 | ConfigModule.forRoot({ isGlobal: true }), 16 | ClientFactoryModule 17 | ], 18 | controllers: [UsersController], 19 | providers: [UsersService] 20 | }) 21 | export class UsersModule {} 22 | -------------------------------------------------------------------------------- /server/application/microservices/user/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/microservices/user" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /server/application/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "microservices/order/src", 4 | "monorepo": true, 5 | "root": "microservices/order", 6 | "compilerOptions": { 7 | "webpack": true, 8 | "tsConfigPath": "microservices/order/tsconfig.app.json" 9 | }, 10 | "projects": { 11 | "product": { 12 | "type": "application", 13 | "root": "microservices/product", 14 | "entryFile": "main", 15 | "sourceRoot": "microservices/product/src", 16 | "compilerOptions": { 17 | "tsConfigPath": "microservices/product/tsconfig.app.json", 18 | "assets": [ 19 | "**/*.json" 20 | ] 21 | } 22 | }, 23 | "order": { 24 | "type": "application", 25 | "root": "microservices/order", 26 | "entryFile": "main", 27 | "sourceRoot": "microservices/order/src", 28 | "compilerOptions": { 29 | "tsConfigPath": "microservices/order/tsconfig.app.json", 30 | "assets": [ 31 | "**/*.json" 32 | ] 33 | } 34 | }, 35 | "auth": { 36 | "type": "library", 37 | "root": "libs/auth", 38 | "entryFile": "index", 39 | "sourceRoot": "libs/auth/src", 40 | "compilerOptions": { 41 | "tsConfigPath": "libs/auth/tsconfig.lib.json", 42 | "assets": [ 43 | "**/*.json" 44 | ] 45 | } 46 | }, 47 | "client-factory": { 48 | "type": "library", 49 | "root": "libs/client-factory", 50 | "entryFile": "index", 51 | "sourceRoot": "libs/client-factory/src", 52 | "compilerOptions": { 53 | "tsConfigPath": "libs/client-factory/tsconfig.lib.json" 54 | } 55 | }, 56 | "user": { 57 | "type": "application", 58 | "root": "microservices/user", 59 | "entryFile": "main", 60 | "sourceRoot": "microservices/user/src", 61 | "compilerOptions": { 62 | "tsConfigPath": "microservices/user/tsconfig.app.json" 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /server/application/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "MIT-0", 8 | "engines": { 9 | "node": ">=14.18 || >= 16.13" 10 | }, 11 | "scripts": { 12 | "prebuild": "rimraf dist", 13 | "build": "nest build", 14 | "start": "nest start", 15 | "start:order": "node dist/microservices/order/main", 16 | "start:product": "node dist/microservices/product/main", 17 | "start:user": "node dist/microservices/user/main" 18 | }, 19 | "dependencies": { 20 | "@aws-sdk/client-cognito-identity-provider": "^3.651.1", 21 | "@aws-sdk/client-dynamodb": "^3.651.1", 22 | "@aws-sdk/client-sts": "^3.651.1", 23 | "@aws-sdk/lib-dynamodb": "^3.651.1", 24 | "@nestjs/common": "^10.3.3", 25 | "@nestjs/config": "^3.2.0", 26 | "@nestjs/core": "^10.3.3", 27 | "@nestjs/jwt": "^10.2.0", 28 | "@nestjs/mapped-types": "^2.0.5", 29 | "@nestjs/passport": "^10.0.3", 30 | "@nestjs/platform-express": "^10.3.3", 31 | "@types/mustache": "^4.2.5", 32 | "jwks-rsa": "^3.1.0", 33 | "mustache": "^4.2.0", 34 | "passport": "^0.7.0", 35 | "passport-jwt": "^4.0.1", 36 | "reflect-metadata": "^0.2.1", 37 | "rimraf": "^5.0.5", 38 | "rxjs": "^7.8.1", 39 | "uuid": "^9.0.1" 40 | }, 41 | "devDependencies": { 42 | "aws-sdk":"^2.840.0", 43 | "mysql2": "^2.2.5", 44 | "@nestjs/cli": "^10.3.2", 45 | "@nestjs/schematics": "^8.0.0", 46 | "@nestjs/testing": "^10.0.0", 47 | "@types/express": "^4.17.11", 48 | "@types/node": "^14.14.36", 49 | "ts-loader": "^8.0.18", 50 | "ts-node": "^9.1.1", 51 | "tsconfig-paths": "^3.9.0", 52 | "typescript": "^4.3.5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /server/application/reverseproxy/index.html: -------------------------------------------------------------------------------- 1 | Service is healthy -------------------------------------------------------------------------------- /server/application/reverseproxy/nginx.template: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | # NGINX will handle gzip compression of responses from the app server 7 | gzip on; 8 | gzip_proxied any; 9 | gzip_types text/plain application/json; 10 | gzip_min_length 1000; 11 | 12 | server { 13 | listen 80; 14 | server_name localhost; 15 | location / { 16 | root /usr/share/nginx/html; 17 | index index.html index.htm; 18 | } 19 | 20 | location = /health { 21 | # access_log off; 22 | add_header 'Content-Type' 'application/json'; 23 | return 200 '{"status":"ok"}'; 24 | } 25 | 26 | # orders api 27 | location ~ ^/orders { 28 | # Reject requests with unsupported HTTP method 29 | if ($request_method !~ ^(GET|POST|HEAD|OPTIONS|PUT|DELETE)$) { 30 | return 405; 31 | } 32 | 33 | # Only requests matching the expectations will 34 | # get sent to the application server 35 | proxy_pass http://orders-api.${NAMESPACE}.sc:3010; 36 | proxy_http_version 1.1; 37 | proxy_set_header Upgrade $http_upgrade; 38 | proxy_set_header Connection 'upgrade'; 39 | proxy_set_header Host $host; 40 | } 41 | 42 | # products api 43 | location ~ ^/products { 44 | # Reject requests with unsupported HTTP method 45 | if ($request_method !~ ^(GET|POST|HEAD|OPTIONS|PUT|DELETE)$) { 46 | return 405; 47 | } 48 | 49 | # Only requests matching the expectations will 50 | # get sent to the application server 51 | proxy_pass http://products-api.${NAMESPACE}.sc:3010; 52 | proxy_http_version 1.1; 53 | proxy_set_header Upgrade $http_upgrade; 54 | proxy_set_header Connection 'upgrade'; 55 | proxy_set_header Host $host; 56 | } 57 | 58 | # users api 59 | location ~ ^/users { 60 | # Reject requests with unsupported HTTP method 61 | if ($request_method !~ ^(GET|POST|HEAD|OPTIONS|PUT|DELETE)$) { 62 | return 405; 63 | } 64 | 65 | # Only requests matching the expectations will 66 | # get sent to the application server 67 | proxy_pass http://users-api.${NAMESPACE}.sc:3010; 68 | proxy_http_version 1.1; 69 | proxy_set_header Upgrade $http_upgrade; 70 | proxy_set_header Connection 'upgrade'; 71 | proxy_set_header Host $host; 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /server/application/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "resolveJsonModule": true, 15 | "paths": { 16 | "@app/auth": [ 17 | "libs/auth/src" 18 | ], 19 | "@app/auth/*": [ 20 | "libs/auth/src/*" 21 | ], 22 | "@app/client-factory": [ 23 | "libs/client-factory/src" 24 | ], 25 | "@app/client-factory/*": [ 26 | "libs/client-factory/src/*" 27 | ] 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /server/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/ecs-saas-ref-template.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], 21 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 22 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 23 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 24 | "@aws-cdk/aws-iam:minimizePolicies": true, 25 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 26 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 27 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 28 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 29 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 30 | "@aws-cdk/core:enablePartitionLiterals": true, 31 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 32 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 33 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 34 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 35 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 36 | "@aws-cdk/aws-route53-patters:useCertificate": true, 37 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 38 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 39 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 40 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 41 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 42 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 43 | "@aws-cdk/aws-redshift:columnId": true, 44 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 45 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 46 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 47 | "@aws-cdk/aws-kms:aliasNameRef": true, 48 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /server/lib/bootstrap-template/control-plane-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { type Construct } from 'constructs'; 3 | import path = require('path'); 4 | import { StaticSite } from './static-site'; 5 | import { ControlPlaneNag } from '../cdknag/control-plane-nag'; 6 | import { addTemplateTag } from '../utilities/helper-functions'; 7 | import * as sbt from '@cdklabs/sbt-aws'; 8 | import { StaticSiteDistro } from '../shared-infra/static-site-distro'; 9 | 10 | interface ControlPlaneStackProps extends cdk.StackProps { 11 | systemAdminEmail: string 12 | accessLogsBucket: cdk.aws_s3.Bucket 13 | distro: StaticSiteDistro 14 | adminSiteUrl: string 15 | } 16 | 17 | export class ControlPlaneStack extends cdk.Stack { 18 | public readonly regApiGatewayUrl: string; 19 | public readonly eventManager: sbt.IEventManager; 20 | public readonly auth: sbt.CognitoAuth; 21 | public readonly adminSiteUrl: string; 22 | public readonly staticSite: StaticSite; 23 | 24 | constructor (scope: Construct, id: string, props: ControlPlaneStackProps) { 25 | super(scope, id, props); 26 | addTemplateTag(this, 'ControlPlaneStack'); 27 | 28 | const cognitoAuth = new sbt.CognitoAuth(this, 'CognitoAuth', { 29 | controlPlaneCallbackURL: props.adminSiteUrl 30 | }); 31 | 32 | const controlPlane = new sbt.ControlPlane(this, 'controlplane-sbt', { 33 | systemAdminEmail: props.systemAdminEmail, 34 | auth: cognitoAuth, 35 | apiCorsConfig: { 36 | allowOrigins: ['https://*'], 37 | allowCredentials: true, 38 | allowHeaders: ['*'], 39 | allowMethods: [cdk.aws_apigatewayv2.CorsHttpMethod.ANY], 40 | maxAge: cdk.Duration.seconds(300), 41 | }, 42 | }); 43 | 44 | this.eventManager = controlPlane.eventManager; 45 | this.regApiGatewayUrl = controlPlane.controlPlaneAPIGatewayUrl; 46 | this.auth = cognitoAuth; 47 | 48 | const staticSite = new StaticSite(this, 'AdminWebUi', { 49 | name: 'AdminSite', 50 | assetDirectory: path.join(__dirname, '../../../client/AdminWeb'), 51 | production: true, 52 | clientId: this.auth.userClientId, //.clientId, 53 | issuer: this.auth.tokenEndpoint, 54 | apiUrl: this.regApiGatewayUrl, 55 | wellKnownEndpointUrl: this.auth.wellKnownEndpointUrl, 56 | distribution: props.distro.cloudfrontDistribution, 57 | appBucket: props.distro.siteBucket, 58 | accessLogsBucket: props.accessLogsBucket, 59 | env: { 60 | account: this.account, 61 | region: this.region 62 | } 63 | }); 64 | 65 | new cdk.CfnOutput(this, 'adminSiteUrl', { 66 | value: props.adminSiteUrl 67 | }); 68 | 69 | new ControlPlaneNag(this, 'controlplane-nag'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /server/lib/cdknag/ecs-saas-pipeline-nag.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { NagSuppressions } from 'cdk-nag'; 4 | 5 | export class ECSSaaSPipelineNag extends Construct { 6 | constructor (scope: Construct, id: string) { 7 | super(scope, id); 8 | 9 | const nagPath1 = '/tenant-update-stack'; 10 | NagSuppressions.addResourceSuppressionsByPath( 11 | cdk.Stack.of(this), 12 | [`${nagPath1}/deployerRole/Resource`], 13 | [ 14 | { 15 | id: 'AwsSolutions-IAM5', 16 | reason: 'This is not related with SaaS itself', 17 | appliesTo: ['Resource::*'] 18 | } 19 | ] 20 | ); 21 | 22 | NagSuppressions.addResourceSuppressionsByPath( 23 | cdk.Stack.of(this), 24 | [`${nagPath1}/Deploy/Resource`], 25 | [ 26 | { 27 | id: 'AwsSolutions-CB3', 28 | reason: 'SBT-ECS SaaS:The CodeBuild project has privileged mode enabled' 29 | } 30 | ] 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/lib/interfaces/api-key-ssm-parameter-names.ts: -------------------------------------------------------------------------------- 1 | // This interface is used to help simplify sharing the 2 | // set of SSM parameter names between constructs and stacks. 3 | export interface ApiKeySSMParameterNames { 4 | [tier: string]: { 5 | keyId: string; 6 | value: string; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /server/lib/interfaces/container-info.ts: -------------------------------------------------------------------------------- 1 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | 4 | 5 | export interface ContainerInfo { 6 | name: string 7 | image: string 8 | memoryLimitMiB: number 9 | cpu: number 10 | containerPort: number 11 | policy?: string 12 | database?: { 13 | kind: string 14 | sortKey?: string, 15 | 16 | 17 | }, 18 | portMappings: Array<{ 19 | name: string, 20 | containerPort: number 21 | appProtocol?: ecs.AppProtocol, 22 | protocol?: ecs.Protocol 23 | }>, 24 | environment: { 25 | TABLE_NAME: string, 26 | iam_arn?: string, 27 | resource?: string, 28 | proxy_endpoint?: string, 29 | cluster_endpoint_resource?:string 30 | namespace?: string, 31 | }, 32 | healthCheck?: { 33 | command: string[], 34 | interval?: cdk.Duration, 35 | timeout?: cdk.Duration, 36 | retries?: number, 37 | startPeriod?: cdk.Duration 38 | } 39 | } -------------------------------------------------------------------------------- /server/lib/interfaces/custom-api-key.ts: -------------------------------------------------------------------------------- 1 | // This interface is used instead of simply using the ApiKey class because 2 | // it allows downstream classes to read the value of the ApiKey. 3 | // more info: https://stackoverflow.com/questions/66142536/cdk-how-to-get-apigateway-key-value-ie-x-api-key-20-chars 4 | export interface CustomApiKey { 5 | value: string 6 | apiKeyId: string 7 | } 8 | -------------------------------------------------------------------------------- /server/lib/interfaces/identity-details.ts: -------------------------------------------------------------------------------- 1 | export interface IdentityDetails { 2 | name: string; 3 | details: { 4 | [key: string]: any; 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /server/lib/interfaces/rproxy-info.ts: -------------------------------------------------------------------------------- 1 | export interface RproxyInfo { 2 | name: string 3 | image: string 4 | memoryLimitMiB: number 5 | cpu: number 6 | containerPort: number 7 | policy: string 8 | } 9 | -------------------------------------------------------------------------------- /server/lib/shared-infra/Resources/requirements.txt: -------------------------------------------------------------------------------- 1 | python-jose[cryptography] -------------------------------------------------------------------------------- /server/lib/shared-infra/layers/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.12-slim 3 | 4 | # Set the working directory to /app 5 | WORKDIR /app 6 | 7 | # Copy the current directory contents into the container at /app 8 | COPY . /app 9 | 10 | # Install any needed packages specified in requirements.txt 11 | RUN pip install --trusted-host pypi.python.org -r requirements.txt 12 | 13 | -------------------------------------------------------------------------------- /server/lib/shared-infra/layers/abstract_classes/idp_authorizer_abstract_class.py: -------------------------------------------------------------------------------- 1 | import abc 2 | class IdpAuthorizerAbstractClass (abc.ABC): 3 | 4 | @abc.abstractmethod 5 | def validateJWT(self,event): 6 | pass -------------------------------------------------------------------------------- /server/lib/shared-infra/layers/abstract_classes/idp_user_management_abstract_class.py: -------------------------------------------------------------------------------- 1 | import abc 2 | class IdpUserManagementAbstractClass (abc.ABC): 3 | 4 | @abc.abstractmethod 5 | def create_user(self, event): 6 | pass 7 | 8 | @abc.abstractmethod 9 | def get_users(self, event): 10 | pass 11 | 12 | @abc.abstractmethod 13 | def get_user(self, event): 14 | pass 15 | 16 | @abc.abstractmethod 17 | def update_user(self, event): 18 | pass 19 | 20 | @abc.abstractmethod 21 | def disable_user(self, event): 22 | pass 23 | 24 | @abc.abstractmethod 25 | def enable_user(self, event): 26 | pass 27 | 28 | @abc.abstractmethod 29 | def delete_user(self, event): 30 | pass -------------------------------------------------------------------------------- /server/lib/shared-infra/layers/cognito/user_management_util.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | cognito = boto3.client('cognito-idp') 4 | 5 | 6 | def create_user_group(user_pool_id, group_name): 7 | response = cognito.create_group( 8 | GroupName=group_name, 9 | UserPoolId=user_pool_id, 10 | Precedence=0 11 | ) 12 | return response 13 | 14 | 15 | def create_user(user_pool_id, user_details): 16 | response = cognito.admin_create_user( 17 | Username=user_details['userName'], 18 | UserPoolId=user_pool_id, 19 | ForceAliasCreation=True, 20 | UserAttributes= 21 | [ 22 | { 23 | 'Name': 'email', 24 | 'Value': user_details['userEmail'] 25 | }, 26 | { 27 | 'Name': 'email_verified', 28 | 'Value': 'true' 29 | }, 30 | { 31 | 'Name': 'custom:userRole', 32 | 'Value': user_details['userRole'] 33 | } 34 | ] 35 | ) 36 | return response 37 | 38 | def add_user_to_group(user_pool_id, user_name, group_name): 39 | response = cognito.admin_add_user_to_group( 40 | UserPoolId=user_pool_id, 41 | Username=user_name, 42 | GroupName=group_name 43 | ) 44 | return response 45 | 46 | def user_group_exists(user_pool_id, group_name): 47 | try: 48 | response=cognito.get_group( 49 | UserPoolId=user_pool_id, 50 | GroupName=group_name) 51 | return True 52 | except Exception as e: 53 | return False 54 | 55 | def validate_user_tenancy(user_pool_id, user_name, group_name): 56 | isValid = False 57 | list_of_groups = cognito.admin_list_groups_for_user( 58 | UserPoolId=user_pool_id, 59 | Username=user_name 60 | ) 61 | for group in list_of_groups['Groups']: 62 | if group['GroupName'] == group_name: 63 | isValid = True 64 | break 65 | return isValid 66 | 67 | 68 | -------------------------------------------------------------------------------- /server/lib/shared-infra/layers/idp_object_factory.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | def get_idp_user_mgmt_object(idp_name): 4 | 5 | idp_impl_class = '' 6 | if (idp_name.upper() == 'COGNITO'): 7 | idp_impl_class = getattr(importlib.import_module("cognito.cognito_user_management_service"), "CognitoUserManagementService") 8 | 9 | return idp_impl_class() 10 | 11 | def get_idp_authorizer_object(idp_name): 12 | 13 | idp_impl_class = '' 14 | if (idp_name.upper() == 'COGNITO'): 15 | idp_impl_class = getattr(importlib.import_module("cognito.cognito_authorizer"), "CognitoAuthorizer") 16 | 17 | return idp_impl_class() 18 | 19 | -------------------------------------------------------------------------------- /server/lib/shared-infra/layers/logger.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | from aws_lambda_powertools import Logger 5 | logger = Logger() 6 | 7 | """Log info messages 8 | """ 9 | def info(log_message): 10 | #logger.structure_logs(append=True, tenant_id=tenant_id) 11 | logger.info (log_message) 12 | 13 | """Log error messages 14 | """ 15 | def error(log_message): 16 | #logger.structure_logs(append=True, tenant_id=tenant_id) 17 | logger.error (log_message) 18 | 19 | """Log with tenant context. Extracts tenant context from the lambda events 20 | """ 21 | def log_with_tenant_context(event, log_message): 22 | logger.structure_logs(append=True, tenant_id= event['requestContext']['authorizer']['tenantId']) 23 | logger.info (log_message) -------------------------------------------------------------------------------- /server/lib/shared-infra/layers/metrics_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import json 5 | from aws_lambda_powertools import Metrics 6 | 7 | metrics = Metrics() 8 | 9 | 10 | def record_metric(event, metric_name, metric_unit, metric_value): 11 | """ Record the metric in Cloudwatch using EMF format 12 | 13 | Args: 14 | event ([type]): [description] 15 | metric_name ([type]): [description] 16 | metric_unit ([type]): [description] 17 | metric_value ([type]): [description] 18 | """ 19 | metrics.add_dimension(name="tenant_id", value=event['requestContext']['authorizer']['tenantId']) 20 | metrics.add_metric(name=metric_name, unit=metric_unit, value=metric_value) 21 | metrics_object = metrics.serialize_metric_set() 22 | metrics.clear_metrics() 23 | print(json.dumps(metrics_object)) 24 | 25 | -------------------------------------------------------------------------------- /server/lib/shared-infra/layers/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-lambda-powertools[all]==2.16.2 2 | simplejson 3 | jsonpickle 4 | aws_requests_auth 5 | python-jose[cryptography] 6 | aws_requests_auth 7 | boto3==1.17.54 -------------------------------------------------------------------------------- /server/lib/shared-infra/mysql-database/SSLCA.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF 3 | ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 4 | b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL 5 | MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv 6 | b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj 7 | ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM 8 | 9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw 9 | IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 10 | VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L 11 | 93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm 12 | jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC 13 | AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA 14 | A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI 15 | U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs 16 | N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv 17 | o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU 18 | 5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy 19 | rqXRfboQnoZsG4q5WTP468SQvvG5 20 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /server/lib/shared-infra/mysql-database/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | PyMySQL 3 | # mysql-connector-python -------------------------------------------------------------------------------- /server/lib/shared-infra/tenant-api-key.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { ApiKey } from 'aws-cdk-lib/aws-apigateway'; 3 | import { StringParameter } from 'aws-cdk-lib/aws-ssm'; 4 | import { addTemplateTag } from '../utilities/helper-functions'; 5 | 6 | interface TenantApiKeyProps { 7 | apiKeyValue: string 8 | ssmParameterApiKeyIdName: string 9 | ssmParameterApiValueName: string 10 | } 11 | 12 | export class TenantApiKey extends Construct { 13 | apiKey: ApiKey; 14 | apiKeyValue: string; 15 | constructor (scope: Construct, id: string, props: TenantApiKeyProps) { 16 | super(scope, id); 17 | addTemplateTag(this, 'TenantApiKey'); 18 | this.apiKeyValue = props.apiKeyValue; 19 | 20 | this.apiKey = new ApiKey(this, 'apiKey', { 21 | value: props.apiKeyValue 22 | }); 23 | new StringParameter(this, 'apiKeyId', { 24 | parameterName: props.ssmParameterApiKeyIdName, 25 | stringValue: this.apiKey.keyId 26 | }); 27 | 28 | new StringParameter(this, 'apiKeyValue', { 29 | parameterName: props.ssmParameterApiValueName, 30 | stringValue: this.apiKeyValue 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/lib/shared-infra/usage-plans.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { ApiKey, Period, type RestApi, type UsagePlan } from 'aws-cdk-lib/aws-apigateway'; 3 | import { addTemplateTag } from '../utilities/helper-functions'; 4 | 5 | interface UsagePlansProps { 6 | apiGateway: RestApi 7 | apiKeyIdBasicTier: string 8 | apiKeyIdAdvancedTier: string 9 | apiKeyIdPremiumTier: string 10 | isPooledDeploy: boolean 11 | } 12 | 13 | export class UsagePlans extends Construct { 14 | public readonly usagePlanBasicTier: UsagePlan; 15 | public readonly usagePlanAdvancedTier: UsagePlan; 16 | public readonly usagePlanPremiumTier: UsagePlan; 17 | public readonly usagePlanSystemAdmin: UsagePlan; 18 | constructor (scope: Construct, id: string, props: UsagePlansProps) { 19 | super(scope, id); 20 | addTemplateTag(this, 'UsagePlans'); 21 | this.usagePlanBasicTier = props.apiGateway.addUsagePlan('UsagePlanBasicTier', { 22 | quota: { 23 | limit: 1000, 24 | period: Period.DAY 25 | }, 26 | throttle: { 27 | burstLimit: 50, 28 | rateLimit: 50 29 | } 30 | }); 31 | 32 | this.usagePlanBasicTier.addApiKey( 33 | ApiKey.fromApiKeyId(this, 'ApiKeyBasic', props.apiKeyIdBasicTier) 34 | ); 35 | 36 | this.usagePlanAdvancedTier = props.apiGateway.addUsagePlan('UsagePlanAdvancedTier', { 37 | quota: { 38 | limit: 2000, 39 | period: Period.DAY 40 | }, 41 | throttle: { 42 | burstLimit: 100, 43 | rateLimit: 75 44 | } 45 | }); 46 | 47 | this.usagePlanAdvancedTier.addApiKey( 48 | ApiKey.fromApiKeyId(this, 'ApiKeyAdvanced', props.apiKeyIdAdvancedTier) 49 | ); 50 | 51 | this.usagePlanPremiumTier = props.apiGateway.addUsagePlan('UsagePlanPremiumTier', { 52 | quota: { 53 | limit: 6000, 54 | period: Period.DAY 55 | }, 56 | throttle: { 57 | burstLimit: 300, 58 | rateLimit: 300 59 | } 60 | }); 61 | 62 | this.usagePlanPremiumTier.addApiKey( 63 | ApiKey.fromApiKeyId(this, 'ApiKeyPremium', props.apiKeyIdPremiumTier) 64 | ); 65 | 66 | for (const usagePlanTier of [ 67 | this.usagePlanBasicTier, 68 | this.usagePlanAdvancedTier, 69 | this.usagePlanPremiumTier 70 | ]) { 71 | usagePlanTier.addApiStage({ 72 | api: props.apiGateway, 73 | stage: props.apiGateway.deploymentStage 74 | }); 75 | } 76 | 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /server/lib/tenant-template/ecs-dynamodb.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 3 | import { Construct } from 'constructs'; 4 | 5 | export interface EcsDynamoDBProps { 6 | name: string 7 | partitionKey: string 8 | sortKey: string 9 | tableName: string 10 | tenantName: string 11 | } 12 | 13 | export class EcsDynamoDB extends Construct { 14 | public readonly table: dynamodb.Table; 15 | public readonly policyDocument: cdk.aws_iam.PolicyDocument; 16 | 17 | constructor (scope: Construct, id: string, props: EcsDynamoDBProps) { 18 | super(scope, id); 19 | 20 | this.table = new dynamodb.Table(this, `${props.tableName}`, { 21 | tableName: `${props.tableName}`, 22 | billingMode: dynamodb.BillingMode.PROVISIONED, 23 | // readCapacity: 5, writeCapacity: 5, 24 | partitionKey: { name: props.partitionKey, type: dynamodb.AttributeType.STRING }, 25 | sortKey: { name: props.sortKey, type: dynamodb.AttributeType.STRING }, 26 | removalPolicy: cdk.RemovalPolicy.DESTROY, 27 | pointInTimeRecovery: true 28 | }); 29 | cdk.Tags.of(this.table).add('TenantName', props.tenantName); 30 | 31 | this.policyDocument = new cdk.aws_iam.PolicyDocument({ 32 | statements: [new cdk.aws_iam.PolicyStatement({ 33 | actions: ['dynamodb:GetItem', 'dynamodb:PutItem', 34 | 'dynamodb:UpdateItem', 'dynamodb:DeleteItem', 'dynamodb:Query'], 35 | resources: [this.table.tableArn], 36 | effect: cdk.aws_iam.Effect.ALLOW 37 | })] 38 | }); 39 | 40 | new cdk.CfnOutput(this, `${props.name}TableName`, { 41 | value: this.table.tableName 42 | }); 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/lib/utilities/destroy-policy-setter.ts: -------------------------------------------------------------------------------- 1 | import { type IConstruct } from 'constructs'; 2 | import { CfnResource, type IAspect, RemovalPolicy } from 'aws-cdk-lib'; 3 | 4 | export class DestroyPolicySetter implements IAspect { 5 | public visit (node: IConstruct): void { 6 | if (node instanceof CfnResource) { 7 | node.applyRemovalPolicy(RemovalPolicy.DESTROY); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecs-saas-ref-template", 3 | "version": "0.1.0", 4 | "bin": { 5 | "ecs-saas-ref-template": "bin/ecs-saas-ref-template.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "cdk": "cdk" 11 | }, 12 | "devDependencies": { 13 | "@types/jest": "^29.5.1", 14 | "@types/node": "20.1.7", 15 | "ts-node": "^10.9.1", 16 | "typescript": "~5.1.3" 17 | }, 18 | "dependencies": { 19 | "@aws-cdk/aws-lambda-python-alpha": "2.140.0-alpha.0", 20 | "@aws-sdk/client-dynamodb": "^3.651.1", 21 | "@aws-sdk/client-rds": "^3.654.0", 22 | "@aws-sdk/client-secrets-manager": "^3.654.0", 23 | "@cdklabs/sbt-aws": "0.5.24", 24 | "aws-cdk-lib": "2.140.0", 25 | "aws-lambda": "^1.0.7", 26 | "constructs": "10.0.5", 27 | "esbuild": "^0.23.1", 28 | "mysql2": "^3.11.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "commonjs", 5 | "lib": ["es2022", "dom"], 6 | "moduleResolution": "NodeNext", 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["node_modules", "cdk.out"] 23 | } 24 | --------------------------------------------------------------------------------