From 33ff227862af8108b2139df250bb1527c5f1b9dd Mon Sep 17 00:00:00 2001 From: azure-sdk Date: Wed, 20 Dec 2023 17:23:34 +0000 Subject: [PATCH] Synchronize repo from Repoman --- .azdo/pipelines/azure-dev.yml | 43 +- .azdo/pipelines/terraform/java/azure-dev.yml | 33 +- .devcontainer/devcontainer.json | 2 +- .vscode/launch.json | 4 +- README.md | 8 +- infra/app/api.bicep | 2 +- infra/core/ai/cognitiveservices.bicep | 11 + infra/core/database/sqlserver/sqlserver.bicep | 2 +- infra/core/host/appservice.bicep | 27 +- infra/core/monitor/applicationinsights.bicep | 5 +- infra/core/monitor/monitoring.bicep | 4 +- src/api/.gitignore | 402 +++++++++++++++++- src/api/ListsController.cs | 184 -------- src/api/Program.cs | 10 +- src/api/Todo.Api.csproj | 26 +- src/api/TodoEndpointsExtensions.cs | 162 +++++++ src/web/package-lock.json | 46 +- src/web/package.json | 2 +- 18 files changed, 698 insertions(+), 275 deletions(-) delete mode 100644 src/api/ListsController.cs create mode 100644 src/api/TodoEndpointsExtensions.cs diff --git a/.azdo/pipelines/azure-dev.yml b/.azdo/pipelines/azure-dev.yml index 036f012..8ae0d8f 100644 --- a/.azdo/pipelines/azure-dev.yml +++ b/.azdo/pipelines/azure-dev.yml @@ -5,8 +5,9 @@ trigger: - master # Azure Pipelines workflow to deploy to Azure using azd -# To configure required secrets for connecting to Azure, simply run `azd pipeline config --provider azdo` +# To configure required secrets and service connection for connecting to Azure, simply run `azd pipeline config --provider azdo` # Task "Install azd" needs to install setup-azd extension for azdo - https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azd +# See below for alternative task to install azd if you can't install above task in your organization pool: vmImage: ubuntu-latest @@ -15,29 +16,41 @@ steps: - task: setup-azd@0 displayName: Install azd - - pwsh: | - $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; - - azd auth login ` - --client-id "$($info.clientId)" ` - --client-secret "$($info.clientSecret)" ` - --tenant-id "$($info.tenantId)" - displayName: azd login - env: - AZURE_CREDENTIALS: $(AZURE_CREDENTIALS) + # If you can't install above task in your organization, you can comment it and uncomment below task to install azd + # - task: Bash@3 + # displayName: Install azd + # inputs: + # targetType: 'inline' + # script: | + # curl -fsSL https://aka.ms/install-azd.sh | bash + # azd delegate auth to az to use service connection with AzureCLI@2 - pwsh: | - azd provision --no-prompt + azd config set auth.useAzCliAuth "true" + displayName: Configure AZD to Use AZ CLI Authentication. + + - task: AzureCLI@2 displayName: Provision Infrastructure + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + azd provision --no-prompt env: AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) AZURE_ENV_NAME: $(AZURE_ENV_NAME) AZURE_LOCATION: $(AZURE_LOCATION) - - pwsh: | - azd deploy --no-prompt + - task: AzureCLI@2 displayName: Deploy Application + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + azd deploy --no-prompt env: AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) AZURE_ENV_NAME: $(AZURE_ENV_NAME) - AZURE_LOCATION: $(AZURE_LOCATION) \ No newline at end of file + AZURE_LOCATION: $(AZURE_LOCATION) diff --git a/.azdo/pipelines/terraform/java/azure-dev.yml b/.azdo/pipelines/terraform/java/azure-dev.yml index aa09969..f6eee5b 100644 --- a/.azdo/pipelines/terraform/java/azure-dev.yml +++ b/.azdo/pipelines/terraform/java/azure-dev.yml @@ -5,8 +5,9 @@ trigger: - master # Azure Pipelines workflow to deploy to Azure using azd -# To configure required secrets for connecting to Azure, simply run `azd pipeline config --provider azdo` +# To configure required secrets and service connection for connecting to Azure, simply run `azd pipeline config --provider azdo` # Task "Install azd" needs to install setup-azd extension for azdo - https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azd +# See below for alternative task to install azd if you can't install above task in your organization pool: vmImage: ubuntu-latest @@ -15,16 +16,18 @@ steps: - task: setup-azd@0 displayName: Install azd + # If you can't install above task in your organization, you can comment it and uncomment below task to install azd + # - task: Bash@3 + # displayName: Install azd + # inputs: + # targetType: 'inline' + # script: | + # curl -fsSL https://aka.ms/install-azd.sh | bash + + # azd delegate auth to az to use service connection with AzureCLI@2 - pwsh: | - $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; - - azd auth login ` - --client-id "$($info.clientId)" ` - --client-secret "$($info.clientSecret)" ` - --tenant-id "$($info.tenantId)" - displayName: azd login - env: - AZURE_CREDENTIALS: $(AZURE_CREDENTIALS) + azd config set auth.useAzCliAuth "true" + displayName: Configure AZD to Use AZ CLI Authentication. - task: JavaToolInstaller@0 inputs: @@ -51,11 +54,15 @@ steps: RS_STORAGE_ACCOUNT: $(RS_STORAGE_ACCOUNT) RS_CONTAINER_NAME: $(RS_CONTAINER_NAME) - - pwsh: | - azd deploy --no-prompt + - task: AzureCLI@2 displayName: Deploy Application + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + azd deploy --no-prompt env: AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) AZURE_ENV_NAME: $(AZURE_ENV_NAME) AZURE_LOCATION: $(AZURE_LOCATION) - diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ace6578..d97ae56 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Azure Developer CLI", - "image": "mcr.microsoft.com/devcontainers/dotnet:6.0-bullseye", + "image": "mcr.microsoft.com/devcontainers/dotnet:8.0-bookworm", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": { }, diff --git a/.vscode/launch.json b/.vscode/launch.json index c60e529..c8da513 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "url": "http://localhost:3000", "sourceMapPathOverrides": { "webpack:///src/*": "${webRoot}/*" - }, + }, }, { @@ -21,7 +21,7 @@ "request": "launch", "preLaunchTask": "Build API", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/src/api/bin/Debug/net6.0/Todo.Api.dll", + "program": "${workspaceFolder}/src/api/bin/Debug/net8.0/Todo.Api.dll", "args": [], "cwd": "${workspaceFolder}/src/api", "stopAtEntry": false, diff --git a/README.md b/README.md index 85dec21..f124f69 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Let's jump in and get this up and running in Azure. When you are finished, you w The following prerequisites are required to use this application. Please ensure that you have them all installed locally. - [Azure Developer CLI](https://aka.ms/azd-install) -- [.NET SDK 6.0](https://dotnet.microsoft.com/download/dotnet/6.0) - for the API backend +- [.NET SDK 8.0](https://dotnet.microsoft.com/download/dotnet/8.0) - for the API backend - [Node.js with npm (18.17.1+)](https://nodejs.org/) - for the Web frontend ### Quickstart @@ -52,7 +52,7 @@ This quickstart will show you how to authenticate on Azure, initialize using a t # Log in to azd. Only required once per-install. azd auth login -# First-time project setup. Initialize a project in the current directory, using this template. +# First-time project setup. Initialize a project in the current directory, using this template. azd init --template Azure-Samples/todo-csharp-cosmos-sql # Provision and deploy to Azure @@ -85,13 +85,13 @@ At this point, you have a complete application deployed on Azure. But there is m > Note: Needs to manually install [setup-azd extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azd) for Azure DevOps (azdo). -- [`azd pipeline config`](https://learn.microsoft.com/azure/developer/azure-developer-cli/configure-devops-pipeline?tabs=GitHub) - to configure a CI/CD pipeline (using GitHub Actions or Azure DevOps) to deploy your application whenever code is pushed to the main branch. +- [`azd pipeline config`](https://learn.microsoft.com/azure/developer/azure-developer-cli/configure-devops-pipeline?tabs=GitHub) - to configure a CI/CD pipeline (using GitHub Actions or Azure DevOps) to deploy your application whenever code is pushed to the main branch. - [`azd monitor`](https://learn.microsoft.com/azure/developer/azure-developer-cli/monitor-your-app) - to monitor the application and quickly navigate to the various Application Insights dashboards (e.g. overview, live metrics, logs) - [Run and Debug Locally](https://learn.microsoft.com/azure/developer/azure-developer-cli/debug?pivots=ide-vs-code) - using Visual Studio Code and the Azure Developer CLI extension -- [`azd down`](https://learn.microsoft.com/azure/developer/azure-developer-cli/reference#azd-down) - to delete all the Azure resources created with this template +- [`azd down`](https://learn.microsoft.com/azure/developer/azure-developer-cli/reference#azd-down) - to delete all the Azure resources created with this template - [Enable optional features, like APIM](./OPTIONAL_FEATURES.md) - for enhanced backend API protection and observability diff --git a/infra/app/api.bicep b/infra/app/api.bicep index abbbf4e..3fcde44 100644 --- a/infra/app/api.bicep +++ b/infra/app/api.bicep @@ -24,7 +24,7 @@ module api '../core/host/appservice.bicep' = { appSettings: appSettings keyVaultName: keyVaultName runtimeName: 'dotnetcore' - runtimeVersion: '6.0' + runtimeVersion: '8.0' scmDoBuildDuringDeployment: false } } diff --git a/infra/core/ai/cognitiveservices.bicep b/infra/core/ai/cognitiveservices.bicep index 1eafef9..1bf5666 100644 --- a/infra/core/ai/cognitiveservices.bicep +++ b/infra/core/ai/cognitiveservices.bicep @@ -6,11 +6,21 @@ param tags object = {} param customSubDomainName string = name param deployments array = [] param kind string = 'OpenAI' + +@allowed([ 'Enabled', 'Disabled' ]) param publicNetworkAccess string = 'Enabled' param sku object = { name: 'S0' } +param allowedIpRules array = [] +param networkAcls object = empty(allowedIpRules) ? { + defaultAction: 'Allow' +} : { + ipRules: allowedIpRules + defaultAction: 'Deny' +} + resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { name: name location: location @@ -19,6 +29,7 @@ resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { properties: { customSubDomainName: customSubDomainName publicNetworkAccess: publicNetworkAccess + networkAcls: networkAcls } sku: sku } diff --git a/infra/core/database/sqlserver/sqlserver.bicep b/infra/core/database/sqlserver/sqlserver.bicep index 9d00ada..84f2cc2 100644 --- a/infra/core/database/sqlserver/sqlserver.bicep +++ b/infra/core/database/sqlserver/sqlserver.bicep @@ -84,7 +84,7 @@ wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8 tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C . cat < ./initDb.sql -drop user ${APPUSERNAME} +drop user if exists ${APPUSERNAME} go create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}' go diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep index 4626bab..bef4d2b 100644 --- a/infra/core/host/appservice.bicep +++ b/infra/core/host/appservice.bicep @@ -65,16 +65,6 @@ resource appService 'Microsoft.Web/sites@2022-03-01' = { identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } - resource configLogs 'config' = { - name: 'logs' - properties: { - applicationLogs: { fileSystem: { level: 'Verbose' } } - detailedErrorMessages: { enabled: true } - failedRequestsTracing: { enabled: true } - httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } - } - } - resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = { name: 'ftp' properties: { @@ -90,7 +80,9 @@ resource appService 'Microsoft.Web/sites@2022-03-01' = { } } -module config 'appservice-appsettings.bicep' = if (!empty(appSettings)) { +// Updates to the single Microsoft.sites/web/config resources that need to be performed sequentially +// sites/web/config 'appsettings' +module configAppSettings 'appservice-appsettings.bicep' = { name: '${name}-appSettings' params: { name: appService.name @@ -105,6 +97,19 @@ module config 'appservice-appsettings.bicep' = if (!empty(appSettings)) { } } +// sites/web/config 'logs' +resource configLogs 'Microsoft.Web/sites/config@2022-03-01' = { + name: 'logs' + parent: appService + properties: { + applicationLogs: { fileSystem: { level: 'Verbose' } } + detailedErrorMessages: { enabled: true } + failedRequestsTracing: { enabled: true } + httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } + } + dependsOn: [configAppSettings] +} + resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { name: keyVaultName } diff --git a/infra/core/monitor/applicationinsights.bicep b/infra/core/monitor/applicationinsights.bicep index 4ee0825..4b4d01e 100644 --- a/infra/core/monitor/applicationinsights.bicep +++ b/infra/core/monitor/applicationinsights.bicep @@ -1,9 +1,8 @@ metadata description = 'Creates an Application Insights instance based on an existing Log Analytics workspace.' param name string -param dashboardName string +param dashboardName string = '' param location string = resourceGroup().location param tags object = {} -param includeDashboard bool = true param logAnalyticsWorkspaceId string resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { @@ -17,7 +16,7 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { } } -module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (includeDashboard) { +module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(dashboardName)) { name: 'application-insights-dashboard' params: { name: dashboardName diff --git a/infra/core/monitor/monitoring.bicep b/infra/core/monitor/monitoring.bicep index 165ada8..6bb05b0 100644 --- a/infra/core/monitor/monitoring.bicep +++ b/infra/core/monitor/monitoring.bicep @@ -1,10 +1,9 @@ metadata description = 'Creates an Application Insights instance and a Log Analytics workspace.' param logAnalyticsName string param applicationInsightsName string -param applicationInsightsDashboardName string +param applicationInsightsDashboardName string = '' param location string = resourceGroup().location param tags object = {} -param includeDashboard bool = true module logAnalytics 'loganalytics.bicep' = { name: 'loganalytics' @@ -22,7 +21,6 @@ module applicationInsights 'applicationinsights.bicep' = { location: location tags: tags dashboardName: applicationInsightsDashboardName - includeDashboard: includeDashboard logAnalyticsWorkspaceId: logAnalytics.outputs.id } } diff --git a/src/api/.gitignore b/src/api/.gitignore index 040309e..8dd4607 100644 --- a/src/api/.gitignore +++ b/src/api/.gitignore @@ -1,4 +1,398 @@ -obj -bin -.vs -.idea \ No newline at end of file +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml \ No newline at end of file diff --git a/src/api/ListsController.cs b/src/api/ListsController.cs deleted file mode 100644 index 0064466..0000000 --- a/src/api/ListsController.cs +++ /dev/null @@ -1,184 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace SimpleTodo.Api; - -[ApiController] -[Route("/lists")] -public class ListsController : ControllerBase -{ - private readonly ListsRepository _repository; - - public ListsController(ListsRepository repository) - { - _repository = repository; - } - - [HttpGet] - [ProducesResponseType(200)] - public async Task>> GetLists([FromQuery] int? skip = null, [FromQuery] int? batchSize = null) - { - return Ok(await _repository.GetListsAsync(skip, batchSize)); - } - - [HttpPost] - [ProducesResponseType(201)] - public async Task CreateList([FromBody]CreateUpdateTodoList list) - { - var todoList = new TodoList(list.name) - { - Description = list.description - }; - - await _repository.AddListAsync(todoList); - - return CreatedAtAction(nameof(GetList), new { list_id = todoList.Id }, todoList); - } - - [HttpGet("{list_id}")] - [ProducesResponseType(200)] - [ProducesResponseType(404)] - public async Task>> GetList(string list_id) - { - var list = await _repository.GetListAsync(list_id); - - return list == null ? NotFound() : Ok(list); - } - - [HttpPut("{list_id}")] - [ProducesResponseType(200)] - public async Task> UpdateList(string list_id, [FromBody]CreateUpdateTodoList list) - { - var existingList = await _repository.GetListAsync(list_id); - if (existingList == null) - { - return NotFound(); - } - - existingList.Name = list.name; - existingList.Description = list.description; - existingList.UpdatedDate = DateTimeOffset.UtcNow; - - await _repository.UpdateList(existingList); - - return Ok(existingList); - } - - [HttpDelete("{list_id}")] - [ProducesResponseType(204)] - [ProducesResponseType(404)] - public async Task DeleteList(string list_id) - { - if (await _repository.GetListAsync(list_id) == null) - { - return NotFound(); - } - - await _repository.DeleteListAsync(list_id); - - return NoContent(); - } - - [HttpGet("{list_id}/items")] - [ProducesResponseType(200)] - [ProducesResponseType(404)] - public async Task>> GetListItems(string list_id, [FromQuery] int? skip = null, [FromQuery] int? batchSize = null) - { - if (await _repository.GetListAsync(list_id) == null) - { - return NotFound(); - } - return Ok(await _repository.GetListItemsAsync(list_id, skip, batchSize)); - } - - [HttpPost("{list_id}/items")] - [ProducesResponseType(201)] - [ProducesResponseType(404)] - public async Task> CreateListItem(string list_id, [FromBody] CreateUpdateTodoItem item) - { - if (await _repository.GetListAsync(list_id) == null) - { - return NotFound(); - } - - var newItem = new TodoItem(list_id, item.name) - { - Name = item.name, - Description = item.description, - State = item.state, - CreatedDate = DateTimeOffset.UtcNow - }; - - await _repository.AddListItemAsync(newItem); - - return CreatedAtAction(nameof(GetListItem), new { list_id = list_id, item_id = newItem.Id }, newItem); - } - - [HttpGet("{list_id}/items/{item_id}")] - [ProducesResponseType(200)] - [ProducesResponseType(404)] - public async Task> GetListItem(string list_id, string item_id) - { - if (await _repository.GetListAsync(list_id) == null) - { - return NotFound(); - } - - var item = await _repository.GetListItemAsync(list_id, item_id); - - return item == null ? NotFound() : Ok(item); - } - - [HttpPut("{list_id}/items/{item_id}")] - [ProducesResponseType(200)] - [ProducesResponseType(404)] - public async Task> UpdateListItem(string list_id, string item_id, [FromBody] CreateUpdateTodoItem item) - { - var existingItem = await _repository.GetListItemAsync(list_id, item_id); - if (existingItem == null) - { - return NotFound(); - } - - existingItem.Name = item.name; - existingItem.Description = item.description; - existingItem.CompletedDate = item.completedDate; - existingItem.DueDate = item.dueDate; - existingItem.State = item.state; - existingItem.UpdatedDate = DateTimeOffset.UtcNow; - - await _repository.UpdateListItem(existingItem); - - return Ok(existingItem); - } - - [HttpDelete("{list_id}/items/{item_id}")] - [ProducesResponseType(204)] - [ProducesResponseType(404)] - public async Task DeleteListItem(string list_id, string item_id) - { - if (await _repository.GetListItemAsync(list_id, item_id) == null) - { - return NotFound(); - } - - await _repository.DeleteListItemAsync(list_id, item_id); - - return NoContent(); - } - - [HttpGet("{list_id}/state/{state}")] - [ProducesResponseType(200)] - [ProducesResponseType(404)] - public async Task>> GetListItemsByState(string list_id, string state, [FromQuery] int? skip = null, [FromQuery] int? batchSize = null) - { - if (await _repository.GetListAsync(list_id) == null) - { - return NotFound(); - } - - return Ok(await _repository.GetListItemsByStateAsync(list_id, state, skip, batchSize)); - } - - public record CreateUpdateTodoList(string name, string? description = null); - public record CreateUpdateTodoItem(string name, string state, DateTimeOffset? dueDate, DateTimeOffset? completedDate, string? description = null); -} \ No newline at end of file diff --git a/src/api/Program.cs b/src/api/Program.cs index 6dd8dbd..d99d626 100644 --- a/src/api/Program.cs +++ b/src/api/Program.cs @@ -1,5 +1,4 @@ using Azure.Identity; -using Microsoft.ApplicationInsights.AspNetCore.Extensions; using Microsoft.Azure.Cosmos; using SimpleTodo.Api; @@ -14,9 +13,10 @@ PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase } })); -builder.Services.AddControllers(); +builder.Services.AddCors(); builder.Services.AddApplicationInsightsTelemetry(builder.Configuration); - +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); var app = builder.Build(); app.UseCors(policy => @@ -37,5 +37,7 @@ ServeUnknownFileTypes = true, }); -app.MapControllers(); +app.MapGroup("/lists") + .MapTodoApi() + .WithOpenApi(); app.Run(); \ No newline at end of file diff --git a/src/api/Todo.Api.csproj b/src/api/Todo.Api.csproj index 6e5d7ee..c71ee27 100644 --- a/src/api/Todo.Api.csproj +++ b/src/api/Todo.Api.csproj @@ -1,15 +1,19 @@  - - net6.0 - enable - enable - + + net8.0 + enable + enable + false + - - - - - - + + + + + + + + + diff --git a/src/api/TodoEndpointsExtensions.cs b/src/api/TodoEndpointsExtensions.cs new file mode 100644 index 0000000..203816c --- /dev/null +++ b/src/api/TodoEndpointsExtensions.cs @@ -0,0 +1,162 @@ +using Microsoft.AspNetCore.Http.HttpResults; + +namespace SimpleTodo.Api +{ + public static class TodoEndpointsExtensions + { + public static RouteGroupBuilder MapTodoApi(this RouteGroupBuilder group) + { + group.MapGet("/", GetLists); + group.MapPost("/", CreateList); + group.MapGet("/{listId}", GetList); + group.MapPut("/{listId}", UpdateList); + group.MapDelete("/{listId}", DeleteList); + group.MapGet("/{listId}/items", GetListItems); + group.MapPost("/{listId}/items", CreateListItem); + group.MapGet("/{listId}/items/{itemId}", GetListItem); + group.MapPut("/{listId}/items/{itemId}", UpdateListItem); + group.MapDelete("/{listId}/items/{itemId}", DeleteListItem); + group.MapGet("/{listId}/state/{state}", GetListItemsByState); + return group; + } + + public static async Task>> GetLists(ListsRepository repository, int? skip = null, int? batchSize = null) + { + return TypedResults.Ok(await repository.GetListsAsync(skip, batchSize)); + } + + public static async Task CreateList(ListsRepository repository, CreateUpdateTodoList list) + { + var todoList = new TodoList(list.name) + { + Description = list.description + }; + + await repository.AddListAsync(todoList); + + return TypedResults.Created($"/lists/{todoList.Id}", todoList); + } + + public static async Task GetList(ListsRepository repository, string listId) + { + var list = await repository.GetListAsync(listId); + + return list == null ? TypedResults.NotFound() : TypedResults.Ok(list); + } + + public static async Task UpdateList(ListsRepository repository, string listId, CreateUpdateTodoList list) + { + var existingList = await repository.GetListAsync(listId); + if (existingList == null) + { + return TypedResults.NotFound(); + } + + existingList.Name = list.name; + existingList.Description = list.description; + existingList.UpdatedDate = DateTimeOffset.UtcNow; + + await repository.UpdateList(existingList); + + return TypedResults.Ok(existingList); + } + + public static async Task DeleteList(ListsRepository repository, string listId) + { + if (await repository.GetListAsync(listId) == null) + { + return TypedResults.NotFound(); + } + + await repository.DeleteListAsync(listId); + + return TypedResults.NoContent(); + } + + public static async Task GetListItems(ListsRepository repository, string listId, int? skip = null, int? batchSize = null) + { + if (await repository.GetListAsync(listId) == null) + { + return TypedResults.NotFound(); + } + return TypedResults.Ok(await repository.GetListItemsAsync(listId, skip, batchSize)); + } + + public static async Task CreateListItem(ListsRepository repository, string listId, CreateUpdateTodoItem item) + { + if (await repository.GetListAsync(listId) == null) + { + return TypedResults.NotFound(); + } + + var newItem = new TodoItem(listId, item.name) + { + Name = item.name, + Description = item.description, + State = item.state, + CreatedDate = DateTimeOffset.UtcNow + }; + + await repository.AddListItemAsync(newItem); + + return TypedResults.Created($"/lists/{listId}/items{newItem.Id}", newItem); + } + + public static async Task GetListItem(ListsRepository repository, string listId, string itemId) + { + if (await repository.GetListAsync(listId) == null) + { + return TypedResults.NotFound(); + } + + var item = await repository.GetListItemAsync(listId, itemId); + + return item == null ? TypedResults.NotFound() : TypedResults.Ok(item); + } + + public static async Task UpdateListItem(ListsRepository repository, string listId, string itemId, CreateUpdateTodoItem item) + { + var existingItem = await repository.GetListItemAsync(listId, itemId); + if (existingItem == null) + { + return TypedResults.NotFound(); + } + + existingItem.Name = item.name; + existingItem.Description = item.description; + existingItem.CompletedDate = item.completedDate; + existingItem.DueDate = item.dueDate; + existingItem.State = item.state; + existingItem.UpdatedDate = DateTimeOffset.UtcNow; + + await repository.UpdateListItem(existingItem); + + return TypedResults.Ok(existingItem); + } + + public static async Task DeleteListItem(ListsRepository repository, string listId, string itemId) + { + if (await repository.GetListItemAsync(listId, itemId) == null) + { + return TypedResults.NotFound(); + } + + await repository.DeleteListItemAsync(listId, itemId); + + return TypedResults.NoContent(); + } + + public static async Task GetListItemsByState(ListsRepository repository, string listId, string state, int? skip = null, int? batchSize = null) + { + if (await repository.GetListAsync(listId) == null) + { + return TypedResults.NotFound(); + } + + return TypedResults.Ok(await repository.GetListItemsByStateAsync(listId, state, skip, batchSize)); + } + } + + public record CreateUpdateTodoList(string name, string? description = null); + public record CreateUpdateTodoItem(string name, string state, DateTimeOffset? dueDate, DateTimeOffset? completedDate, string? description = null); +} \ No newline at end of file diff --git a/src/web/package-lock.json b/src/web/package-lock.json index bc5f5c8..a775845 100644 --- a/src/web/package-lock.json +++ b/src/web/package-lock.json @@ -11,7 +11,7 @@ "@fluentui/react": "^8.73.0", "@microsoft/applicationinsights-react-js": "^3.3.4", "@microsoft/applicationinsights-web": "^2.8.4", - "axios": "^0.27.2", + "axios": "^1.6.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-router-dom": "^6.3.0", @@ -39,9 +39,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", - "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", + "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", "dev": true }, "node_modules/@ampproject/remapping": { @@ -5550,12 +5550,13 @@ } }, "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/axobject-query": { @@ -14801,6 +14802,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -18070,9 +18076,9 @@ }, "dependencies": { "@adobe/css-tools": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", - "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", + "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", "dev": true }, "@ampproject/remapping": { @@ -22150,12 +22156,13 @@ "dev": true }, "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "axobject-query": { @@ -29028,6 +29035,11 @@ } } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", diff --git a/src/web/package.json b/src/web/package.json index d394358..d8c4118 100644 --- a/src/web/package.json +++ b/src/web/package.json @@ -6,7 +6,7 @@ "@fluentui/react": "^8.73.0", "@microsoft/applicationinsights-react-js": "^3.3.4", "@microsoft/applicationinsights-web": "^2.8.4", - "axios": "^0.27.2", + "axios": "^1.6.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-router-dom": "^6.3.0",