├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── azure-infra-cicd.yml │ ├── functions-api-cicd.yml │ └── spa-cicd.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── deploy ├── api.bicep ├── content │ ├── api-policy.xml │ └── cos-policy.xml ├── main.bicep ├── modules │ ├── apim.bicep │ ├── apimAPI.bicep │ ├── apimOpenAPI.bicep │ ├── cdn.bicep │ ├── cosmosdb.bicep │ ├── function.bicep │ ├── keyVault.bicep │ ├── newKeyVault.bicep │ └── staticWebsite.bicep └── scripts │ └── appRegistrationAndPermission.ps1 ├── media ├── serverless-web-app.png └── serverless-web-app.svg ├── src ├── api │ └── dotnet │ │ └── ToDoFunctionApp │ │ ├── .gitignore │ │ ├── Helpers │ │ ├── Constants.cs │ │ └── Startup.cs │ │ ├── Models │ │ └── TodoItem.cs │ │ ├── TodoList.cs │ │ ├── TodoListFn.csproj │ │ ├── authorization.json │ │ └── host.json └── client │ └── angular │ └── ToDoSpa │ ├── .editorconfig │ ├── .gitignore │ ├── README.md │ ├── angular.json │ ├── browserslist │ ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.json │ ├── karma.conf.js │ ├── package.json │ ├── src │ ├── app │ │ ├── app-config.json │ │ ├── app-routing.module.ts │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── home │ │ │ ├── home.component.css │ │ │ ├── home.component.html │ │ │ └── home.component.ts │ │ ├── todo-edit │ │ │ ├── todo-edit.component.css │ │ │ ├── todo-edit.component.html │ │ │ └── todo-edit.component.ts │ │ ├── todo-view │ │ │ ├── todo-view.component.css │ │ │ ├── todo-view.component.html │ │ │ └── todo-view.component.ts │ │ ├── todo.service.ts │ │ └── todo.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.svg │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── tslint.json └── workflows ├── azure-infra-cicd.yml ├── functions-api-cicd.yml └── spa-cicd.yml /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.github/workflows/azure-infra-cicd.yml: -------------------------------------------------------------------------------- 1 | name: Create Azure Resource (IaC) 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | AZURE_REGION: 6 | description: 'Azure Region to deploy Azure resources' 7 | required: true 8 | default: 'azure-region' 9 | ENVIRONMENT_TYPE: 10 | description: 'Environment: dev, test, or prod' 11 | required: true 12 | default: 'dev' 13 | APP_NAME_PREFIX: 14 | description: 'Prefix to be used in naming Azure resources' 15 | required: true 16 | default: 'prefix' 17 | RESOURCE_GROUP_NAME: 18 | description: 'Resource Group to deploy Azure resources' 19 | required: true 20 | default: 'resource-group' 21 | MSI_NAME: 22 | description: 'User Managed Identity' 23 | required: true 24 | default: 'user-msi' 25 | MSI_RESOURCE_GROUP: 26 | description: 'Resource Group where User Managed Identity is located' 27 | required: true 28 | default: 'msi-resource-group' 29 | 30 | # CONFIGURATION 31 | # For help, go to https://github.com/Azure/Actions 32 | # 33 | # 1. Set up the following secrets in your repository: 34 | # AZURE_CREDENTIALS 35 | # 36 | # 2. Change below variables for your configuration: 37 | env: 38 | AZURE_REGION: ${{ github.event.inputs.AZURE_REGION }} 39 | ENVIRONMENT_TYPE: ${{ github.event.inputs.ENVIRONMENT_TYPE }} 40 | APP_NAME_PREFIX: ${{ github.event.inputs.APP_NAME_PREFIX }} 41 | RESOURCE_GROUP_NAME: ${{ github.event.inputs.RESOURCE_GROUP_NAME }} 42 | MSI_NAME: ${{ github.event.inputs.MSI_NAME }} 43 | MSI_RESOURCE_GROUP: ${{ github.event.inputs.MSI_RESOURCE_GROUP }} 44 | BICEP_FILE_PATH: 'deploy' 45 | BICEP_FILE_NAME: 'main' 46 | 47 | jobs: 48 | validate_deploy: 49 | runs-on: ubuntu-latest 50 | steps: 51 | # Authentication 52 | # Set up the following secrets in your repository: AZURE_CREDENTIALS 53 | # For details on usage of secrets, please refer https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets 54 | - name: Azure Login 55 | uses: azure/login@v1 56 | with: 57 | creds: ${{ secrets.AZURE_CREDENTIALS }} 58 | 59 | # Checkout 60 | - name: Checkout 61 | uses: actions/checkout@v1 62 | 63 | # Build ARM Template from Bicep and create a target Azure resource group 64 | - name: Azure CLI - Validate Bicep file ${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.bicep 65 | uses: Azure/cli@1.0.4 66 | with: 67 | # Azure CLI version to be used to execute the script. If not provided, latest version is used 68 | azcliversion: 2.27.2 69 | # Specify the script here 70 | inlineScript: | 71 | az group create -l ${{ env.AZURE_REGION }} -n ${{ env.RESOURCE_GROUP_NAME }} 72 | az deployment group validate -g ${{ env.APP_NAME_PREFIX }}-${{ env.ENVIRONMENT_TYPE }}-rg --template-file ./${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.bicep 73 | az bicep upgrade 74 | az bicep build --file ./${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.bicep 75 | 76 | # Deployment Bicep template 77 | - name: Deploy ${{ env.ENVIRONMENT_TYPE }} environment infrastructure to ${{ env.RESOURCE_GROUP_NAME }} 78 | id: infraDeployment 79 | uses: azure/arm-deploy@v1 80 | with: 81 | deploymentName: ${{ github.run_number }} 82 | resourceGroupName: ${{ env.RESOURCE_GROUP_NAME }} 83 | template: ./${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.json # Set this to the location of your template file 84 | parameters: appNameSuffix=${{ env.APP_NAME_PREFIX }} environmentType=${{ env.ENVIRONMENT_TYPE }} userAssignedIdentityName=${{ env.MSI_NAME }} userAssignedIdentityResourceGroup=${{ env.MSI_RESOURCE_GROUP }} 85 | 86 | # Azure logout 87 | - name: logout 88 | run: | 89 | az logout 90 | if: always() 91 | -------------------------------------------------------------------------------- /.github/workflows/functions-api-cicd.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish .NET Functions 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | ENVIRONMENT_TYPE: 6 | description: 'Environment: dev, test, or prod' 7 | required: true 8 | default: 'dev' 9 | APP_NAME_PREFIX: 10 | description: 'Prefix to be used in naming Azure resources' 11 | required: true 12 | default: 'prefix' 13 | RESOURCE_GROUP_NAME: 14 | description: 'Resource Group to deploy Azure resources' 15 | required: true 16 | default: 'resource-group' 17 | API_NAME: 18 | description: 'API name' 19 | required: true 20 | default: '2do' 21 | API_DOCUMENT_URL: 22 | description: 'API definition URL' 23 | required: true 24 | default: 'https://.azurewebsites.net/api/swagger.json' 25 | APIM_NAME: 26 | description: 'APIM name' 27 | required: true 28 | default: 'apim-name' 29 | FUNCTION_NAME: 30 | description: 'Azure Functions name' 31 | required: true 32 | default: 'function-name' 33 | ORIGIN_URL: 34 | description: 'Client app URL' # This is CDN endpoint URL 35 | required: true 36 | default: 'https://.azureedge.net' 37 | 38 | # CONFIGURATION 39 | # For help, go to https://github.com/Azure/Actions 40 | # 41 | # 1. Set up the following secrets in your repository: 42 | # AZURE_CREDENTIALS 43 | # 44 | # 2. Change below variables for your configuration: 45 | env: 46 | ENVIRONMENT_TYPE: ${{ github.event.inputs.ENVIRONMENT_TYPE }} 47 | APP_NAME_PREFIX: ${{ github.event.inputs.APP_NAME_PREFIX }} 48 | RESOURCE_GROUP_NAME: ${{ github.event.inputs.RESOURCE_GROUP_NAME }} 49 | API_NAME: ${{ github.event.inputs.API_NAME }} 50 | API_DOCUMENT_URL: ${{ github.event.inputs.API_DOCUMENT_URL }} 51 | APIM_NAME: ${{ github.event.inputs.APIM_NAME }} 52 | FUNCTION_NAME: ${{ github.event.inputs.FUNCTION_NAME }} 53 | ORIGIN_URL: ${{ github.event.inputs.ORIGIN_URL }} 54 | APP_SOURCE_PATH: 'src' 55 | FUNCTIONAPP_PATH: 'api/dotnet/ToDoFunctionApp' 56 | DOTNET_VERSION: '3.1.410' 57 | BICEP_FILE_PATH: 'deploy' 58 | BICEP_FILE_NAME: 'api' 59 | 60 | jobs: 61 | function_cicd: 62 | runs-on: ubuntu-latest 63 | steps: 64 | # Authentication 65 | # Set up the following secrets in your repository: AZURE_CREDENTIALS 66 | # For details on usage of secrets, please refer https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets 67 | - name: Azure Login 68 | uses: azure/login@v1 69 | with: 70 | creds: ${{ secrets.AZURE_CREDENTIALS }} 71 | 72 | # Checkout 73 | - name: Checkout 74 | uses: actions/checkout@v1 75 | 76 | # Setup .NET Core environment 77 | - name: Setup DotNet ${{ env.DOTNET_VERSION }} Environment 78 | uses: actions/setup-dotnet@v1 79 | with: 80 | dotnet-version: ${{ env.DOTNET_VERSION }} 81 | 82 | # Build .NET application 83 | - name: 'Build .NET application' 84 | shell: bash 85 | run: | 86 | pushd ./${{ env.APP_SOURCE_PATH }}/${{ env.FUNCTIONAPP_PATH }} 87 | dotnet build --configuration Release --output ./outputs 88 | popd 89 | 90 | # Publish .NET application to Azure Function 91 | - name: Publish to Azure Functions to ${{ env.FUNCTION_NAME }} 92 | uses: Azure/functions-action@v1 93 | id: fa 94 | with: 95 | app-name: ${{ env.FUNCTION_NAME }} 96 | package: ./${{ env.APP_SOURCE_PATH }}/${{ env.FUNCTIONAPP_PATH }}/outputs 97 | 98 | # Validate and Build ARM Template from Bicep 99 | - name: Azure CLI - Validate Bicep file ${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.bicep 100 | uses: Azure/cli@1.0.4 101 | with: 102 | # Azure CLI version to be used to execute the script. If not provided, latest version is used 103 | azcliversion: 2.27.2 104 | # Specify the script here 105 | inlineScript: | 106 | az deployment group validate -g ${{ env.RESOURCE_GROUP_NAME }} --template-file ./${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.bicep 107 | az bicep upgrade 108 | az bicep build --file ./${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.bicep 109 | 110 | # Deployment Bicep template for APIM API 111 | - name: Import ${{ env.ENVIRONMENT_TYPE }} environment API to ${{ env.APIM_NAME }} 112 | id: apiDeployment 113 | uses: azure/arm-deploy@v1 114 | with: 115 | deploymentName: '${{ github.run_number }}-api' 116 | resourceGroupName: ${{ env.RESOURCE_GROUP_NAME }} 117 | template: ./${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.json # Set this to the location of your template file 118 | parameters: apimName=${{ env.APIM_NAME }} openApiUrl=${{ env.API_DOCUMENT_URL }} originUrl=${{ env.ORIGIN_URL }} apimApiName=${{ env.API_NAME }} 119 | 120 | # Azure logout 121 | - name: logout 122 | run: | 123 | az logout 124 | if: always() 125 | -------------------------------------------------------------------------------- /.github/workflows/spa-cicd.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Angular (SPA) 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | ENVIRONMENT_TYPE: 6 | description: 'Environment: dev, test, or prod' 7 | required: true 8 | default: 'dev' 9 | APP_NAME_PREFIX: 10 | description: 'Prefix to be used in naming Azure resources' 11 | required: true 12 | default: 'prefix' 13 | RESOURCE_GROUP_NAME: 14 | description: 'Resource Group to deploy Azure resources' 15 | required: true 16 | default: 'resource-group' 17 | CLIENT_URL: 18 | description: 'Client URL' 19 | required: true 20 | default: 'https://.azureedge.net' 21 | API_URL: 22 | description: 'API on APIM URL' 23 | required: true 24 | default: 'https://.azure-api.net/' 25 | AZURE_STORAGE_NAME: 26 | description: 'Azure storage account name' 27 | required: true 28 | default: 'storageaccountname' 29 | CDN_PROFILE_NAME: 30 | description: 'CDN profile name' 31 | required: true 32 | default: 'cdn-profile-name' 33 | CDN_ENDPOINT_NAME: 34 | description: 'CDN endpoint name' 35 | required: true 36 | default: 'cdn-endpoint-name' 37 | 38 | # CONFIGURATION 39 | # For help, go to https://github.com/Azure/Actions 40 | # 41 | # 1. Set up the following secrets in your repository: 42 | # AZURE_CREDENTIALS 43 | # 44 | # 2. Change below variables for your configuration: 45 | env: 46 | ENVIRONMENT_TYPE: ${{ github.event.inputs.ENVIRONMENT_TYPE }} 47 | APP_NAME_PREFIX: ${{ github.event.inputs.APP_NAME_PREFIX }} 48 | RESOURCE_GROUP_NAME: ${{ github.event.inputs.RESOURCE_GROUP_NAME }} 49 | CLIENT_URL: ${{ github.event.inputs.CLIENT_URL }} 50 | API_URL: ${{ github.event.inputs.API_URL }} 51 | AZURE_STORAGE_NAME: ${{ github.event.inputs.AZURE_STORAGE_NAME }} 52 | CDN_PROFILE_NAME: ${{ github.event.inputs.CDN_PROFILE_NAME }} 53 | CDN_ENDPOINT_NAME: ${{ github.event.inputs.CDN_ENDPOINT_NAME }} 54 | APP_SOURCE_PATH: 'src' 55 | ANGULAR_PATH: 'client/angular/ToDoSpa' 56 | NODE_VERSION: '14' 57 | BICEP_FILE_PATH: 'deploy' 58 | 59 | jobs: 60 | angular_cicd: 61 | runs-on: ubuntu-latest 62 | steps: 63 | # Authentication 64 | # Set up the following secrets in your repository: AZURE_CREDENTIALS 65 | # For details on usage of secrets, please refer https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets 66 | - name: Azure Login 67 | uses: azure/login@v1 68 | with: 69 | creds: ${{ secrets.AZURE_CREDENTIALS }} 70 | 71 | # Checkout 72 | - name: Checkout 73 | uses: actions/checkout@v1 74 | 75 | # Run app registration against AAD using PowerShell script 76 | - name: 'App Registration' 77 | id: appRegistration 78 | continue-on-error: true 79 | shell: pwsh 80 | run: | 81 | .\${{ env.BICEP_FILE_PATH }}\scripts\appRegistrationAndPermission.ps1 ` 82 | -clientName ${{ env.APP_NAME_PREFIX }}${{ env.ENVIRONMENT_TYPE }} ` 83 | -apiName fn-${{ env.APP_NAME_PREFIX }}-${{ env.ENVIRONMENT_TYPE }} ` 84 | -resourceGroup ${{ env.APP_NAME_PREFIX }}-${{ env.ENVIRONMENT_TYPE }}-rg ` 85 | -staticWebURL https://${{ env.APP_NAME_PREFIX }}-${{ env.ENVIRONMENT_TYPE }}.azureedge.net 86 | 87 | # Set app configurations of Angular 88 | - name: 'Replace tokens' 89 | uses: cschleiden/replace-tokens@v1.0 90 | with: 91 | tokenPrefix: '__' 92 | tokenSuffix: '__' 93 | files: ${{ github.workspace }}/${{ env.APP_SOURCE_PATH }}/${{ env.ANGULAR_PATH }}/src/app/app-config.json 94 | env: 95 | clientAppId: ${{ steps.appRegistration.outputs.clientId }} 96 | clientAppURL: ${{ env.CLIENT_URL }} 97 | apimURL: ${{ env.API_URL }} 98 | backendAPIScope: ${{ steps.appRegistration.outputs.scope }} 99 | tenantDomainName: ${{ steps.appRegistration.outputs.tenantDomainName }} 100 | 101 | # Setup Node.js environment 102 | - name: Setup Node.js ${{ env.NODE_VERSION }} environment 103 | uses: actions/setup-node@v2 104 | with: 105 | node-version: ${{ env.NODE_VERSION }} 106 | 107 | # Build Angular application 108 | - name: Build Angular application 109 | run: | 110 | pushd ./${{ env.APP_SOURCE_PATH }}/${{ env.ANGULAR_PATH }} 111 | npm install 112 | npm install -g @angular/cli 113 | ng build -c=production --output-path=./dist 114 | popd 115 | 116 | # Deploy Angular application to Storage Account 117 | - name: Publish static website to Azure storage account ${{ env.AZURE_STORAGE_NAME }} 118 | uses: Azure/cli@1.0.4 119 | with: 120 | # Azure CLI version to be used to execute the script. If not provided, latest version is used 121 | azcliversion: 2.21.0 122 | # Specify the script here 123 | inlineScript: az storage blob upload-batch -s ./${{ env.APP_SOURCE_PATH }}/${{ env.ANGULAR_PATH }}/dist -d '$web' --account-name ${{ env.AZURE_STORAGE_NAME }} 124 | 125 | # Purge CDN endpoint 126 | - name: Purge CDN endpoint on ${{ env.CDN_ENDPOINT_NAME }} 127 | uses: Azure/cli@1.0.4 128 | with: 129 | azcliversion: 2.21.0 130 | inlineScript: | 131 | az cdn endpoint purge --content-paths "/*" --profile-name ${{ env.CDN_PROFILE_NAME }} --name ${{ env.CDN_ENDPOINT_NAME }} --resource-group ${{ env.RESOURCE_GROUP_NAME }} 132 | 133 | # Azure logout 134 | - name: logout 135 | run: | 136 | az logout 137 | if: always() 138 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | *.exclude 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Mono auto generated files 18 | mono_crash.* 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # StyleCop 66 | StyleCopReport.xml 67 | 68 | # Files built by Visual Studio 69 | *_i.c 70 | *_p.c 71 | *_h.h 72 | *.ilk 73 | *.meta 74 | *.obj 75 | *.iobj 76 | *.pch 77 | *.pdb 78 | *.ipdb 79 | *.pgc 80 | *.pgd 81 | *.rsp 82 | *.sbr 83 | *.tlb 84 | *.tli 85 | *.tlh 86 | *.tmp 87 | *.tmp_proj 88 | *_wpftmp.csproj 89 | *.log 90 | *.vspscc 91 | *.vssscc 92 | .builds 93 | *.pidb 94 | *.svclog 95 | *.scc 96 | 97 | # Chutzpah Test files 98 | _Chutzpah* 99 | 100 | # Visual C++ cache files 101 | ipch/ 102 | *.aps 103 | *.ncb 104 | *.opendb 105 | *.opensdf 106 | *.sdf 107 | *.cachefile 108 | *.VC.db 109 | *.VC.VC.opendb 110 | 111 | # Visual Studio profiler 112 | *.psess 113 | *.vsp 114 | *.vspx 115 | *.sap 116 | 117 | # Visual Studio Trace Files 118 | *.e2e 119 | 120 | # TFS 2012 Local Workspace 121 | $tf/ 122 | 123 | # Guidance Automation Toolkit 124 | *.gpState 125 | 126 | # ReSharper is a .NET coding add-in 127 | _ReSharper*/ 128 | *.[Rr]e[Ss]harper 129 | *.DotSettings.user 130 | 131 | # TeamCity is a build add-in 132 | _TeamCity* 133 | 134 | # DotCover is a Code Coverage Tool 135 | *.dotCover 136 | 137 | # AxoCover is a Code Coverage Tool 138 | .axoCover/* 139 | !.axoCover/settings.json 140 | 141 | # Visual Studio code coverage results 142 | *.coverage 143 | *.coveragexml 144 | 145 | # NCrunch 146 | _NCrunch_* 147 | .*crunch*.local.xml 148 | nCrunchTemp_* 149 | 150 | # MightyMoose 151 | *.mm.* 152 | AutoTest.Net/ 153 | 154 | # Web workbench (sass) 155 | .sass-cache/ 156 | 157 | # Installshield output folder 158 | [Ee]xpress/ 159 | 160 | # DocProject is a documentation generator add-in 161 | DocProject/buildhelp/ 162 | DocProject/Help/*.HxT 163 | DocProject/Help/*.HxC 164 | DocProject/Help/*.hhc 165 | DocProject/Help/*.hhk 166 | DocProject/Help/*.hhp 167 | DocProject/Help/Html2 168 | DocProject/Help/html 169 | 170 | # Click-Once directory 171 | publish/ 172 | 173 | # Publish Web Output 174 | *.[Pp]ublish.xml 175 | *.azurePubxml 176 | # Note: Comment the next line if you want to checkin your web deploy settings, 177 | # but database connection strings (with potential passwords) will be unencrypted 178 | *.pubxml 179 | *.publishproj 180 | 181 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 182 | # checkin your Azure Web App publish settings, but sensitive information contained 183 | # in these scripts will be unencrypted 184 | PublishScripts/ 185 | 186 | # NuGet Packages 187 | *.nupkg 188 | # NuGet Symbol Packages 189 | *.snupkg 190 | # The packages folder can be ignored because of Package Restore 191 | **/[Pp]ackages/* 192 | # except build/, which is used as an MSBuild target. 193 | !**/[Pp]ackages/build/ 194 | # Uncomment if necessary however generally it will be regenerated when needed 195 | #!**/[Pp]ackages/repositories.config 196 | # NuGet v3's project.json files produces more ignorable files 197 | *.nuget.props 198 | *.nuget.targets 199 | 200 | # Microsoft Azure Build Output 201 | csx/ 202 | *.build.csdef 203 | 204 | # Microsoft Azure Emulator 205 | ecf/ 206 | rcf/ 207 | 208 | # Windows Store app package directories and files 209 | AppPackages/ 210 | BundleArtifacts/ 211 | Package.StoreAssociation.xml 212 | _pkginfo.txt 213 | *.appx 214 | *.appxbundle 215 | *.appxupload 216 | 217 | # Visual Studio cache files 218 | # files ending in .cache can be ignored 219 | *.[Cc]ache 220 | # but keep track of directories ending in .cache 221 | !?*.[Cc]ache/ 222 | 223 | # Others 224 | ClientBin/ 225 | ~$* 226 | *~ 227 | *.dbmdl 228 | *.dbproj.schemaview 229 | *.jfm 230 | *.pfx 231 | *.publishsettings 232 | orleans.codegen.cs 233 | 234 | # Including strong name files can present a security risk 235 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 236 | #*.snk 237 | 238 | # Since there are multiple workflows, uncomment next line to ignore bower_components 239 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 240 | #bower_components/ 241 | 242 | # RIA/Silverlight projects 243 | Generated_Code/ 244 | 245 | # Backup & report files from converting an old project file 246 | # to a newer Visual Studio version. Backup files are not needed, 247 | # because we have git ;-) 248 | _UpgradeReport_Files/ 249 | Backup*/ 250 | UpgradeLog*.XML 251 | UpgradeLog*.htm 252 | ServiceFabricBackup/ 253 | *.rptproj.bak 254 | 255 | # SQL Server files 256 | *.mdf 257 | *.ldf 258 | *.ndf 259 | 260 | # Business Intelligence projects 261 | *.rdl.data 262 | *.bim.layout 263 | *.bim_*.settings 264 | *.rptproj.rsuser 265 | *- [Bb]ackup.rdl 266 | *- [Bb]ackup ([0-9]).rdl 267 | *- [Bb]ackup ([0-9][0-9]).rdl 268 | 269 | # Microsoft Fakes 270 | FakesAssemblies/ 271 | 272 | # GhostDoc plugin setting file 273 | *.GhostDoc.xml 274 | 275 | # Node.js 276 | .ntvs_analysis.dat 277 | node_modules/ 278 | package-lock.json 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | # Ionide (cross platform F# VS Code tools) working folder 352 | .ionide/ 353 | 354 | # VS Code settings folder 355 | vscode/ 356 | .vscode/ 357 | dist/ 358 | 359 | *.copy.* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Serverless Web Application Sample 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/Azure-Samples/serverless-web-application/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/Azure-Samples/serverless-web-application/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless web application 2 | 3 | This sample references an architecture of a [serverless web application](https://docs.microsoft.com/en-us/azure/architecture/reference-architectures/serverless/web-app). The application serves static Angular.JS content from Azure Blob Storage (Static Website), and implements REST APIs for CRUD of a to do list with Azure Functions. The API reads data from Cosmos DB and returns the results to the web app. The GitHub workflow uses Azure Bicep for Infrastructure as Code to deploy and configure Azure resources. 4 | 5 | ![Architecture Diagram](./media/serverless-web-app.png) 6 | 7 | ## Security 8 | 9 | The application uses MSAL.js (1.x) with Implicit flow to authenticate users. You can choose between Implicit flow and Authorization code flow for Single-page application (SPA) and API pattern. However, Authorization code flow is a recommended flow as it is more secure. The built-in authentication on Azure Functions is enabled for Authentication and authorization. In order for the authentication to work, the GitHub workflow uses AZ CLI to register the applications on Azure Active Directory and configure permissions between the SPA and API. Both Azure API Manager and Functions also implement CORS policy to allow only traffic from the client origin to access the API. Azure Functions has network access restriction is enabled to allow only traffic from API Management's IP address to make the request. To connect to Cosmos DB, Azure Functions uses Managed Identity to read connection strings stored in Azure Key Vault. 10 | 11 | ## Azure Functions HTTP Trigger and OpenAPI documents 12 | 13 | On APIM, there are two approaches to import Azure Functions as API. 14 | 15 | 1. Azure backend integration. 16 | 17 | This is done through adding backend services of your Functions. This uses Functions' app key to access functions. The Bicep module [apimAPI.bicep](./deploy/modules/apimAPI.bicep) demonstrates how deploy this. 18 | 19 | 1. OpenAPI specification. 20 | 21 | By default, Azure Functions HTTP Trigger does not follow OpenAPI standard. [OpenAPI extension](https://github.com/Azure/azure-functions-openapi-extension/blob/main/docs/enable-open-api-endpoints-in-proc.md) is required to enable OpenAPI documents. The Bicep module [apimOpenAPI.bicep](./deploy/modules/apimOpenAPI.bicep) demonstrates how deploy this. 22 | 23 | ## Prerequisites 24 | 25 | 1. GitHub account and repository. 26 | 1. Azure subscription. 27 | 1. [User-assigned managed identity (MSI)](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-manage-user-assigned-managed-identities?pivots=identity-mi-methods-azp#create-a-user-assigned-managed-identity) with Contributor role. This will be used for executing Deployment Scripts in Bicep. 28 | 1. A Service Principal with Contributor role at subscription scope. This is the identity that will be used to access the Azure resources from GitHub Action. If you don't have a Service Principal, create one by following [these steps](https://docs.microsoft.com/en-us/azure/developer/github/connect-from-azure). The Service Principal also requires [Read/Write permissions to Azure Graph API](https://docs.microsoft.com/en-us/graph/api/application-post-applications?view=graph-rest-1.0&tabs=http#permissions). 29 | 30 | ## About sample workflows: 31 | 32 | This repo contains three GitHub workflows: 33 | 34 | * [Create Azure Resource (IaC)](.github/workflows/azure-infra-cicd.yml) workflow validates Bicep files and creates Azure resources necessary to host the sample solution. The Bicep file will create the following resources as a pre-requisite to the next two workflows: 35 | 36 | - Azure API Management. 37 | - Azure CDN. 38 | - Azure Cosmos DB for MongolDB. 39 | - Azure Functions (Windows). 40 | - Azure Key Vault option to BYO. 41 | - Azure Storage Account for hosting Static Website. 42 | 43 | * [Build and publish .NET](.github/workflows/functions-api-cicd.yml) workflow build .NET Core application and publish it to Azure Function. It also import the HTTP Trigger Functions as API's to the API Management using Bicep. This requires that Functions must be able to generate an OpenAPI specification. 44 | 45 | * [Build and publish Angular (SPA)](.github/workflows/spa-cicd.yml) workflow build Angular application and publish it to Azure Storage Account as a static website. This workflow will register both client and API applications in Azure Active Directory tenant of your subscription for authentication. It also purge Azure CDN to refresh static web content. 46 | 47 | ## Setup an end-to-end CI/CD workflows: 48 | 49 | 1. Fork this repo to your GitHub account. 50 | 1. Clone the copy repo to your local machine. 51 | 1. Edit [workflow](./.github/workflows/serverless-api.yml); modify parameter values. 52 | 1. Optional parameters in [Bicep file](./deploy/main.bicep) can be edited. 53 | 1. Commit changes will automatically trigger the workflow to deploy Azure resources and applications. 54 | 55 | ## References 56 | 57 | * [Azure Bicep](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/overview) 58 | * [Protect a web API backend in Azure API Management](https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-protect-backend-with-aad) 59 | * [Host a RESTful API with CORS in Azure App Service](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-tutorial-rest-api) 60 | * [Authentication and Authorization flow single-page application](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-spa-overview) 61 | * [Azure Functions and App Services built-in authentication](https://docs.microsoft.com/en-us/azure/app-service/overview-authentication-authorization) 62 | * [API Management cross domain policies](https://docs.microsoft.com/en-us/azure/api-management/api-management-cross-domain-policies#AllowCrossDomainCalls) 63 | * [Azure Functions and App Services network access restriction](https://docs.microsoft.com/en-us/azure/app-service/networking-features#access-restrictions) 64 | * [Use Key Vault from App Service with Azure Managed Identity](https://docs.microsoft.com/en-us/samples/azure-samples/app-service-msi-keyvault-dotnet/keyvault-msi-appservice-sample/) 65 | 66 | ## License 67 | 68 | See [LICENSE](./LICENSE.md). 69 | 70 | ## Contributing 71 | 72 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. More details on how to contribute see [contributing guide](./CONTRIBUTING.md). 73 | -------------------------------------------------------------------------------- /deploy/api.bicep: -------------------------------------------------------------------------------- 1 | @description('APIM name') 2 | param apimName string 3 | 4 | @description('Open API Definition URL') 5 | param openApiUrl string 6 | 7 | @description('Static Website URL') 8 | param originUrl string 9 | 10 | @description('API friendly name') 11 | param apimApiName string = '2do' 12 | 13 | module apimOpenApi 'modules/apimOpenAPI.bicep' = { 14 | name: 'apimOpenAPI' 15 | params: { 16 | apimName: apimName 17 | openApiUrl: openApiUrl 18 | apiName: apimApiName 19 | originUrl: originUrl 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /deploy/content/api-policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /deploy/content/cos-policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | __ORIGIN__ 6 | 7 | 8 | * 9 | 10 | 11 |
*
12 |
13 | 14 |
*
15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 |
-------------------------------------------------------------------------------- /deploy/main.bicep: -------------------------------------------------------------------------------- 1 | @description('Suffix for naming resources') 2 | param appNameSuffix string = 'app${uniqueString(resourceGroup().id)}' 3 | 4 | @allowed([ 5 | 'dev' 6 | 'test' 7 | 'prod' 8 | ]) 9 | @description('Environment') 10 | param environmentType string = 'dev' 11 | 12 | @description('Do you want to create new APIM?') 13 | param createApim bool = true 14 | 15 | @description('APIM name') 16 | param apimName string = 'apim-${appNameSuffix}-${environmentType}' 17 | 18 | @description('APIM resource group') 19 | param apimResourceGroup string = resourceGroup().name 20 | 21 | @description('Do you want to create new vault?') 22 | param createKeyVault bool = true 23 | 24 | @description('Key Vault name') 25 | param keyVaultName string = 'kv-${appNameSuffix}-${environmentType}' 26 | 27 | @description('Key Vault resource group') 28 | param keyVaultResourceGroup string = resourceGroup().name 29 | 30 | @description('User assigned managed idenity name') 31 | param userAssignedIdentityName string = 'umsi-${appNameSuffix}-${environmentType}' 32 | 33 | @description('User assigned managed idenity resource group') 34 | param userAssignedIdentityResourceGroup string = resourceGroup().name 35 | 36 | @description('API friendly name') 37 | param apimApiName string = '2do' 38 | 39 | param resourceTags object = { 40 | ProjectType: 'Azure Serverless Web' 41 | Purpose: 'Demo' 42 | } 43 | 44 | var location = resourceGroup().location 45 | var staticWebsiteStorageAccountName = '${appNameSuffix}${environmentType}' 46 | var cdnProfileName = 'cdn-${appNameSuffix}-${environmentType}' 47 | var functionStorageAccountName = 'fn${appNameSuffix}${environmentType}' 48 | var functionAppName = 'fn-${appNameSuffix}-${environmentType}' 49 | var functionRuntime = 'dotnet' 50 | var appServicePlanName = 'asp-${appNameSuffix}-${environmentType}' 51 | var appInsightsName = 'ai-${appNameSuffix}-${environmentType}' 52 | var cosmosDbName = '${appNameSuffix}-${environmentType}' 53 | var cosmosDbAccountName = 'cosmos-${appNameSuffix}-${environmentType}' 54 | 55 | // SKUs 56 | var functionSku = environmentType == 'prod' ? 'EP1' : 'Y1' 57 | var apimSku = environmentType == 'prod' ? 'Standard' : 'Developer' 58 | 59 | // static values 60 | var cosmosDbCollectionName = 'items' 61 | 62 | // Use existing User Assigned MSI. See https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/deployment-script-template#configure-the-minimum-permissions 63 | resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = { 64 | name: userAssignedIdentityName 65 | scope: resourceGroup(userAssignedIdentityResourceGroup) 66 | } 67 | 68 | resource appInsights 'Microsoft.Insights/components@2018-05-01-preview' = { 69 | name: appInsightsName 70 | location: location 71 | kind: 'web' 72 | properties: { 73 | Application_Type: 'web' 74 | publicNetworkAccessForIngestion: 'Enabled' 75 | publicNetworkAccessForQuery: 'Enabled' 76 | } 77 | } 78 | 79 | module staticWebsite 'modules/staticWebsite.bicep' = { 80 | name: 'staticWebsite' 81 | params: { 82 | storageAccountName: staticWebsiteStorageAccountName 83 | deploymentScriptServicePrincipalId: userAssignedIdentity.id 84 | resourceTags: resourceTags 85 | } 86 | } 87 | 88 | module cdn 'modules/cdn.bicep' = { 89 | name: 'cdn' 90 | params: { 91 | cdnProfileName: cdnProfileName 92 | staticWebsiteURL: staticWebsite.outputs.staticWebsiteURL 93 | } 94 | } 95 | 96 | module cosmosDB 'modules/cosmosdb.bicep' = { 97 | name: 'cosmosdb' 98 | params: { 99 | accountName: cosmosDbAccountName 100 | databaseName: cosmosDbName 101 | collectionName: cosmosDbCollectionName 102 | } 103 | } 104 | 105 | module functionApp 'modules/function.bicep' = { 106 | name: 'functionApp' 107 | params: { 108 | functionRuntime: functionRuntime 109 | functionSku: functionSku 110 | storageAccountName: functionStorageAccountName 111 | functionAppName: functionAppName 112 | appServicePlanName: appServicePlanName 113 | appInsightsInstrumentationKey: appInsights.properties.InstrumentationKey 114 | staticWebsiteURL: staticWebsite.outputs.staticWebsiteURL 115 | cosmosAccountName: cosmosDbAccountName 116 | cosmosDbName: cosmosDbName 117 | cosmosDbCollectionName: cosmosDbCollectionName 118 | keyVaultName: keyVaultName 119 | apimIPAddress: apim.outputs.apiIPAddress 120 | resourceTags: resourceTags 121 | } 122 | } 123 | 124 | module keyVault 'modules/keyVault.bicep' = if (!createKeyVault) { 125 | name: 'keyVault' 126 | scope: resourceGroup(keyVaultResourceGroup) 127 | params: { 128 | keyVaultName: keyVaultName 129 | functionAppName: functionApp.outputs.functionAppName 130 | cosmosAccountName: cosmosDB.outputs.cosmosDBAccountName 131 | deploymentScriptServicePrincipalId: userAssignedIdentity.id 132 | currentResourceGroup: resourceGroup().name 133 | } 134 | } 135 | 136 | module newKeyVault 'modules/newKeyVault.bicep' = if (createKeyVault) { 137 | name: 'newKeyVault' 138 | params: { 139 | keyVaultName: keyVaultName 140 | functionAppName: functionApp.outputs.functionAppName 141 | cosmosAccountName: cosmosDB.outputs.cosmosDBAccountName 142 | deploymentScriptServicePrincipalId: userAssignedIdentity.id 143 | resourceTags: resourceTags 144 | } 145 | } 146 | 147 | module apim 'modules/apim.bicep' = if (createApim) { 148 | name: 'apim' 149 | params: { 150 | apimName: apimName 151 | appInsightsName: appInsightsName 152 | appInsightsInstrumentationKey: appInsights.properties.InstrumentationKey 153 | sku: apimSku 154 | resourceTags: resourceTags 155 | } 156 | } 157 | 158 | module apimApi 'modules/apimAPI.bicep' = { 159 | name: 'apimAPI' 160 | scope: resourceGroup(apimResourceGroup) 161 | params: { 162 | apimName: apimName 163 | currentResourceGroup: resourceGroup().name 164 | backendApiName: functionApp.outputs.functionAppName 165 | apiName: apimApiName 166 | originUrl: cdn.outputs.cdnEndpointURL 167 | } 168 | } 169 | 170 | output functionAppName string = functionApp.outputs.functionAppName 171 | output apiUrl string = '${apim.outputs.gatewayUrl}/${apimApiName}' 172 | output staticWebsiteStorageAccountName string = staticWebsiteStorageAccountName 173 | output staticWebsiteUrl string = staticWebsite.outputs.staticWebsiteURL 174 | output apimName string = apimName 175 | output cdnEndpointName string = cdn.outputs.cdnEndpointName 176 | output cdnProfileName string = cdn.outputs.cdnProfileName 177 | output cdnEndpointURL string = cdn.outputs.cdnEndpointURL 178 | -------------------------------------------------------------------------------- /deploy/modules/apim.bicep: -------------------------------------------------------------------------------- 1 | @description('API Management DB account name') 2 | param apimName string 3 | param appInsightsName string 4 | param appInsightsInstrumentationKey string 5 | param resourceTags object 6 | 7 | @allowed([ 8 | 'Consumption' 9 | 'Developer' 10 | 'Basic' 11 | 'Standard' 12 | 'Premium' 13 | ]) 14 | @description('The pricing tier of this API Management service') 15 | param sku string = 'Developer' 16 | 17 | @description('The instance size of this API Management service.') 18 | @minValue(1) 19 | param skuCount int = 1 20 | 21 | var location = resourceGroup().location 22 | var publisherEmail = 'email@domain.com' 23 | var publisherName = 'Your Company' 24 | 25 | resource apiManagement 'Microsoft.ApiManagement/service@2021-01-01-preview' = { 26 | name: apimName 27 | location: location 28 | tags: resourceTags 29 | sku: { 30 | name: sku 31 | capacity: skuCount 32 | } 33 | properties: { 34 | publisherEmail: publisherEmail 35 | publisherName: publisherName 36 | } 37 | identity: { 38 | type: 'SystemAssigned' 39 | } 40 | } 41 | 42 | resource apiManagementLogger 'Microsoft.ApiManagement/service/loggers@2021-01-01-preview' = { 43 | name: appInsightsName 44 | parent: apiManagement 45 | properties: { 46 | loggerType: 'applicationInsights' 47 | description: 'Logger resources to APIM' 48 | credentials: { 49 | instrumentationKey: appInsightsInstrumentationKey 50 | } 51 | } 52 | } 53 | 54 | resource apimInstanceDiagnostics 'Microsoft.ApiManagement/service/diagnostics@2021-01-01-preview' = { 55 | name: 'applicationinsights' 56 | parent: apiManagement 57 | properties: { 58 | loggerId: apiManagementLogger.id 59 | alwaysLog: 'allErrors' 60 | logClientIp: true 61 | sampling: { 62 | percentage: 100 63 | samplingType: 'fixed' 64 | } 65 | } 66 | } 67 | 68 | output gatewayUrl string = apiManagement.properties.gatewayUrl 69 | output apiIPAddress string = apiManagement.properties.publicIPAddresses[0] 70 | -------------------------------------------------------------------------------- /deploy/modules/apimAPI.bicep: -------------------------------------------------------------------------------- 1 | param apimName string 2 | param currentResourceGroup string 3 | param backendApiName string 4 | param apiName string 5 | param originUrl string 6 | 7 | var functionAppKeyName = '${backendApiName}-key' 8 | 9 | resource backendApiApp 'Microsoft.Web/sites@2021-01-15' existing = { 10 | name: backendApiName 11 | scope: resourceGroup(currentResourceGroup) 12 | } 13 | 14 | resource functionKey 'Microsoft.Web/sites/functions/keys@2021-01-15' existing = { 15 | name: '${backendApiName}/GetTodoItems/default' 16 | scope: resourceGroup(currentResourceGroup) 17 | } 18 | 19 | resource apim 'Microsoft.ApiManagement/service@2021-01-01-preview' existing = { 20 | name: apimName 21 | } 22 | 23 | resource namedValues 'Microsoft.ApiManagement/service/namedValues@2021-01-01-preview' = { 24 | parent: apim 25 | name: functionAppKeyName 26 | properties: { 27 | displayName: functionAppKeyName 28 | value: listKeys('${backendApiApp.id}/host/default','2019-08-01').functionKeys.default 29 | } 30 | } 31 | 32 | resource backendApi 'Microsoft.ApiManagement/service/backends@2021-01-01-preview' = { 33 | parent: apim 34 | name: backendApiName 35 | properties: { 36 | description: backendApiName 37 | resourceId: 'https://management.azure.com${backendApiApp.id}' 38 | credentials: { 39 | header:{ 40 | 'x-functions-key': [ 41 | '{{${namedValues.properties.displayName}}}' 42 | ] 43 | } 44 | } 45 | url: 'https://${backendApiApp.properties.hostNames[0]}/api' 46 | protocol: 'http' 47 | } 48 | } 49 | 50 | resource api 'Microsoft.ApiManagement/service/apis@2021-01-01-preview' = { 51 | parent: apim 52 | name: apiName 53 | properties: { 54 | path: apiName 55 | displayName: apiName 56 | isCurrent: true 57 | subscriptionRequired: false 58 | protocols: [ 59 | 'https' 60 | ] 61 | } 62 | } 63 | 64 | resource apiPolicy 'Microsoft.ApiManagement/service/apis/policies@2021-01-01-preview' = { 65 | parent: api 66 | name: 'policy' 67 | properties: { 68 | format: 'rawxml' 69 | value: replace(loadTextContent('../content/cos-policy.xml'),'__ORIGIN__',originUrl) 70 | } 71 | } 72 | 73 | resource opGetTodos 'Microsoft.ApiManagement/service/apis/operations@2021-01-01-preview' = { 74 | name: 'getTodoList' 75 | parent: api 76 | properties: { 77 | displayName: 'Get Todo List' 78 | method: 'GET' 79 | urlTemplate: '/todos' 80 | } 81 | } 82 | 83 | resource opGetTodosPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2021-01-01-preview' = { 84 | parent: opGetTodos 85 | name: 'policy' 86 | properties: { 87 | format: 'rawxml' 88 | value: replace(loadTextContent('../content/api-policy.xml'),'__BACKEND-ID__',backendApi.name) 89 | } 90 | } 91 | 92 | resource opGetTodosById 'Microsoft.ApiManagement/service/apis/operations@2021-01-01-preview' = { 93 | name: 'getTodoItem' 94 | parent: api 95 | properties: { 96 | displayName: 'Get Todo Item' 97 | method: 'GET' 98 | urlTemplate: '/todos/{id}' 99 | templateParameters: [ 100 | { 101 | name: 'id' 102 | required: true 103 | type: 'String' 104 | } 105 | ] 106 | } 107 | } 108 | 109 | resource opGetTodosByIdPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2021-01-01-preview' = { 110 | parent: opGetTodosById 111 | name: 'policy' 112 | properties: { 113 | format: 'rawxml' 114 | value: replace(loadTextContent('../content/api-policy.xml'),'__BACKEND-ID__',backendApi.name) 115 | } 116 | } 117 | 118 | resource opPostTodoItem 'Microsoft.ApiManagement/service/apis/operations@2021-01-01-preview' = { 119 | name: 'postTodoItem' 120 | parent: api 121 | properties: { 122 | displayName: 'Create Todo Item' 123 | method: 'POST' 124 | urlTemplate: '/todos' 125 | } 126 | } 127 | 128 | resource opPostTodoItemPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2021-01-01-preview' = { 129 | parent: opPostTodoItem 130 | name: 'policy' 131 | properties: { 132 | format: 'rawxml' 133 | value: replace(loadTextContent('../content/api-policy.xml'),'__BACKEND-ID__',backendApi.name) 134 | } 135 | } 136 | 137 | resource opPutTodosById 'Microsoft.ApiManagement/service/apis/operations@2021-01-01-preview' = { 138 | name: 'putTodoItem' 139 | parent: api 140 | properties: { 141 | displayName: 'Update Todo Item' 142 | method: 'PUT' 143 | urlTemplate: '/todos/{id}' 144 | templateParameters: [ 145 | { 146 | name: 'id' 147 | required: true 148 | type: 'String' 149 | } 150 | ] 151 | } 152 | } 153 | 154 | resource opPutTodosByIdPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2021-01-01-preview' = { 155 | parent: opPutTodosById 156 | name: 'policy' 157 | properties: { 158 | format: 'rawxml' 159 | value: replace(loadTextContent('../content/api-policy.xml'),'__BACKEND-ID__',backendApi.name) 160 | } 161 | } 162 | 163 | resource opDeleteTodosById 'Microsoft.ApiManagement/service/apis/operations@2021-01-01-preview' = { 164 | name: 'deleteTodoItem' 165 | parent: api 166 | properties: { 167 | displayName: 'Delete Todo Item' 168 | method: 'DELETE' 169 | urlTemplate: '/todos/{id}' 170 | templateParameters: [ 171 | { 172 | name: 'id' 173 | required: true 174 | type: 'String' 175 | } 176 | ] 177 | } 178 | } 179 | 180 | resource opDeleteTodosByIdPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2021-01-01-preview' = { 181 | parent: opDeleteTodosById 182 | name: 'policy' 183 | properties: { 184 | format: 'rawxml' 185 | value: replace(loadTextContent('../content/api-policy.xml'),'__BACKEND-ID__',backendApi.name) 186 | } 187 | } 188 | 189 | resource opHealthCheck 'Microsoft.ApiManagement/service/apis/operations@2021-01-01-preview' = { 190 | name: 'HealthCheck' 191 | parent: api 192 | properties: { 193 | displayName: 'Health Probe' 194 | method: 'HEAD' 195 | urlTemplate: '/todos' 196 | } 197 | } 198 | 199 | resource opHealthCheckPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2021-01-01-preview' = { 200 | parent: opHealthCheck 201 | name: 'policy' 202 | properties: { 203 | format: 'rawxml' 204 | value: replace(loadTextContent('../content/api-policy.xml'),'__BACKEND-ID__',backendApi.name) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /deploy/modules/apimOpenAPI.bicep: -------------------------------------------------------------------------------- 1 | param apimName string 2 | param openApiUrl string 3 | param apiName string 4 | param originUrl string 5 | 6 | resource apim 'Microsoft.ApiManagement/service@2021-01-01-preview' existing = { 7 | name: apimName 8 | } 9 | 10 | resource api 'Microsoft.ApiManagement/service/apis@2021-01-01-preview' = { 11 | parent: apim 12 | name: apiName 13 | properties: { 14 | path: apiName 15 | displayName: apiName 16 | isCurrent: true 17 | subscriptionRequired: false 18 | format: 'swagger-link-json' 19 | value: openApiUrl //'https://name.azurewebsites.net/api/swagger.json' 20 | protocols: [ 21 | 'https' 22 | ] 23 | } 24 | } 25 | 26 | resource apiPolicy 'Microsoft.ApiManagement/service/apis/policies@2021-01-01-preview' = { 27 | parent: api 28 | name: 'policy' 29 | properties: { 30 | format: 'rawxml' 31 | value: replace(loadTextContent('../content/cos-policy.xml'),'__ORIGIN__',originUrl) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /deploy/modules/cdn.bicep: -------------------------------------------------------------------------------- 1 | param cdnProfileName string 2 | param staticWebsiteURL string 3 | 4 | var endpointName = replace(cdnProfileName,'cdn-','') 5 | var staticWebsiteHostName = replace(replace(staticWebsiteURL,'https://',''),'/','') 6 | 7 | resource cdnProfile 'Microsoft.Cdn/profiles@2020-04-15' = { 8 | name: cdnProfileName 9 | location: resourceGroup().location 10 | sku: { 11 | name: 'Standard_Microsoft' 12 | } 13 | } 14 | 15 | resource endpoint 'Microsoft.Cdn/profiles/endpoints@2020-04-15' = { 16 | parent: cdnProfile 17 | name: endpointName 18 | location: resourceGroup().location 19 | properties: { 20 | originHostHeader: staticWebsiteHostName 21 | isHttpAllowed: false 22 | isHttpsAllowed: true 23 | queryStringCachingBehavior: 'IgnoreQueryString' 24 | optimizationType: 'GeneralWebDelivery' 25 | contentTypesToCompress: [ 26 | 'text/plain' 27 | 'text/html' 28 | 'text/css' 29 | 'text/javascript' 30 | 'application/x-javascript' 31 | 'application/javascript' 32 | 'application/json' 33 | 'application/xml' 34 | ] 35 | isCompressionEnabled: true 36 | origins: [ 37 | { 38 | name: replace(staticWebsiteHostName,'.','-') 39 | properties: { 40 | hostName: staticWebsiteHostName 41 | } 42 | } 43 | ] 44 | } 45 | } 46 | 47 | output cdnEndpointURL string = 'https://${endpoint.properties.hostName}' 48 | output cdnEndpointName string = endpoint.name 49 | output cdnProfileName string = cdnProfile.name 50 | -------------------------------------------------------------------------------- /deploy/modules/cosmosdb.bicep: -------------------------------------------------------------------------------- 1 | 2 | @description('Cosmos DB account name') 3 | param accountName string 4 | 5 | @description('Location for the Cosmos DB account.') 6 | param location string = resourceGroup().location 7 | 8 | @description('The name for the Core (MongoDB) database') 9 | param databaseName string 10 | 11 | @description('The name for the collection') 12 | param collectionName string 13 | 14 | resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2021-04-15' = { 15 | name: toLower(accountName) 16 | kind: 'MongoDB' 17 | location: location 18 | properties: { 19 | databaseAccountOfferType: 'Standard' 20 | consistencyPolicy: { 21 | defaultConsistencyLevel: 'Session' 22 | } 23 | locations: [ 24 | { 25 | locationName: location 26 | } 27 | ] 28 | } 29 | } 30 | 31 | resource cosmosDB 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2021-04-15' = { 32 | name: '${toLower(databaseName)}' 33 | parent: cosmosAccount 34 | properties: { 35 | resource: { 36 | id: databaseName 37 | } 38 | options: { 39 | throughput: 400 40 | } 41 | } 42 | } 43 | 44 | resource collection 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2021-06-15' = { 45 | name: '${toLower(collectionName)}' 46 | parent: cosmosDB 47 | properties: { 48 | resource: { 49 | id: collectionName 50 | } 51 | } 52 | } 53 | 54 | output cosmosDBAccountName string = cosmosAccount.name 55 | -------------------------------------------------------------------------------- /deploy/modules/function.bicep: -------------------------------------------------------------------------------- 1 | param location string = resourceGroup().location 2 | param functionRuntime string 3 | param functionSku string 4 | param storageAccountName string 5 | param functionAppName string 6 | param appServicePlanName string 7 | param appInsightsInstrumentationKey string 8 | param staticWebsiteURL string 9 | param cosmosAccountName string 10 | param cosmosDbName string 11 | param cosmosDbCollectionName string 12 | param keyVaultName string 13 | param apimIPAddress string 14 | param resourceTags object 15 | 16 | 17 | var functionTier = functionSku == 'Y1' ? 'Dynamic' : 'ElasticPremium' 18 | var functionKind = functionSku == 'Y1' ? 'functionapp' : 'elastic' 19 | var keyVaultSecretName = '${cosmosAccountName}-key' 20 | 21 | resource storageAccount 'Microsoft.Storage/storageAccounts@2019-06-01' = { 22 | name: storageAccountName 23 | location: location 24 | tags: resourceTags 25 | sku: { 26 | name: 'Standard_LRS' 27 | } 28 | kind: 'StorageV2' 29 | properties: { 30 | supportsHttpsTrafficOnly: true 31 | encryption: { 32 | services: { 33 | file: { 34 | keyType: 'Account' 35 | enabled: true 36 | } 37 | blob: { 38 | keyType: 'Account' 39 | enabled: true 40 | } 41 | } 42 | keySource: 'Microsoft.Storage' 43 | } 44 | accessTier: 'Hot' 45 | } 46 | } 47 | 48 | resource plan 'Microsoft.Web/serverFarms@2020-06-01' = { 49 | name: appServicePlanName 50 | location: location 51 | kind: functionKind 52 | tags: resourceTags 53 | sku: { 54 | name: functionSku 55 | tier: functionTier 56 | } 57 | properties: {} 58 | } 59 | 60 | resource functionApp 'Microsoft.Web/sites@2020-06-01' = { 61 | name: functionAppName 62 | location: location 63 | kind: 'functionapp' 64 | tags: resourceTags 65 | properties: { 66 | serverFarmId: plan.id 67 | siteConfig: { 68 | appSettings: [ 69 | { 70 | name: 'AzureWebJobsStorage' 71 | value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageAccount.id, storageAccount.apiVersion).keys[0].value}' 72 | } 73 | { 74 | name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' 75 | value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageAccount.id, storageAccount.apiVersion).keys[0].value}' 76 | } 77 | { 78 | name: 'APPINSIGHTS_INSTRUMENTATIONKEY' 79 | value: appInsightsInstrumentationKey 80 | } 81 | { 82 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' 83 | value: 'InstrumentationKey=${appInsightsInstrumentationKey}' 84 | } 85 | { 86 | name: 'FUNCTIONS_WORKER_RUNTIME' 87 | value: functionRuntime 88 | } 89 | { 90 | name: 'FUNCTIONS_EXTENSION_VERSION' 91 | value: '~3' 92 | } 93 | { 94 | name: 'KEY_VAULT_URI' 95 | value: 'https://${keyVaultName}.vault.azure.net' 96 | } 97 | { 98 | name: 'CONNECTION_STRINGS' 99 | value: keyVaultSecretName 100 | } 101 | { 102 | name: 'DATABASE_NAME' 103 | value: cosmosDbName 104 | } 105 | { 106 | name: 'COLLECTION_NAME' 107 | value: cosmosDbCollectionName 108 | } 109 | ] 110 | cors: { 111 | allowedOrigins: [ 112 | staticWebsiteURL 113 | ] 114 | } 115 | ipSecurityRestrictions: [ 116 | { 117 | ipAddress: '${apimIPAddress}/32' 118 | action: 'Allow' 119 | priority: 100 120 | name: 'APIM' 121 | description: 'Traffic from APIM' 122 | } 123 | { 124 | ipAddress: 'Any' 125 | action: 'Deny' 126 | priority: 2147483647 127 | name: 'Deny all' 128 | description: 'Deny all access' 129 | } 130 | ] 131 | } 132 | httpsOnly: true 133 | } 134 | identity: { 135 | type: 'SystemAssigned' 136 | } 137 | } 138 | 139 | output functionAppName string = functionApp.name 140 | -------------------------------------------------------------------------------- /deploy/modules/keyVault.bicep: -------------------------------------------------------------------------------- 1 | param keyVaultName string 2 | param functionAppName string 3 | param cosmosAccountName string 4 | param deploymentScriptServicePrincipalId string 5 | param currentResourceGroup string 6 | 7 | var keyVaultSecretName = '${cosmosAccountName}-key' 8 | 9 | resource functionApp 'Microsoft.Web/sites@2021-01-15' existing = { 10 | name: functionAppName 11 | scope: resourceGroup(currentResourceGroup) 12 | } 13 | 14 | resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = { 15 | name: keyVaultName 16 | } 17 | 18 | resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2021-06-01-preview' = { 19 | parent: keyVault 20 | name: 'add' 21 | properties: { 22 | accessPolicies: [ 23 | { 24 | tenantId: functionApp.identity.tenantId 25 | objectId: functionApp.identity.principalId 26 | permissions: { 27 | secrets: [ 28 | 'get' 29 | 'list' 30 | ] 31 | } 32 | } 33 | ] 34 | } 35 | } 36 | 37 | resource deploymentScripts 'Microsoft.Resources/deploymentScripts@2020-10-01' = { 38 | name: 'getConnectionString' 39 | kind: 'AzurePowerShell' 40 | location: resourceGroup().location 41 | identity:{ 42 | type: 'UserAssigned' 43 | userAssignedIdentities: { 44 | '${deploymentScriptServicePrincipalId}': {} 45 | } 46 | } 47 | properties: { 48 | azPowerShellVersion: '6.1' 49 | timeout: 'PT30M' 50 | arguments: '-accountName ${cosmosAccountName} -resourceGroup ${currentResourceGroup}' 51 | scriptContent: ''' 52 | param([string] $accountName, [string] $resourceGroup) 53 | $connectionStrings = Get-AzCosmosDBAccountKey ` 54 | -ResourceGroupName $resourceGroup ` 55 | -Name $accountName ` 56 | -Type "ConnectionStrings" 57 | $DeploymentScriptOutputs = @{} 58 | $DeploymentScriptOutputs['connectionString'] = $connectionStrings["Primary MongoDB Connection String"] 59 | ''' 60 | cleanupPreference: 'Always' 61 | retentionInterval: 'P1D' 62 | } 63 | } 64 | 65 | resource keyVaultSecrets 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = { 66 | parent: keyVault 67 | name : keyVaultSecretName 68 | properties: { 69 | value: deploymentScripts.properties.outputs.connectionString 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /deploy/modules/newKeyVault.bicep: -------------------------------------------------------------------------------- 1 | param keyVaultName string 2 | param functionAppName string 3 | param cosmosAccountName string 4 | param deploymentScriptServicePrincipalId string 5 | param resourceTags object 6 | 7 | var keyVaultSecretName = '${cosmosAccountName}-key' 8 | 9 | resource functionApp 'Microsoft.Web/sites@2021-01-15' existing = { 10 | name: functionAppName 11 | } 12 | 13 | resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { 14 | name: keyVaultName 15 | location: resourceGroup().location 16 | properties: { 17 | sku: { 18 | name: 'standard' 19 | family: 'A' 20 | } 21 | tenantId: subscription().tenantId 22 | accessPolicies: [ 23 | { 24 | tenantId: functionApp.identity.tenantId 25 | objectId: functionApp.identity.principalId 26 | permissions: { 27 | secrets: [ 28 | 'get' 29 | ] 30 | } 31 | } 32 | ] 33 | } 34 | tags: resourceTags 35 | } 36 | 37 | resource deploymentScripts 'Microsoft.Resources/deploymentScripts@2020-10-01' = { 38 | name: 'getConnectionString' 39 | kind: 'AzurePowerShell' 40 | location: resourceGroup().location 41 | identity:{ 42 | type: 'UserAssigned' 43 | userAssignedIdentities: { 44 | '${deploymentScriptServicePrincipalId}': {} 45 | } 46 | } 47 | properties: { 48 | azPowerShellVersion: '6.1' 49 | timeout: 'PT30M' 50 | arguments: '-accountName ${cosmosAccountName} -resourceGroup ${resourceGroup().name}' 51 | scriptContent: ''' 52 | param([string] $accountName, [string] $resourceGroup) 53 | $connectionStrings = Get-AzCosmosDBAccountKey ` 54 | -ResourceGroupName $resourceGroup ` 55 | -Name $accountName ` 56 | -Type "ConnectionStrings" 57 | $DeploymentScriptOutputs = @{} 58 | $DeploymentScriptOutputs['connectionString'] = $connectionStrings["Primary MongoDB Connection String"] 59 | ''' 60 | cleanupPreference: 'Always' 61 | retentionInterval: 'P1D' 62 | } 63 | } 64 | 65 | resource keyVaultSecrets 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = { 66 | parent: keyVault 67 | name : keyVaultSecretName 68 | properties: { 69 | value: deploymentScripts.properties.outputs.connectionString 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /deploy/modules/staticWebsite.bicep: -------------------------------------------------------------------------------- 1 | param storageAccountName string 2 | param resourceTags object 3 | param deploymentScriptServicePrincipalId string 4 | 5 | var location = resourceGroup().location 6 | 7 | resource storageAccount 'Microsoft.Storage/storageAccounts@2019-06-01' = { 8 | name: storageAccountName 9 | location: location 10 | tags: resourceTags 11 | sku: { 12 | name: 'Standard_LRS' 13 | tier: 'Standard' 14 | } 15 | kind: 'StorageV2' 16 | properties: { 17 | supportsHttpsTrafficOnly: true 18 | encryption: { 19 | services: { 20 | file: { 21 | keyType: 'Account' 22 | enabled: true 23 | } 24 | blob: { 25 | keyType: 'Account' 26 | enabled: true 27 | } 28 | } 29 | keySource: 'Microsoft.Storage' 30 | } 31 | accessTier: 'Hot' 32 | } 33 | } 34 | 35 | resource deploymentScripts 'Microsoft.Resources/deploymentScripts@2020-10-01' = { 36 | name: 'configStaticWeb' 37 | kind: 'AzurePowerShell' 38 | location: location 39 | identity:{ 40 | type: 'UserAssigned' 41 | userAssignedIdentities: { 42 | '${deploymentScriptServicePrincipalId}': {} 43 | } 44 | } 45 | properties: { 46 | azPowerShellVersion: '6.1' 47 | timeout: 'PT30M' 48 | arguments: '-storageAccount ${storageAccount.name} -resourceGroup ${resourceGroup().name}' 49 | scriptContent: ''' 50 | param([string] $storageAccount, [string] $resourceGroup) 51 | $storage = Get-AzStorageAccount -ResourceGroupName $resourceGroup -Name $storageAccount 52 | $ctx = $storage.Context 53 | Enable-AzStorageStaticWebsite -Context $ctx -IndexDocument index.html -ErrorDocument404Path notfound.html 54 | $output = $storage.PrimaryEndpoints.Web 55 | $output = $output.TrimEnd('/') 56 | $DeploymentScriptOutputs = @{} 57 | $DeploymentScriptOutputs['URL'] = $output 58 | ''' 59 | cleanupPreference: 'Always' 60 | retentionInterval: 'P1D' 61 | } 62 | } 63 | 64 | output staticWebsiteURL string = deploymentScripts.properties.outputs.URL 65 | -------------------------------------------------------------------------------- /deploy/scripts/appRegistrationAndPermission.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string] $clientName, 4 | [string] $apiName, 5 | [string] $resourceGroup, 6 | [string] $staticWebURL 7 | ) 8 | 9 | az config set extension.use_dynamic_install=yes_without_prompt 10 | 11 | # Create Client App 12 | $clientApp=$(az ad app create --display-name "${clientName}-staticwebapp" --native-app false --oauth2-allow-implicit-flow true --reply-urls $staticWebURL | ConvertFrom-Json) 13 | 14 | # Create API App 15 | $apiApp=$(az ad app create --display-name "${apiName}-app" --identifier-uris "api://${apiName}" --reply-urls "https://${apiName}.azurewebsites.net/.auth/login/aad/callback" | ConvertFrom-Json) 16 | 17 | $clientAppId=$clientApp.appId 18 | $apiAppId=$apiApp.appId 19 | $apiPermissionId=$apiApp.oauth2Permissions.id 20 | 21 | # Create service principal for api app and to grant permissions 22 | az ad sp create --id $apiAppId --only-show-errors 23 | 24 | az ad app permission add --id $clientAppId --api $apiAppId --api-permissions "${apiPermissionId}=Scope" 25 | az ad app permission add --id $apiAppId --api 00000003-0000-0000-c000-000000000000 --api-permissions "e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope" 26 | 27 | 28 | # Allow client app above to access API as a trusted service 29 | $secret=$(az ad app credential reset --id $apiApp.objectId --append --only-show-errors | ConvertFrom-Json) 30 | 31 | $tenantDomainName=$clientApp.publisherDomain 32 | 33 | $account=$(az account show | ConvertFrom-Json) 34 | $tenantId=$account.tenantId 35 | 36 | az webapp auth microsoft update -g $resourceGroup -n $apiName -y ` 37 | --allowed-audiences "api://${apiName}" ` 38 | --client-id $apiAppId ` 39 | --client-secret $secret.password ` 40 | --issuer "https://sts.windows.net/${tenantId}/v2.0" -o none 41 | 42 | az webapp auth update -g $resourceGroup -n $apiName --enabled true --action AllowAnonymous 43 | 44 | echo "::set-output name=clientId::${clientAppId}" 45 | echo "::set-output name=scope::api://${apiName}/user_impersonation" 46 | echo "::set-output name=tenantDomainName::https://login.microsoftonline.com/${tenantDomainName}" -------------------------------------------------------------------------------- /media/serverless-web-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/serverless-web-application/735b4d573ffa6865d53735a982fd453c344f4037/media/serverless-web-app.png -------------------------------------------------------------------------------- /media/serverless-web-app.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/dotnet/ToDoFunctionApp/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /src/api/dotnet/ToDoFunctionApp/Helpers/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace AzninjaTodoFn.Helpers 6 | { 7 | public class Constants 8 | { 9 | public const string kvUri = "KEY_VAULT_URI"; 10 | public const string kvSecretName = "CONNECTION_STRINGS"; 11 | public const string databaseName = "DATABASE_NAME"; 12 | public const string collectionName = "COLLECTION_NAME"; 13 | 14 | } 15 | } -------------------------------------------------------------------------------- /src/api/dotnet/ToDoFunctionApp/Helpers/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using Azure.Identity; 7 | using Azure.Security.KeyVault.Secrets; 8 | using MongoDB.Driver; 9 | using Azure; 10 | 11 | [assembly: FunctionsStartup(typeof(AzninjaTodoFn.Helpers.Startup))] 12 | namespace AzninjaTodoFn.Helpers 13 | { 14 | public class Startup : FunctionsStartup 15 | { 16 | public override void Configure(IFunctionsHostBuilder builder) 17 | { 18 | builder.Services.AddLogging(loggingBuilder => 19 | { 20 | loggingBuilder.AddFilter(level => true); 21 | }); 22 | 23 | builder.Services.AddHttpContextAccessor(); 24 | 25 | var config = new ConfigurationBuilder() 26 | .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true) 27 | .AddEnvironmentVariables() 28 | .Build(); 29 | 30 | builder.Services.AddSingleton((s) => 31 | { 32 | // Use System Managed Identity to get access to the Key Vault 33 | SecretClient kvClient = new SecretClient(new Uri(config[Constants.kvUri]), new DefaultAzureCredential()); 34 | Response secret = kvClient.GetSecret(config[Constants.kvSecretName]); 35 | MongoClient client = new MongoClient(secret.Value.Value); 36 | return client; 37 | }); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/api/dotnet/ToDoFunctionApp/Models/TodoItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | using MongoDB.Bson; 8 | using MongoDB.Bson.Serialization.Attributes; 9 | 10 | namespace AzninjaTodoFn.Models 11 | { 12 | public class TodoItem 13 | { 14 | [BsonId] 15 | public string Id { get; set; } 16 | 17 | [BsonElement("owner")] 18 | public string Owner { get; set; } 19 | 20 | [BsonElement("description")] 21 | public string Description { get; set; } 22 | 23 | [BsonElement("status")] 24 | public bool Status { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /src/api/dotnet/ToDoFunctionApp/TodoList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using System.Diagnostics; 7 | using System.Collections.Generic; 8 | using System.Security.Claims; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Azure.WebJobs; 12 | using Microsoft.Azure.WebJobs.Extensions.Http; 13 | using Microsoft.Azure.WebJobs.Host; 14 | using Microsoft.Extensions.Logging; 15 | using Microsoft.Extensions.Configuration; 16 | using Newtonsoft.Json; 17 | using MongoDB.Driver; 18 | using AzninjaTodoFn.Models; 19 | using AzninjaTodoFn.Helpers; 20 | using Microsoft.OpenApi.Models; 21 | using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; 22 | using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums; 23 | 24 | namespace AzninjaTodoFn 25 | { 26 | public class TodoList 27 | { 28 | 29 | private readonly ILogger _logger; 30 | private readonly IConfiguration _config; 31 | private MongoClient _client; 32 | private readonly IMongoCollection _todolist; 33 | private string _user; 34 | 35 | public TodoList(ILogger logger, IConfiguration config, MongoClient client, IHttpContextAccessor context) 36 | { 37 | _logger = logger; 38 | _config = config; 39 | _client = client; 40 | _user = context.HttpContext.User.Identity.Name ?? "*"; 41 | var database = _client.GetDatabase(_config[Constants.databaseName]); 42 | _todolist = database.GetCollection(_config[Constants.collectionName]); 43 | } 44 | 45 | [OpenApiOperation(operationId: "GetTodoItems")] 46 | [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] 47 | [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(TodoItem[]), Description = "A to do list")] 48 | [FunctionName("GetTodoItems")] 49 | public async Task GetTodoItems( 50 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "todos")] 51 | HttpRequest req) 52 | { 53 | IActionResult returnValue = null; 54 | 55 | try 56 | { 57 | var result = _todolist.Find(item => item.Owner == _user).ToList(); 58 | 59 | if (result == null) 60 | { 61 | _logger.LogInformation($"There are no items in the collection"); 62 | returnValue = new NotFoundResult(); 63 | } 64 | else 65 | { 66 | returnValue = new OkObjectResult(result); 67 | } 68 | } 69 | catch (Exception ex) 70 | { 71 | _logger.LogError($"Exception thrown: {ex.Message}"); 72 | returnValue = new StatusCodeResult(StatusCodes.Status500InternalServerError); 73 | } 74 | 75 | return returnValue; 76 | } 77 | 78 | [OpenApiOperation(operationId: "GetTodoItem")] 79 | [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] 80 | [OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Description = "To do item id")] 81 | [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(TodoItem), Description = "A to do item")] 82 | [FunctionName("GetTodoItem")] 83 | public async Task GetTodoItem( 84 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", 85 | Route = "todos/{id}")]HttpRequestMessage req, string id) 86 | { 87 | 88 | IActionResult returnValue = null; 89 | 90 | try 91 | { 92 | var result =_todolist.Find(item => item.Id == id && item.Owner == _user).FirstOrDefault(); 93 | 94 | if (result == null) 95 | { 96 | _logger.LogWarning("That item doesn't exist!"); 97 | returnValue = new NotFoundResult(); 98 | } 99 | else 100 | { 101 | returnValue = new OkObjectResult(result); 102 | } 103 | } 104 | catch (Exception ex) 105 | { 106 | _logger.LogError($"Couldn't find item with id: {id}. Exception thrown: {ex.Message}"); 107 | returnValue = new StatusCodeResult(StatusCodes.Status500InternalServerError); 108 | } 109 | 110 | return returnValue; 111 | } 112 | 113 | [OpenApiOperation(operationId: "PostTodoItem")] 114 | [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] 115 | [OpenApiRequestBody(contentType: "application/json", bodyType: typeof(TodoItem), Required = true, Description = "To do object that needs to be added to the list")] 116 | [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(TodoItem), Description = "A to do item")] 117 | [FunctionName("PostTodoItem")] 118 | public async Task PostTodoItem( 119 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "todos")] HttpRequest req) 120 | { 121 | IActionResult returnValue = null; 122 | 123 | try 124 | { 125 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 126 | 127 | var input = JsonConvert.DeserializeObject(requestBody); 128 | 129 | var todo = new TodoItem 130 | { 131 | Id = Guid.NewGuid().ToString(), 132 | Description = input.Description, 133 | Owner = _user, 134 | Status = false 135 | }; 136 | 137 | _todolist.InsertOne(todo); 138 | 139 | _logger.LogInformation("Todo item inserted"); 140 | returnValue = new OkObjectResult(todo); 141 | } 142 | catch (Exception ex) 143 | { 144 | _logger.LogError($"Could not insert item. Exception thrown: {ex.Message}"); 145 | returnValue = new StatusCodeResult(StatusCodes.Status500InternalServerError); 146 | } 147 | 148 | return returnValue; 149 | } 150 | 151 | [OpenApiOperation(operationId: "PutTodoItem")] 152 | [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] 153 | [OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Description = "To do Id")] 154 | [OpenApiRequestBody(contentType: "application/json", bodyType: typeof(TodoItem), Required = true, Description = "To do object that needs to be updated to the list")] 155 | [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(TodoItem), Description = "A to do item")] 156 | [FunctionName("PutTodoItem")] 157 | public async Task PutTodoItem( 158 | [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "todos/{id}")] HttpRequest req, 159 | string id) 160 | { 161 | IActionResult returnValue = null; 162 | 163 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 164 | 165 | var updatedResult = JsonConvert.DeserializeObject(requestBody); 166 | 167 | updatedResult.Id = id; 168 | 169 | try 170 | { 171 | var replacedItem = _todolist.ReplaceOne(item => item.Id == id && item.Owner == _user, updatedResult); 172 | 173 | if (replacedItem == null) 174 | { 175 | returnValue = new NotFoundResult(); 176 | } 177 | else 178 | { 179 | returnValue = new OkObjectResult(updatedResult); 180 | } 181 | } 182 | catch (Exception ex) 183 | { 184 | _logger.LogError($"Could not update Album with id: {id}. Exception thrown: {ex.Message}"); 185 | returnValue = new StatusCodeResult(StatusCodes.Status500InternalServerError); 186 | } 187 | 188 | return returnValue; 189 | } 190 | 191 | 192 | [OpenApiOperation(operationId: "DeleteTodoItem")] 193 | [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] 194 | [OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(string), Description = "To do Id that needs to be removed from the list")] 195 | [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "text/plain", bodyType: typeof(string), Description = "OK Response")] 196 | [FunctionName("DeleteTodoItem")] 197 | public async Task DeleteTodoItem( 198 | [HttpTrigger(AuthorizationLevel.Anonymous, "delete", 199 | Route = "todos/{id}")]HttpRequestMessage req, string id) 200 | { 201 | 202 | IActionResult returnValue = null; 203 | 204 | try 205 | { 206 | var itemToDelete = _todolist.DeleteOne(item => item.Id == id && item.Owner == _user); 207 | 208 | if (itemToDelete == null) 209 | { 210 | _logger.LogInformation($"Todo item with id: {id} does not exist. Delete failed"); 211 | returnValue = new StatusCodeResult(StatusCodes.Status404NotFound); 212 | } 213 | 214 | returnValue = new StatusCodeResult(StatusCodes.Status200OK); 215 | } 216 | catch (Exception ex) 217 | { 218 | _logger.LogError($"Could not delete item. Exception thrown: {ex.Message}"); 219 | returnValue = new StatusCodeResult(StatusCodes.Status500InternalServerError); 220 | } 221 | 222 | return returnValue; 223 | } 224 | 225 | [OpenApiOperation(operationId: "HealthCheck")] 226 | [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.OK, Description = "Status code 200")] 227 | [FunctionName("HealthCheck")] 228 | public async Task HealthCheck( 229 | [HttpTrigger(AuthorizationLevel.Anonymous, "head", 230 | Route = "todos")]HttpRequestMessage req) 231 | { 232 | 233 | IActionResult returnValue = null; 234 | 235 | returnValue = new StatusCodeResult(StatusCodes.Status200OK); 236 | 237 | return returnValue; 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/api/dotnet/ToDoFunctionApp/TodoListFn.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | v3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | PreserveNewest 30 | Always 31 | 32 | 33 | PreserveNewest 34 | Never 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/api/dotnet/ToDoFunctionApp/authorization.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "http_methods": [ "GET", "POST", "PUT", "DELETE" ], 5 | "path_prefix": "/api/todos", 6 | "policies": { 7 | "unauthenticated_action": "RedirectToLoginPage" 8 | } 9 | }, 10 | { 11 | "http_methods": [ "HEAD" ], 12 | "path_prefix": "/api/todos", 13 | "policies": { 14 | "unauthenticated_action": "AllowAnonymous" 15 | } 16 | }, 17 | { 18 | "http_methods": [ "GET" ], 19 | "path_prefix": "/api/swagger.*", 20 | "policies": { 21 | "unauthenticated_action": "AllowAnonymous" 22 | } 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /src/api/dotnet/ToDoFunctionApp/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensions": { 4 | "cosmosDB": { 5 | "connectionMode": "Gateway", 6 | "protocol": "Https" 7 | } 8 | }, 9 | "logging": { 10 | "applicationInsights": { 11 | "samplingSettings": { 12 | "isEnabled": true 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/.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 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/README.md: -------------------------------------------------------------------------------- 1 | # Angular9SampleApp 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.0.1. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "todo-app": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/todo-app", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "aot": true, 22 | "assets": [ 23 | "src/favicon.svg", 24 | "src/assets" 25 | ], 26 | "styles": [ 27 | "src/styles.css" 28 | ], 29 | "scripts": [] 30 | }, 31 | "configurations": { 32 | "production": { 33 | "fileReplacements": [ 34 | { 35 | "replace": "src/environments/environment.ts", 36 | "with": "src/environments/environment.prod.ts" 37 | } 38 | ], 39 | "optimization": true, 40 | "outputHashing": "all", 41 | "sourceMap": false, 42 | "extractCss": true, 43 | "namedChunks": false, 44 | "extractLicenses": true, 45 | "vendorChunk": false, 46 | "buildOptimizer": true, 47 | "budgets": [ 48 | { 49 | "type": "initial", 50 | "maximumWarning": "2mb", 51 | "maximumError": "5mb" 52 | }, 53 | { 54 | "type": "anyComponentStyle", 55 | "maximumWarning": "6kb", 56 | "maximumError": "10kb" 57 | } 58 | ] 59 | } 60 | } 61 | }, 62 | "serve": { 63 | "builder": "@angular-devkit/build-angular:dev-server", 64 | "options": { 65 | "browserTarget": "todo-app:build" 66 | }, 67 | "configurations": { 68 | "production": { 69 | "browserTarget": "todo-app:build:production" 70 | } 71 | } 72 | }, 73 | "extract-i18n": { 74 | "builder": "@angular-devkit/build-angular:extract-i18n", 75 | "options": { 76 | "browserTarget": "todo-app:build" 77 | } 78 | }, 79 | "test": { 80 | "builder": "@angular-devkit/build-angular:karma", 81 | "options": { 82 | "main": "src/test.ts", 83 | "polyfills": "src/polyfills.ts", 84 | "tsConfig": "tsconfig.spec.json", 85 | "karmaConfig": "karma.conf.js", 86 | "assets": [ 87 | "src/favicon.svg", 88 | "src/assets" 89 | ], 90 | "styles": [ 91 | "src/styles.css" 92 | ], 93 | "scripts": [] 94 | } 95 | }, 96 | "lint": { 97 | "builder": "@angular-devkit/build-angular:tslint", 98 | "options": { 99 | "tsConfig": [ 100 | "tsconfig.app.json", 101 | "tsconfig.spec.json", 102 | "e2e/tsconfig.json" 103 | ], 104 | "exclude": [ 105 | "**/node_modules/**" 106 | ] 107 | } 108 | }, 109 | "e2e": { 110 | "builder": "@angular-devkit/build-angular:protractor", 111 | "options": { 112 | "protractorConfig": "e2e/protractor.conf.js", 113 | "devServerTarget": "todo-app:serve" 114 | }, 115 | "configurations": { 116 | "production": { 117 | "devServerTarget": "todo-app:serve:production" 118 | } 119 | } 120 | }, 121 | "azureLogout": { 122 | "builder": "@azure/ng-deploy:logout" 123 | }, 124 | "deploy": { 125 | "builder": "@azure/ng-deploy:deploy", 126 | "options": { 127 | "host": "Azure", 128 | "type": "static", 129 | "config": "azure.json" 130 | } 131 | } 132 | } 133 | } 134 | }, 135 | "defaultProject": "todo-app" 136 | } 137 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/browserslist: -------------------------------------------------------------------------------- 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 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | IE 9-11 # For IE 9-11 support, remove 'not'. 13 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | afterEach(async () => { 12 | // Assert that there are no errors emitted from the browser 13 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 14 | expect(logs).not.toContain(jasmine.objectContaining({ 15 | level: logging.Level.SEVERE, 16 | } as logging.Entry)); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/angular9-todo-app'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular9-todo-app", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^9.0.7", 15 | "@angular/cdk": "^9.1.3", 16 | "@angular/common": "^9.0.7", 17 | "@angular/compiler": "^9.0.7", 18 | "@angular/core": "^9.0.7", 19 | "@angular/forms": "^9.0.7", 20 | "@angular/material": "^9.1.3", 21 | "@angular/platform-browser": "^9.0.7", 22 | "@angular/platform-browser-dynamic": "^9.0.7", 23 | "@angular/router": "^9.0.7", 24 | "@azure/msal-angular": "^1.1.1", 25 | "@azure/ng-deploy": "^0.2.3", 26 | "core-js": "^3.6.4", 27 | "material-icons": "^0.3.1", 28 | "msal": "^1.4.0", 29 | "rxjs": "~6.5.4", 30 | "tslib": "^1.11.1", 31 | "zone.js": "^0.10.3" 32 | }, 33 | "devDependencies": { 34 | "@angular-devkit/build-angular": "^0.1000.8", 35 | "@angular/cli": "^9.0.7", 36 | "@angular/compiler-cli": "^9.0.7", 37 | "@angular/language-service": "^9.0.7", 38 | "@types/jasmine": "^3.5.10", 39 | "@types/jasminewd2": "~2.0.3", 40 | "@types/node": "^12.12.31", 41 | "codelyzer": "^5.1.2", 42 | "jasmine-core": "~3.5.0", 43 | "jasmine-spec-reporter": "~4.2.1", 44 | "karma": "^5.0.8", 45 | "karma-chrome-launcher": "~3.1.0", 46 | "karma-coverage-istanbul-reporter": "~2.1.0", 47 | "karma-jasmine": "~2.0.1", 48 | "karma-jasmine-html-reporter": "^1.5.3", 49 | "protractor": "~5.4.3", 50 | "ts-node": "~8.3.0", 51 | "tslint": "~5.18.0", 52 | "typescript": "~3.7.5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/app-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "clientId": "__clientAppId__", 4 | "authority": "__tenantDomainName__", 5 | "validateAuthority": true, 6 | "redirectUri": "__clientAppURL__", 7 | "postLogoutRedirectUri": "__clientAppURL__", 8 | "navigateToLoginRequestUrl": true 9 | }, 10 | "cache": { 11 | "cacheLocation": "localStorage" 12 | }, 13 | "scopes": { 14 | "loginRequest": ["openid", "profile"] 15 | }, 16 | "resources": { 17 | "todoListApi": { 18 | "resourceUri": "__apimURL__/todos", 19 | "resourceScope": "__backendAPIScope__" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { MsalGuard } from '@azure/msal-angular'; 4 | import { HomeComponent } from './home/home.component'; 5 | import { TodoViewComponent } from './todo-view/todo-view.component'; 6 | import { TodoEditComponent } from './todo-edit/todo-edit.component'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: 'todo-edit/:id', 11 | component: TodoEditComponent, 12 | canActivate: [ 13 | MsalGuard 14 | ] 15 | }, 16 | { 17 | path: 'todo-view', 18 | component: TodoViewComponent, 19 | canActivate: [ 20 | MsalGuard 21 | ] 22 | }, 23 | { 24 | path: '', 25 | component: HomeComponent 26 | } 27 | ]; 28 | 29 | @NgModule({ 30 | imports: [RouterModule.forRoot(routes, { useHash: false })], 31 | exports: [RouterModule] 32 | }) 33 | export class AppRoutingModule { } 34 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .toolbar-spacer { 2 | flex: 1 1 auto; 3 | } 4 | 5 | a.title { 6 | color: white; 7 | } 8 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ title }} 3 | 4 |
5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | import { MatToolbarModule } from '@angular/material/toolbar'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatListModule } from '@angular/material/list'; 7 | import { MsalService, MSAL_CONFIG, MSAL_CONFIG_ANGULAR, MsalAngularConfiguration, BroadcastService } from '@azure/msal-angular'; 8 | import { Configuration } from 'msal'; 9 | import { msalConfig, msalAngularConfig } from './app-config'; 10 | 11 | describe('AppComponent', () => { 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [ 15 | RouterTestingModule, 16 | MatToolbarModule, 17 | MatButtonModule, 18 | MatListModule, 19 | ], 20 | declarations: [ 21 | AppComponent, 22 | 23 | ], 24 | providers: [ 25 | MsalService, 26 | { 27 | provide: MSAL_CONFIG, 28 | useValue: msalConfig as Configuration 29 | }, 30 | { 31 | provide: MSAL_CONFIG_ANGULAR, 32 | useValue: msalAngularConfig as MsalAngularConfiguration 33 | }, 34 | BroadcastService 35 | ] 36 | }).compileComponents(); 37 | })); 38 | 39 | it('should create the app', () => { 40 | const fixture = TestBed.createComponent(AppComponent); 41 | const app = fixture.componentInstance; 42 | expect(app).toBeTruthy(); 43 | }); 44 | 45 | it(`should have as title 'Microsoft Identity Platform'`, () => { 46 | const fixture = TestBed.createComponent(AppComponent); 47 | const app = fixture.componentInstance; 48 | expect(app.title).toEqual('Microsoft Identity Platform'); 49 | }); 50 | 51 | it('should render title', () => { 52 | const fixture = TestBed.createComponent(AppComponent); 53 | fixture.detectChanges(); 54 | const compiled = fixture.nativeElement; 55 | expect(compiled.querySelector('.title').textContent).toContain('Microsoft Identity Platform'); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { BroadcastService, MsalService } from '@azure/msal-angular'; 3 | import { Logger, CryptoUtils } from 'msal'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.css'] 9 | }) 10 | export class AppComponent implements OnInit { 11 | title = 'Microsoft Identity Platform'; 12 | isIframe = false; 13 | loggedIn = false; 14 | 15 | constructor(private broadcastService: BroadcastService, private authService: MsalService) { } 16 | 17 | ngOnInit() { 18 | this.isIframe = window !== window.parent && !window.opener; 19 | 20 | this.checkoutAccount(); 21 | 22 | this.broadcastService.subscribe('msal:loginSuccess', (payload) => { 23 | console.log(payload); 24 | this.checkoutAccount(); 25 | }); 26 | 27 | this.broadcastService.subscribe('msal:loginFailure', (payload) => { 28 | console.log(payload); 29 | console.log('login failed'); 30 | }); 31 | 32 | this.authService.handleRedirectCallback((authError, response) => { 33 | if (authError) { 34 | console.error('Redirect Error: ', authError.errorMessage); 35 | return; 36 | } 37 | 38 | console.log('Redirect Success: ', response.accessToken); 39 | }); 40 | 41 | this.authService.setLogger(new Logger((logLevel, message, piiEnabled) => { 42 | console.log('MSAL Logging: ', message); 43 | }, { 44 | correlationId: CryptoUtils.createNewGuid(), 45 | piiLoggingEnabled: false 46 | })); 47 | } 48 | 49 | checkoutAccount() { 50 | this.loggedIn = !!this.authService.getAccount(); 51 | } 52 | 53 | login() { 54 | const isIE = window.navigator.userAgent.indexOf('MSIE ') > -1 || window.navigator.userAgent.indexOf('Trident/') > -1; 55 | 56 | if (isIE) { 57 | this.authService.loginRedirect(); 58 | } else { 59 | this.authService.loginPopup(); 60 | } 61 | } 62 | 63 | logout() { 64 | this.authService.logout(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { NgModule } from '@angular/core'; 5 | import { FormsModule } from '@angular/forms' 6 | import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; 7 | 8 | import { MatButtonModule } from '@angular/material/button'; 9 | import { MatToolbarModule } from '@angular/material/toolbar'; 10 | import { MatListModule } from '@angular/material/list'; 11 | import { MatCardModule } from '@angular/material/card'; 12 | import { MatCheckboxModule } from '@angular/material/checkbox'; 13 | import { MatIconModule } from '@angular/material/icon' 14 | import { MatTableModule } from '@angular/material/table'; 15 | import { MatInputModule } from '@angular/material/input'; 16 | import { MatFormFieldModule } from '@angular/material/form-field'; 17 | 18 | import { Configuration, CacheLocation } from 'msal'; 19 | import { 20 | MsalModule, 21 | MsalInterceptor, 22 | MSAL_CONFIG, 23 | MSAL_CONFIG_ANGULAR, 24 | MsalService, 25 | MsalAngularConfiguration 26 | } from '@azure/msal-angular'; 27 | 28 | import * as config from './app-config.json'; 29 | import { AppRoutingModule } from './app-routing.module'; 30 | import { HomeComponent } from './home/home.component'; 31 | import { TodoService } from './todo.service'; 32 | import { AppComponent } from './app.component'; 33 | import { TodoEditComponent } from './todo-edit/todo-edit.component'; 34 | import { TodoViewComponent } from './todo-view/todo-view.component'; 35 | 36 | // checks if the app is running on IE 37 | export const isIE = window.navigator.userAgent.indexOf('MSIE ') > -1 || window.navigator.userAgent.indexOf('Trident/') > -1; 38 | 39 | export const protectedResourceMap: [string, string[]][] = [ 40 | [config.resources.todoListApi.resourceUri, [config.resources.todoListApi.resourceScope]] 41 | ]; 42 | 43 | function MSALConfigFactory(): Configuration { 44 | return { 45 | auth: { 46 | clientId: config.auth.clientId, 47 | authority: config.auth.authority, 48 | validateAuthority: true, 49 | redirectUri: config.auth.redirectUri, 50 | postLogoutRedirectUri: config.auth.postLogoutRedirectUri, 51 | navigateToLoginRequestUrl: true, 52 | }, 53 | cache: { 54 | cacheLocation: config.cache.cacheLocation, 55 | storeAuthStateInCookie: isIE, // set to true for IE 11 56 | }, 57 | }; 58 | } 59 | 60 | function MSALAngularConfigFactory(): MsalAngularConfiguration { 61 | return { 62 | popUp: !isIE, 63 | consentScopes: [ 64 | config.resources.todoListApi.resourceScope, 65 | ...config.scopes.loginRequest 66 | ], 67 | unprotectedResources: [], 68 | protectedResourceMap, 69 | extraQueryParameters: {} 70 | }; 71 | } 72 | 73 | @NgModule({ 74 | declarations: [ 75 | AppComponent, 76 | HomeComponent, 77 | TodoEditComponent, 78 | TodoViewComponent, 79 | ], 80 | imports: [ 81 | BrowserModule, 82 | AppRoutingModule, 83 | BrowserAnimationsModule, 84 | HttpClientModule, 85 | MatToolbarModule, 86 | MatButtonModule, 87 | MatListModule, 88 | MatCardModule, 89 | AppRoutingModule, 90 | MsalModule, 91 | FormsModule, 92 | MatInputModule, 93 | MatTableModule, 94 | MatFormFieldModule, 95 | MatCheckboxModule, 96 | MatIconModule, 97 | ], 98 | providers: [ 99 | { 100 | provide: HTTP_INTERCEPTORS, 101 | useClass: MsalInterceptor, 102 | multi: true 103 | }, 104 | { 105 | provide: MSAL_CONFIG, 106 | useFactory: MSALConfigFactory 107 | }, 108 | { 109 | provide: MSAL_CONFIG_ANGULAR, 110 | useFactory: MSALAngularConfigFactory 111 | }, 112 | MsalService, 113 | TodoService, 114 | ], 115 | bootstrap: [AppComponent] 116 | }) 117 | export class AppModule { } 118 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/home/home.component.css: -------------------------------------------------------------------------------- 1 | .card-section { 2 | margin: 10%; 3 | padding: 5%; 4 | } -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sign-in with Microsoft Azure AD 4 | Getting an access token with Azure AD and calling a Web API 5 | This simple sample demonstrates how to use the Microsoft Authentication Library for JavaScript (msal.js) to get an access token and call an API secured by Azure AD. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home', 5 | templateUrl: './home.component.html', 6 | styleUrls: ['./home.component.css'] 7 | }) 8 | export class HomeComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/todo-edit/todo-edit.component.css: -------------------------------------------------------------------------------- 1 | .card-section { 2 | margin: 10%; 3 | padding: 5%; 4 | } 5 | 6 | .form-field { 7 | min-width: 100px; 8 | max-width: 800px; 9 | width: 100%; 10 | } 11 | 12 | .input-field { 13 | width: 100%; 14 | } -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/todo-edit/todo-edit.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Edit a task

4 |
5 |
6 |
7 |
8 | 9 | 10 | 11 | 12 |
13 |
-------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/todo-edit/todo-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { TodoService } from './../todo.service'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | import { Todo } from '../todo'; 5 | 6 | @Component({ 7 | selector: 'app-todo-edit', 8 | templateUrl: './todo-edit.component.html', 9 | styleUrls: ['./todo-edit.component.css'] 10 | }) 11 | export class TodoEditComponent implements OnInit { 12 | 13 | todo: Todo = { 14 | id: undefined, 15 | owner: undefined, 16 | description: undefined, 17 | status: undefined, 18 | }; 19 | 20 | constructor(private route: ActivatedRoute, private router: Router, private service: TodoService) { } 21 | 22 | ngOnInit(): void { 23 | this.route.paramMap.subscribe((params) => { 24 | let id = +params.get('id'); 25 | this.service.getTodo(id).subscribe((response: Todo) => { 26 | this.todo = response; 27 | }) 28 | }) 29 | } 30 | 31 | editTodo(todo): void { 32 | this.todo.description = todo.description; 33 | this.service.editTodo(this.todo).subscribe(() => { 34 | this.router.navigate(['/todo-view']); 35 | }) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/todo-view/todo-view.component.css: -------------------------------------------------------------------------------- 1 | table { 2 | min-width: 100px; 3 | width: 100%; 4 | } 5 | 6 | .form-field { 7 | min-width: 100px; 8 | width: 90%; 9 | } 10 | 11 | .input-field { 12 | width: 90%; 13 | } 14 | 15 | .card-section { 16 | margin-top: 5%; 17 | } 18 | 19 | #submit-button { 20 | margin-left: 1%; 21 | } 22 | 23 | .material-icons:hover { 24 | color: orange !important; 25 | cursor: pointer 26 | } -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/todo-view/todo-view.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | Enter a task 5 | 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
Status Description {{todo.description}} Edit edit Remove delete
39 |
40 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/todo-view/todo-view.component.ts: -------------------------------------------------------------------------------- 1 | import { TodoService } from './../todo.service'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { NgForm } from '@angular/forms'; 4 | import { BroadcastService, MsalService } from '@azure/msal-angular'; 5 | import { InteractionRequiredAuthError, AuthError } from 'msal'; 6 | import * as config from '../app-config.json'; 7 | import { Todo } from '../todo'; 8 | 9 | @Component({ 10 | selector: 'app-todo-view', 11 | templateUrl: './todo-view.component.html', 12 | styleUrls: ['./todo-view.component.css'] 13 | }) 14 | export class TodoViewComponent implements OnInit { 15 | 16 | todo: Todo; 17 | 18 | todos: Todo[]; 19 | 20 | displayedColumns = ['status', 'description', 'edit', 'remove']; 21 | 22 | constructor(private authService: MsalService, private service: TodoService, private broadcastService: BroadcastService) { } 23 | 24 | ngOnInit(): void { 25 | this.broadcastService.subscribe('msal:acquireTokenSuccess', (payload) => { 26 | console.log(payload); 27 | console.log('access token acquired: ' + new Date().toString()); 28 | 29 | }); 30 | 31 | this.broadcastService.subscribe('msal:acquireTokenFailure', (payload) => { 32 | console.log(payload); 33 | console.log('access token acquisition fails'); 34 | }); 35 | 36 | this.getTodos(); 37 | } 38 | 39 | getTodos(): void { 40 | this.service.getTodos().subscribe({ 41 | next: (response: Todo[]) => { 42 | this.todos = response; 43 | }, 44 | error: (err: AuthError) => { 45 | // If there is an interaction required error, 46 | // call one of the interactive methods and then make the request again. 47 | if (InteractionRequiredAuthError.isInteractionRequiredError(err.errorCode)) { 48 | this.authService.acquireTokenPopup({ 49 | scopes: this.authService.getScopesForEndpoint(config.resources.todoListApi.resourceUri) 50 | }) 51 | .then(() => { 52 | this.service.getTodos() 53 | .toPromise() 54 | .then((response: Todo[]) => { 55 | this.todos = response; 56 | }); 57 | }); 58 | } 59 | } 60 | }); 61 | } 62 | 63 | addTodo(add: NgForm): void { 64 | this.service.postTodo(add.value).subscribe(() => { 65 | this.getTodos(); 66 | }) 67 | add.resetForm(); 68 | } 69 | 70 | checkTodo(todo): void { 71 | this.service.editTodo(todo).subscribe(); 72 | } 73 | 74 | removeTodo(id): void { 75 | this.service.deleteTodo(id).subscribe(() => { 76 | this.getTodos(); 77 | }) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/todo.service.ts: -------------------------------------------------------------------------------- 1 | import { Todo } from './todo'; 2 | import { Injectable } from '@angular/core'; 3 | import { HttpClient } from '@angular/common/http'; 4 | import * as config from './app-config.json'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class TodoService { 10 | url = config.resources.todoListApi.resourceUri; 11 | 12 | constructor(private http: HttpClient) { } 13 | 14 | getTodos() { 15 | return this.http.get(this.url); 16 | } 17 | 18 | getTodo(id) { 19 | return this.http.get(this.url + '/' + id); 20 | } 21 | 22 | postTodo(todo) { 23 | return this.http.post(this.url, todo); 24 | } 25 | 26 | deleteTodo(id) { 27 | return this.http.delete(this.url + '/' + id); 28 | } 29 | 30 | editTodo(todo) { 31 | return this.http.put(this.url + '/' + todo.id, todo); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/app/todo.ts: -------------------------------------------------------------------------------- 1 | export interface Todo { 2 | id: string; 3 | owner: string; 4 | description: string; 5 | status: boolean; 6 | } -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/serverless-web-application/735b4d573ffa6865d53735a982fd453c344f4037/src/client/angular/ToDoSpa/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` 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 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Icon-identity-221 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MS Identity - Angular Todo App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic/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().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/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 Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'core-js'; // Needed for IE11 59 | import 'zone.js/dist/zone'; // Included with Angular CLI. 60 | 61 | 62 | /*************************************************************************************************** 63 | * APPLICATION IMPORTS 64 | */ 65 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; 3 | @import '~material-icons/iconfont/material-icons.css'; 4 | 5 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "importHelpers": true, 14 | "target": "es5", 15 | "skipLibCheck": true, 16 | "paths": { 17 | "@angular/*": ["./node_modules/@angular/*"] 18 | }, 19 | "typeRoots": [ 20 | "node_modules/@types" 21 | ], 22 | "lib": [ 23 | "es2018", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "fullTemplateTypeCheck": true, 29 | "strictInjectionParameters": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/client/angular/ToDoSpa/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [ 13 | true, 14 | "attribute", 15 | "app", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "app", 22 | "kebab-case" 23 | ], 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "interface-name": false, 29 | "max-classes-per-file": false, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-empty": false, 56 | "no-inferrable-types": [ 57 | true, 58 | "ignore-params" 59 | ], 60 | "no-non-null-assertion": true, 61 | "no-redundant-jsdoc": true, 62 | "no-switch-case-fall-through": true, 63 | "no-var-requires": false, 64 | "object-literal-key-quotes": [ 65 | true, 66 | "as-needed" 67 | ], 68 | "object-literal-sort-keys": false, 69 | "ordered-imports": false, 70 | "quotemark": [ 71 | true, 72 | "single" 73 | ], 74 | "trailing-comma": false, 75 | "no-conflicting-lifecycle": true, 76 | "no-host-metadata-property": true, 77 | "no-input-rename": true, 78 | "no-inputs-metadata-property": true, 79 | "no-output-native": true, 80 | "no-output-on-prefix": true, 81 | "no-output-rename": true, 82 | "no-outputs-metadata-property": true, 83 | "template-banana-in-box": true, 84 | "template-no-negated-async": true, 85 | "use-lifecycle-interface": true, 86 | "use-pipe-transform-interface": true 87 | }, 88 | "rulesDirectory": [ 89 | "codelyzer" 90 | ] 91 | } -------------------------------------------------------------------------------- /workflows/azure-infra-cicd.yml: -------------------------------------------------------------------------------- 1 | name: Create Azure Resource (IaC) 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | AZURE_REGION: 6 | description: 'Azure Region to deploy Azure resources' 7 | required: true 8 | default: 'azure-region' 9 | ENVIRONMENT_TYPE: 10 | description: 'Environment: dev, test, or prod' 11 | required: true 12 | default: 'dev' 13 | APP_NAME_PREFIX: 14 | description: 'Prefix to be used in naming Azure resources' 15 | required: true 16 | default: 'prefix' 17 | RESOURCE_GROUP_NAME: 18 | description: 'Resource Group to deploy Azure resources' 19 | required: true 20 | default: 'resource-group' 21 | MSI_NAME: 22 | description: 'User Managed Identity' 23 | required: true 24 | default: 'user-msi' 25 | MSI_RESOURCE_GROUP: 26 | description: 'Resource Group where User Managed Identity is located' 27 | required: true 28 | default: 'msi-resource-group' 29 | 30 | # CONFIGURATION 31 | # For help, go to https://github.com/Azure/Actions 32 | # 33 | # 1. Set up the following secrets in your repository: 34 | # AZURE_CREDENTIALS 35 | # 36 | # 2. Change below variables for your configuration: 37 | env: 38 | AZURE_REGION: ${{ github.event.inputs.AZURE_REGION }} 39 | ENVIRONMENT_TYPE: ${{ github.event.inputs.ENVIRONMENT_TYPE }} 40 | APP_NAME_PREFIX: ${{ github.event.inputs.APP_NAME_PREFIX }} 41 | RESOURCE_GROUP_NAME: ${{ github.event.inputs.RESOURCE_GROUP_NAME }} 42 | MSI_NAME: ${{ github.event.inputs.MSI_NAME }} 43 | MSI_RESOURCE_GROUP: ${{ github.event.inputs.MSI_RESOURCE_GROUP }} 44 | BICEP_FILE_PATH: 'deploy' 45 | BICEP_FILE_NAME: 'main' 46 | 47 | jobs: 48 | validate_deploy: 49 | runs-on: ubuntu-latest 50 | steps: 51 | # Authentication 52 | # Set up the following secrets in your repository: AZURE_CREDENTIALS 53 | # For details on usage of secrets, please refer https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets 54 | - name: Azure Login 55 | uses: azure/login@v1 56 | with: 57 | creds: ${{ secrets.AZURE_CREDENTIALS }} 58 | 59 | # Checkout 60 | - name: Checkout 61 | uses: actions/checkout@v1 62 | 63 | # Build ARM Template from Bicep and create a target Azure resource group 64 | - name: Azure CLI - Validate Bicep file ${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.bicep 65 | uses: Azure/cli@1.0.4 66 | with: 67 | # Azure CLI version to be used to execute the script. If not provided, latest version is used 68 | azcliversion: 2.27.2 69 | # Specify the script here 70 | inlineScript: | 71 | az group create -l ${{ env.AZURE_REGION }} -n ${{ env.RESOURCE_GROUP_NAME }} 72 | az deployment group validate -g ${{ env.APP_NAME_PREFIX }}-${{ env.ENVIRONMENT_TYPE }}-rg --template-file ./${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.bicep 73 | az bicep upgrade 74 | az bicep build --file ./${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.bicep 75 | 76 | # Deployment Bicep template 77 | - name: Deploy ${{ env.ENVIRONMENT_TYPE }} environment infrastructure to ${{ env.RESOURCE_GROUP_NAME }} 78 | id: infraDeployment 79 | uses: azure/arm-deploy@v1 80 | with: 81 | deploymentName: ${{ github.run_number }} 82 | resourceGroupName: ${{ env.RESOURCE_GROUP_NAME }} 83 | template: ./${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.json # Set this to the location of your template file 84 | parameters: appNameSuffix=${{ env.APP_NAME_PREFIX }} environmentType=${{ env.ENVIRONMENT_TYPE }} userAssignedIdentityName=${{ env.MSI_NAME }} userAssignedIdentityResourceGroup=${{ env.MSI_RESOURCE_GROUP }} 85 | 86 | # Azure logout 87 | - name: logout 88 | run: | 89 | az logout 90 | if: always() 91 | -------------------------------------------------------------------------------- /workflows/functions-api-cicd.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish .NET Functions 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | ENVIRONMENT_TYPE: 6 | description: 'Environment: dev, test, or prod' 7 | required: true 8 | default: 'dev' 9 | APP_NAME_PREFIX: 10 | description: 'Prefix to be used in naming Azure resources' 11 | required: true 12 | default: 'prefix' 13 | RESOURCE_GROUP_NAME: 14 | description: 'Resource Group to deploy Azure resources' 15 | required: true 16 | default: 'resource-group' 17 | API_NAME: 18 | description: 'API name' 19 | required: true 20 | default: '2do' 21 | API_DOCUMENT_URL: 22 | description: 'API definition URL' 23 | required: true 24 | default: 'https://.azurewebsites.net/api/swagger.json' 25 | APIM_NAME: 26 | description: 'APIM name' 27 | required: true 28 | default: 'apim-name' 29 | FUNCTION_NAME: 30 | description: 'Azure Functions name' 31 | required: true 32 | default: 'function-name' 33 | ORIGIN_URL: 34 | description: 'Client app URL' # This is CDN endpoint URL 35 | required: true 36 | default: 'https://.azureedge.net' 37 | 38 | # CONFIGURATION 39 | # For help, go to https://github.com/Azure/Actions 40 | # 41 | # 1. Set up the following secrets in your repository: 42 | # AZURE_CREDENTIALS 43 | # 44 | # 2. Change below variables for your configuration: 45 | env: 46 | ENVIRONMENT_TYPE: ${{ github.event.inputs.ENVIRONMENT_TYPE }} 47 | APP_NAME_PREFIX: ${{ github.event.inputs.APP_NAME_PREFIX }} 48 | RESOURCE_GROUP_NAME: ${{ github.event.inputs.RESOURCE_GROUP_NAME }} 49 | API_NAME: ${{ github.event.inputs.API_NAME }} 50 | API_DOCUMENT_URL: ${{ github.event.inputs.API_DOCUMENT_URL }} 51 | APIM_NAME: ${{ github.event.inputs.APIM_NAME }} 52 | FUNCTION_NAME: ${{ github.event.inputs.FUNCTION_NAME }} 53 | ORIGIN_URL: ${{ github.event.inputs.ORIGIN_URL }} 54 | APP_SOURCE_PATH: 'src' 55 | FUNCTIONAPP_PATH: 'api/dotnet/ToDoFunctionApp' 56 | DOTNET_VERSION: '3.1.410' 57 | BICEP_FILE_PATH: 'deploy' 58 | BICEP_FILE_NAME: 'api' 59 | 60 | jobs: 61 | function_cicd: 62 | runs-on: ubuntu-latest 63 | steps: 64 | # Authentication 65 | # Set up the following secrets in your repository: AZURE_CREDENTIALS 66 | # For details on usage of secrets, please refer https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets 67 | - name: Azure Login 68 | uses: azure/login@v1 69 | with: 70 | creds: ${{ secrets.AZURE_CREDENTIALS }} 71 | 72 | # Checkout 73 | - name: Checkout 74 | uses: actions/checkout@v1 75 | 76 | # Setup .NET Core environment 77 | - name: Setup DotNet ${{ env.DOTNET_VERSION }} Environment 78 | uses: actions/setup-dotnet@v1 79 | with: 80 | dotnet-version: ${{ env.DOTNET_VERSION }} 81 | 82 | # Build .NET application 83 | - name: 'Build .NET application' 84 | shell: bash 85 | run: | 86 | pushd ./${{ env.APP_SOURCE_PATH }}/${{ env.FUNCTIONAPP_PATH }} 87 | dotnet build --configuration Release --output ./outputs 88 | popd 89 | 90 | # Publish .NET application to Azure Function 91 | - name: Publish to Azure Functions to ${{ env.FUNCTION_NAME }} 92 | uses: Azure/functions-action@v1 93 | id: fa 94 | with: 95 | app-name: ${{ env.FUNCTION_NAME }} 96 | package: ./${{ env.APP_SOURCE_PATH }}/${{ env.FUNCTIONAPP_PATH }}/outputs 97 | 98 | # Validate and Build ARM Template from Bicep 99 | - name: Azure CLI - Validate Bicep file ${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.bicep 100 | uses: Azure/cli@1.0.4 101 | with: 102 | # Azure CLI version to be used to execute the script. If not provided, latest version is used 103 | azcliversion: 2.27.2 104 | # Specify the script here 105 | inlineScript: | 106 | az deployment group validate -g ${{ env.RESOURCE_GROUP_NAME }} --template-file ./${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.bicep 107 | az bicep upgrade 108 | az bicep build --file ./${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.bicep 109 | 110 | # Deployment Bicep template for APIM API 111 | - name: Import ${{ env.ENVIRONMENT_TYPE }} environment API to ${{ env.APIM_NAME }} 112 | id: apiDeployment 113 | uses: azure/arm-deploy@v1 114 | with: 115 | deploymentName: '${{ github.run_number }}-api' 116 | resourceGroupName: ${{ env.RESOURCE_GROUP_NAME }} 117 | template: ./${{ env.BICEP_FILE_PATH }}/${{ env.BICEP_FILE_NAME }}.json # Set this to the location of your template file 118 | parameters: apimName=${{ env.APIM_NAME }} openApiUrl=${{ env.API_DOCUMENT_URL }} originUrl=${{ env.ORIGIN_URL }} apimApiName=${{ env.API_NAME }} 119 | 120 | # Azure logout 121 | - name: logout 122 | run: | 123 | az logout 124 | if: always() 125 | -------------------------------------------------------------------------------- /workflows/spa-cicd.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Angular (SPA) 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | ENVIRONMENT_TYPE: 6 | description: 'Environment: dev, test, or prod' 7 | required: true 8 | default: 'dev' 9 | APP_NAME_PREFIX: 10 | description: 'Prefix to be used in naming Azure resources' 11 | required: true 12 | default: 'prefix' 13 | RESOURCE_GROUP_NAME: 14 | description: 'Resource Group to deploy Azure resources' 15 | required: true 16 | default: 'resource-group' 17 | CLIENT_URL: 18 | description: 'Client URL' 19 | required: true 20 | default: 'https://.azureedge.net' 21 | API_URL: 22 | description: 'API on APIM URL' 23 | required: true 24 | default: 'https://.azure-api.net/' 25 | AZURE_STORAGE_NAME: 26 | description: 'Azure storage account name' 27 | required: true 28 | default: 'storageaccountname' 29 | CDN_PROFILE_NAME: 30 | description: 'CDN profile name' 31 | required: true 32 | default: 'cdn-profile-name' 33 | CDN_ENDPOINT_NAME: 34 | description: 'CDN endpoint name' 35 | required: true 36 | default: 'cdn-endpoint-name' 37 | 38 | # CONFIGURATION 39 | # For help, go to https://github.com/Azure/Actions 40 | # 41 | # 1. Set up the following secrets in your repository: 42 | # AZURE_CREDENTIALS 43 | # 44 | # 2. Change below variables for your configuration: 45 | env: 46 | ENVIRONMENT_TYPE: ${{ github.event.inputs.ENVIRONMENT_TYPE }} 47 | APP_NAME_PREFIX: ${{ github.event.inputs.APP_NAME_PREFIX }} 48 | RESOURCE_GROUP_NAME: ${{ github.event.inputs.RESOURCE_GROUP_NAME }} 49 | CLIENT_URL: ${{ github.event.inputs.CLIENT_URL }} 50 | API_URL: ${{ github.event.inputs.API_URL }} 51 | AZURE_STORAGE_NAME: ${{ github.event.inputs.AZURE_STORAGE_NAME }} 52 | CDN_PROFILE_NAME: ${{ github.event.inputs.CDN_PROFILE_NAME }} 53 | CDN_ENDPOINT_NAME: ${{ github.event.inputs.CDN_ENDPOINT_NAME }} 54 | APP_SOURCE_PATH: 'src' 55 | ANGULAR_PATH: 'client/angular/ToDoSpa' 56 | NODE_VERSION: '14' 57 | BICEP_FILE_PATH: 'deploy' 58 | 59 | jobs: 60 | angular_cicd: 61 | runs-on: ubuntu-latest 62 | steps: 63 | # Authentication 64 | # Set up the following secrets in your repository: AZURE_CREDENTIALS 65 | # For details on usage of secrets, please refer https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets 66 | - name: Azure Login 67 | uses: azure/login@v1 68 | with: 69 | creds: ${{ secrets.AZURE_CREDENTIALS }} 70 | 71 | # Checkout 72 | - name: Checkout 73 | uses: actions/checkout@v1 74 | 75 | # Run app registration against AAD using PowerShell script 76 | - name: 'App Registration' 77 | id: appRegistration 78 | continue-on-error: true 79 | shell: pwsh 80 | run: | 81 | .\${{ env.BICEP_FILE_PATH }}\scripts\appRegistrationAndPermission.ps1 ` 82 | -clientName ${{ env.APP_NAME_PREFIX }}${{ env.ENVIRONMENT_TYPE }} ` 83 | -apiName fn-${{ env.APP_NAME_PREFIX }}-${{ env.ENVIRONMENT_TYPE }} ` 84 | -resourceGroup ${{ env.APP_NAME_PREFIX }}-${{ env.ENVIRONMENT_TYPE }}-rg ` 85 | -staticWebURL https://${{ env.APP_NAME_PREFIX }}-${{ env.ENVIRONMENT_TYPE }}.azureedge.net 86 | 87 | # Set app configurations of Angular 88 | - name: 'Replace tokens' 89 | uses: cschleiden/replace-tokens@v1.0 90 | with: 91 | tokenPrefix: '__' 92 | tokenSuffix: '__' 93 | files: ${{ github.workspace }}/${{ env.APP_SOURCE_PATH }}/${{ env.ANGULAR_PATH }}/src/app/app-config.json 94 | env: 95 | clientAppId: ${{ steps.appRegistration.outputs.clientId }} 96 | clientAppURL: ${{ env.CLIENT_URL }} 97 | apimURL: ${{ env.API_URL }} 98 | backendAPIScope: ${{ steps.appRegistration.outputs.scope }} 99 | tenantDomainName: ${{ steps.appRegistration.outputs.tenantDomainName }} 100 | 101 | # Setup Node.js environment 102 | - name: Setup Node.js ${{ env.NODE_VERSION }} environment 103 | uses: actions/setup-node@v2 104 | with: 105 | node-version: ${{ env.NODE_VERSION }} 106 | 107 | # Build Angular application 108 | - name: Build Angular application 109 | run: | 110 | pushd ./${{ env.APP_SOURCE_PATH }}/${{ env.ANGULAR_PATH }} 111 | npm install 112 | npm install -g @angular/cli 113 | ng build -c=production --output-path=./dist 114 | popd 115 | 116 | # Deploy Angular application to Storage Account 117 | - name: Publish static website to Azure storage account ${{ env.AZURE_STORAGE_NAME }} 118 | uses: Azure/cli@1.0.4 119 | with: 120 | # Azure CLI version to be used to execute the script. If not provided, latest version is used 121 | azcliversion: 2.21.0 122 | # Specify the script here 123 | inlineScript: az storage blob upload-batch -s ./${{ env.APP_SOURCE_PATH }}/${{ env.ANGULAR_PATH }}/dist -d '$web' --account-name ${{ env.AZURE_STORAGE_NAME }} 124 | 125 | # Purge CDN endpoint 126 | - name: Purge CDN endpoint on ${{ env.CDN_ENDPOINT_NAME }} 127 | uses: Azure/cli@1.0.4 128 | with: 129 | azcliversion: 2.21.0 130 | inlineScript: | 131 | az cdn endpoint purge --content-paths "/*" --profile-name ${{ env.CDN_PROFILE_NAME }} --name ${{ env.CDN_ENDPOINT_NAME }} --resource-group ${{ env.RESOURCE_GROUP_NAME }} 132 | 133 | # Azure logout 134 | - name: logout 135 | run: | 136 | az logout 137 | if: always() 138 | --------------------------------------------------------------------------------