fix conflict
This commit is contained in:
@@ -1,83 +0,0 @@
|
|||||||
|
|
||||||
#-------------------------------------------------------------------------------------------------------------
|
|
||||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
|
|
||||||
#-------------------------------------------------------------------------------------------------------------
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:7.0
|
|
||||||
|
|
||||||
|
|
||||||
# This Dockerfile adds a non-root user with sudo access. Use the "remoteUser"
|
|
||||||
# property in devcontainer.json to use it. On Linux, the container user's GID/UIDs
|
|
||||||
# will be updated to match your local UID/GID (when using the dockerFile property).
|
|
||||||
# See https://aka.ms/vscode-remote/containers/non-root-user for details.
|
|
||||||
ARG USERNAME=vscode
|
|
||||||
ARG USER_UID=1000
|
|
||||||
ARG USER_GID=$USER_UID
|
|
||||||
|
|
||||||
# [Optional] Version of Node.js to install.
|
|
||||||
ARG INSTALL_NODE="false"
|
|
||||||
ARG NODE_VERSION="lts/*"
|
|
||||||
ENV NVM_DIR=/usr/local/share/nvm
|
|
||||||
|
|
||||||
# [Optional] Install the Azure CLI
|
|
||||||
ARG INSTALL_AZURE_CLI="false"
|
|
||||||
|
|
||||||
# Configure apt and install packages
|
|
||||||
RUN curl -fsSL https://aka.ms/install-azd.sh | bash \
|
|
||||||
&& apt-get update \
|
|
||||||
&& export DEBIAN_FRONTEND=noninteractive \
|
|
||||||
&& apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \
|
|
||||||
#
|
|
||||||
# Verify git, process tools, lsb-release (common in install instructions for CLIs) installed
|
|
||||||
&& apt-get -y install git openssh-client less iproute2 procps apt-transport-https gnupg2 curl lsb-release \
|
|
||||||
#
|
|
||||||
# Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user.
|
|
||||||
&& groupadd --gid $USER_GID $USERNAME \
|
|
||||||
&& useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \
|
|
||||||
# [Optional] Add sudo support for the non-root user
|
|
||||||
&& apt-get install -y sudo \
|
|
||||||
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\
|
|
||||||
&& chmod 0440 /etc/sudoers.d/$USERNAME \
|
|
||||||
#
|
|
||||||
# [Optional] Install Node.js for ASP.NET Core Web Applicationss
|
|
||||||
&& if [ "$INSTALL_NODE" = "true" ]; then \
|
|
||||||
#
|
|
||||||
# Install nvm and Node
|
|
||||||
mkdir -p ${NVM_DIR} \
|
|
||||||
&& curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash 2>&1 \
|
|
||||||
&& chown -R ${USER_UID}:${USER_GID} ${NVM_DIR} \
|
|
||||||
&& /bin/bash -c "source $NVM_DIR/nvm.sh \
|
|
||||||
&& nvm alias default ${NODE_VERSION}" 2>&1 \
|
|
||||||
&& echo '[ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh" && [ -s "$NVM_DIR/bash_completion" ] && \\. "$NVM_DIR/bash_completion"' \
|
|
||||||
| tee -a /home/${USERNAME}/.bashrc /home/${USERNAME}/.zshrc >> /root/.zshrc \
|
|
||||||
&& echo "if [ \"\$(stat -c '%U' ${NVM_DIR})\" != \"${USERNAME}\" ]; then sudo chown -R ${USER_UID}:root ${NVM_DIR}; fi" \
|
|
||||||
| tee -a /root/.bashrc /root/.zshrc /home/${USERNAME}/.bashrc >> /home/${USERNAME}/.zshrc \
|
|
||||||
&& chown ${USER_UID}:${USER_GID} /home/${USERNAME}/.bashrc /home/${USERNAME}/.zshrc \
|
|
||||||
&& chown -R ${USER_UID}:root ${NVM_DIR} \
|
|
||||||
#
|
|
||||||
# Install yarn
|
|
||||||
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
|
|
||||||
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get -y install --no-install-recommends yarn; \
|
|
||||||
fi \
|
|
||||||
#
|
|
||||||
# [Optional] Install the Azure CLI
|
|
||||||
&& if [ "$INSTALL_AZURE_CLI" = "true" ]; then \
|
|
||||||
echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list \
|
|
||||||
&& curl -sL https://packages.microsoft.com/keys/microsoft.asc | apt-key add - 2>/dev/null \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y azure-cli; \
|
|
||||||
fi \
|
|
||||||
#
|
|
||||||
# Install EF Core dotnet tool
|
|
||||||
&& dotnet tool install dotnet-ef --tool-path /home/$USERNAME/.dotnet/tools \
|
|
||||||
&& chown -R $USERNAME /home/$USERNAME/.dotnet \
|
|
||||||
#
|
|
||||||
# Clean up
|
|
||||||
&& apt-get autoremove -y \
|
|
||||||
&& apt-get clean -y \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Set PATH for dotnet tools
|
|
||||||
ENV PATH "$PATH:/home/$USERNAME/.dotnet/tools"
|
|
||||||
@@ -1,74 +1,43 @@
|
|||||||
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.112.0/containers/dotnetcore-3.1
|
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||||
{
|
{
|
||||||
"name": "eShopOnWeb",
|
"name": "eShopOnWeb",
|
||||||
"build": {
|
"image": "mcr.microsoft.com/devcontainers/dotnet:0-7.0",
|
||||||
"dockerfile": "Dockerfile",
|
|
||||||
"args": {
|
"customizations": {
|
||||||
"USERNAME": "vscode",
|
// Configure properties specific to VS Code.
|
||||||
"INSTALL_NODE": "false",
|
"vscode": {
|
||||||
"NODE_VERSION": "lts/*",
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
"INSTALL_AZURE_CLI": "false"
|
"extensions": [
|
||||||
}
|
"ms-dotnettools.csharp",
|
||||||
},
|
"formulahendry.dotnet-test-explorer",
|
||||||
"features": {
|
"ms-vscode.vscode-node-azure-pack",
|
||||||
"ghcr.io/devcontainers/features/azure-cli:1": {
|
"ms-kubernetes-tools.vscode-kubernetes-tools",
|
||||||
"version": "2.38"
|
"redhat.vscode-yaml"
|
||||||
},
|
]
|
||||||
"ghcr.io/devcontainers/features/docker-from-docker:1": {
|
}
|
||||||
"version": "20.10"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
|
||||||
"version": "2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Comment out to connect as root user. See https://aka.ms/vscode-remote/containers/non-root.
|
|
||||||
// make sure this is the same as USERNAME above
|
|
||||||
"remoteUser": "vscode",
|
|
||||||
"runArgs": [
|
|
||||||
"-v",
|
|
||||||
"/var/run/docker.sock:/var/run/docker.sock"
|
|
||||||
],
|
|
||||||
// Set *default* container specific settings.json values on container create.
|
|
||||||
"settings": {
|
|
||||||
"terminal.integrated.profiles.linux": {
|
|
||||||
"bash": {
|
|
||||||
"path": "bash",
|
|
||||||
"icon": "terminal-bash"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Add the IDs of extensions you want installed when the container is created.
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
"extensions": [
|
"forwardPorts": [5000, 5001],
|
||||||
"ms-dotnettools.csharp",
|
|
||||||
"formulahendry.dotnet-test-explorer",
|
|
||||||
"ms-vscode.vscode-node-azure-pack",
|
|
||||||
"ms-kubernetes-tools.vscode-kubernetes-tools",
|
|
||||||
"redhat.vscode-yaml"
|
|
||||||
],
|
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
"postCreateCommand": "dotnet dev-certs https --trust"
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
// [Optional] To reuse of your local HTTPS dev cert, first export it locally using this command:
|
||||||
"forwardPorts": [5000, 5001],
|
// * Windows PowerShell:
|
||||||
|
// dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
|
||||||
// [Optional] To reuse of your local HTTPS dev cert, first export it locally using this command:
|
// * macOS/Linux terminal:
|
||||||
// * Windows PowerShell:
|
// dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
|
||||||
// dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
|
//
|
||||||
// * macOS/Linux terminal:
|
// Next, after running the command above, uncomment lines in the 'mounts' and 'remoteEnv' lines below,
|
||||||
// dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
|
// and open / rebuild the container so the settings take effect.
|
||||||
//
|
//
|
||||||
// Next, after running the command above, uncomment lines in the 'mounts' and 'remoteEnv' lines below,
|
// "mounts": [
|
||||||
// and open / rebuild the container so the settings take effect.
|
// // "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind"
|
||||||
//
|
// ],
|
||||||
"mounts": [
|
// "remoteEnv": {
|
||||||
// "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind"
|
// // "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere",
|
||||||
],
|
// // "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx",
|
||||||
"remoteEnv": {
|
// },
|
||||||
// "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere",
|
|
||||||
// "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
// "postCreateCommand": "dotnet restore"
|
|
||||||
}
|
}
|
||||||
|
|||||||
36
.devcontainer/devcontainerreadme.md
Normal file
36
.devcontainer/devcontainerreadme.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Dev container
|
||||||
|
|
||||||
|
This project includes a [dev container](https://containers.dev/), which lets you use a container as a full-featured dev environment.
|
||||||
|
|
||||||
|
You can use the dev container configuration in this folder to build and run the app without needing to install any of its tools locally! You can use it in [GitHub Codespaces](https://github.com/features/codespaces) or the [VS Code Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
|
||||||
|
|
||||||
|
## GitHub Codespaces
|
||||||
|
Follow these steps to open this sample in a Codespace:
|
||||||
|
1. Click the **Code** drop-down menu at the top of https://github.com/dotnet-architecture/eShopOnWeb.
|
||||||
|
1. Click on the **Codespaces** tab.
|
||||||
|
1. Click **Create codespace on main** .
|
||||||
|
|
||||||
|
For more info, check out the [GitHub documentation](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/creating-a-codespace#creating-a-codespace).
|
||||||
|
|
||||||
|
## VS Code Dev Containers
|
||||||
|
|
||||||
|
If you already have VS Code and Docker installed, you can click [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/dotnet-architecture/eShopOnWeb) to get started. This will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
|
||||||
|
|
||||||
|
You can also follow these steps to open this sample in a container using the VS Code Dev Containers extension:
|
||||||
|
|
||||||
|
1. If this is your first time using a development container, please ensure your system meets the pre-reqs (i.e. have Docker installed) in the [getting started steps](https://aka.ms/vscode-remote/containers/getting-started).
|
||||||
|
|
||||||
|
2. Open a locally cloned copy of the code:
|
||||||
|
|
||||||
|
- Clone this repository to your local filesystem.
|
||||||
|
- Press <kbd>F1</kbd> and select the **Dev Containers: Open Folder in Container...** command.
|
||||||
|
- Select the cloned copy of this folder, wait for the container to start, and try things out!
|
||||||
|
|
||||||
|
You can learn more in the [Dev Containers documentation](https://code.visualstudio.com/docs/devcontainers/containers).
|
||||||
|
|
||||||
|
## Tips and tricks
|
||||||
|
|
||||||
|
* Since the dev container is Linux-based, you won't be able to use LocalDB. Add ` "UseOnlyInMemoryDatabase": true,` to the [appsettings.json](../src/Web/appsettings.json) file (there's additional context on this [in the app's readme](../README.md#configuring-the-sample-to-use-sql-server)).
|
||||||
|
* If you get a `502 bad gateway` error, you may need to set your port to the https protocol. You can do this by opening the Ports view in VS Code (**Ports: Focus on Ports View**), right-clicking on the port you're using, select **Change Port Protocol**, and set **https**.
|
||||||
|
* If you are working with the same repository folder in a container and Windows, you'll want consistent line endings (otherwise you may see hundreds of changes in the SCM view). The `.gitattributes` file in the root of this repo disables line ending conversion and should prevent this. See [tips and tricks](https://code.visualstudio.com/docs/devcontainers/tips-and-tricks#_resolving-git-line-ending-issues-in-containers-resulting-in-many-modified-files) for more info.
|
||||||
|
* If you'd like to review the contents of the image used in this dev container, you can check it out in the [devcontainers/images](https://github.com/devcontainers/images/tree/main/src/dotnet) repo.
|
||||||
@@ -141,4 +141,10 @@ csharp_preserve_single_line_blocks = true
|
|||||||
###############################
|
###############################
|
||||||
[*.vb]
|
[*.vb]
|
||||||
# Modifier preferences
|
# Modifier preferences
|
||||||
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion
|
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion###############################
|
||||||
|
######################################
|
||||||
|
# Configure Nullable Reference Types #
|
||||||
|
######################################
|
||||||
|
[{**/*Dto.cs,**/*Request.cs,**/*Response.cs}]
|
||||||
|
# CS8618: Non-nullable field is uninitialized. Consider declaring as nullable.
|
||||||
|
dotnet_diagnostic.CS8618.severity = none
|
||||||
|
|||||||
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||||
|
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||||
@@ -1,56 +1,56 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="Ardalis.ApiEndpoints" Version="4.0.1" />
|
<PackageVersion Include="Ardalis.ApiEndpoints" Version="4.0.1" />
|
||||||
<PackageVersion Include="Ardalis.GuardClauses" Version="4.0.1" />
|
<PackageVersion Include="Ardalis.GuardClauses" Version="4.0.1" />
|
||||||
<PackageVersion Include="Ardalis.Specification.EntityFrameworkCore" Version="6.1.0" />
|
<PackageVersion Include="Ardalis.Specification.EntityFrameworkCore" Version="6.1.0" />
|
||||||
<PackageVersion Include="Ardalis.Result" Version="4.1.0" />
|
<PackageVersion Include="Ardalis.Result" Version="7.0.0" />
|
||||||
<PackageVersion Include="Ardalis.Specification" Version="6.1.0" />
|
<PackageVersion Include="Ardalis.Specification" Version="6.1.0" />
|
||||||
<PackageVersion Include="Ardalis.ListStartupServices" Version="1.1.4" />
|
<PackageVersion Include="Ardalis.ListStartupServices" Version="1.1.4" />
|
||||||
<PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
|
|
||||||
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.1" />
|
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.1" />
|
||||||
<PackageVersion Include="Azure.Identity" Version="1.9.0-beta.2" />
|
<PackageVersion Include="Azure.Identity" Version="1.9.0-beta.2" />
|
||||||
|
<PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||||
<PackageVersion Include="BlazorInputFile" Version="0.2.0" />
|
<PackageVersion Include="BlazorInputFile" Version="0.2.0" />
|
||||||
<PackageVersion Include="Blazored.LocalStorage" Version="4.3.0" />
|
<PackageVersion Include="Blazored.LocalStorage" Version="4.3.0" />
|
||||||
<PackageVersion Include="BuildBundlerMinifier" Version="3.2.449" PrivateAssets="All" />
|
<PackageVersion Include="BuildBundlerMinifier" Version="3.2.449" PrivateAssets="All" />
|
||||||
<PackageVersion Include="FluentValidation" Version="11.4.0" />
|
<PackageVersion Include="FluentValidation" Version="11.5.1" />
|
||||||
<PackageVersion Include="MediatR" Version="11.1.0" />
|
<PackageVersion Include="MediatR" Version="12.0.1" />
|
||||||
<PackageVersion Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="7.0.4" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="7.0.2" />
|
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.4" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.2" />
|
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="7.0.4" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="7.0.2" />
|
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.4" PrivateAssets="all" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.2" PrivateAssets="all" />
|
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="7.0.4" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="7.0.2" />
|
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.4" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.2" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.4" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.2" />
|
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="7.0.4" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="7.0.2" />
|
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.4" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.2" />
|
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="7.0.2" />
|
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="7.0.4" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Configuration" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Logging.Configuration" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.2" />
|
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.5" />
|
||||||
<PackageVersion Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" />
|
<PackageVersion Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.2" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.5" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.2" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.4" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.2">
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageVersion>
|
</PackageVersion>
|
||||||
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
|
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
|
||||||
<PackageVersion Include="MinimalApi.Endpoint" Version="1.3.0" />
|
<PackageVersion Include="MinimalApi.Endpoint" Version="1.3.0" />
|
||||||
<PackageVersion Include="System.Net.Http.Json" Version="7.0.0" />
|
<PackageVersion Include="System.Net.Http.Json" Version="7.0.1" />
|
||||||
<PackageVersion Include="System.Security.Claims" Version="4.3.0" />
|
<PackageVersion Include="System.Security.Claims" Version="4.3.0" />
|
||||||
<PackageVersion Include="System.Text.Json" Version="7.0.1" />
|
<PackageVersion Include="System.Text.Json" Version="7.0.2" />
|
||||||
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="6.25.1" />
|
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="6.27.0" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.5.0" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.5.0" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
|
||||||
<!-- Test -->
|
<!-- Test -->
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.2" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||||
<PackageVersion Include="xunit" Version="2.4.2" />
|
<PackageVersion Include="xunit" Version="2.4.2" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5">
|
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
92
Everything.sln
Normal file
92
Everything.sln
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApplicationCore", "src\ApplicationCore\ApplicationCore.csproj", "{1A5759FF-9990-4CF5-AD78-528452C5EFCC}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorAdmin", "src\BlazorAdmin\BlazorAdmin.csproj", "{7D7D0B73-4153-4E9B-BBD1-C9D7C8AEE970}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorShared", "src\BlazorShared\BlazorShared.csproj", "{6FD75683-D186-4BE3-ABD0-2324650B46B5}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{35457566-83CE-44FC-A650-265CC9C544DC}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicApi", "src\PublicApi\PublicApi.csproj", "{7F226129-E8B0-4274-87A7-347AA4F7D374}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "src\Web\Web.csproj", "{7559FA9E-7CFC-4615-8D09-3CDEFC765455}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BAA5312D-B54C-42D6-A3B9-504DD12F8250}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionalTests", "tests\FunctionalTests\FunctionalTests.csproj", "{020545FF-D985-4274-9FDB-FD8B9B32D2ED}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests", "tests\IntegrationTests\IntegrationTests.csproj", "{D6829485-DD9C-42CE-BEDE-4EB0E81021AC}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicApiIntegrationTests", "tests\PublicApiIntegrationTests\PublicApiIntegrationTests.csproj", "{698594AE-78D3-429F-B5CC-3A6F6BCE397A}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "tests\UnitTests\UnitTests.csproj", "{EAD6CF0B-2979-462C-BBB9-AF723B1EB570}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{1A5759FF-9990-4CF5-AD78-528452C5EFCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1A5759FF-9990-4CF5-AD78-528452C5EFCC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1A5759FF-9990-4CF5-AD78-528452C5EFCC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1A5759FF-9990-4CF5-AD78-528452C5EFCC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7D7D0B73-4153-4E9B-BBD1-C9D7C8AEE970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7D7D0B73-4153-4E9B-BBD1-C9D7C8AEE970}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7D7D0B73-4153-4E9B-BBD1-C9D7C8AEE970}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7D7D0B73-4153-4E9B-BBD1-C9D7C8AEE970}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6FD75683-D186-4BE3-ABD0-2324650B46B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6FD75683-D186-4BE3-ABD0-2324650B46B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6FD75683-D186-4BE3-ABD0-2324650B46B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6FD75683-D186-4BE3-ABD0-2324650B46B5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{35457566-83CE-44FC-A650-265CC9C544DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{35457566-83CE-44FC-A650-265CC9C544DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{35457566-83CE-44FC-A650-265CC9C544DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{35457566-83CE-44FC-A650-265CC9C544DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7F226129-E8B0-4274-87A7-347AA4F7D374}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7F226129-E8B0-4274-87A7-347AA4F7D374}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7F226129-E8B0-4274-87A7-347AA4F7D374}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7F226129-E8B0-4274-87A7-347AA4F7D374}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7559FA9E-7CFC-4615-8D09-3CDEFC765455}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7559FA9E-7CFC-4615-8D09-3CDEFC765455}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7559FA9E-7CFC-4615-8D09-3CDEFC765455}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7559FA9E-7CFC-4615-8D09-3CDEFC765455}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{020545FF-D985-4274-9FDB-FD8B9B32D2ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{020545FF-D985-4274-9FDB-FD8B9B32D2ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{020545FF-D985-4274-9FDB-FD8B9B32D2ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{020545FF-D985-4274-9FDB-FD8B9B32D2ED}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D6829485-DD9C-42CE-BEDE-4EB0E81021AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D6829485-DD9C-42CE-BEDE-4EB0E81021AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D6829485-DD9C-42CE-BEDE-4EB0E81021AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D6829485-DD9C-42CE-BEDE-4EB0E81021AC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{698594AE-78D3-429F-B5CC-3A6F6BCE397A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{698594AE-78D3-429F-B5CC-3A6F6BCE397A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{698594AE-78D3-429F-B5CC-3A6F6BCE397A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{698594AE-78D3-429F-B5CC-3A6F6BCE397A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{EAD6CF0B-2979-462C-BBB9-AF723B1EB570}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{EAD6CF0B-2979-462C-BBB9-AF723B1EB570}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EAD6CF0B-2979-462C-BBB9-AF723B1EB570}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EAD6CF0B-2979-462C-BBB9-AF723B1EB570}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{1A5759FF-9990-4CF5-AD78-528452C5EFCC} = {8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7}
|
||||||
|
{7D7D0B73-4153-4E9B-BBD1-C9D7C8AEE970} = {8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7}
|
||||||
|
{6FD75683-D186-4BE3-ABD0-2324650B46B5} = {8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7}
|
||||||
|
{35457566-83CE-44FC-A650-265CC9C544DC} = {8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7}
|
||||||
|
{7F226129-E8B0-4274-87A7-347AA4F7D374} = {8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7}
|
||||||
|
{7559FA9E-7CFC-4615-8D09-3CDEFC765455} = {8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7}
|
||||||
|
{020545FF-D985-4274-9FDB-FD8B9B32D2ED} = {BAA5312D-B54C-42D6-A3B9-504DD12F8250}
|
||||||
|
{D6829485-DD9C-42CE-BEDE-4EB0E81021AC} = {BAA5312D-B54C-42D6-A3B9-504DD12F8250}
|
||||||
|
{698594AE-78D3-429F-B5CC-3A6F6BCE397A} = {BAA5312D-B54C-42D6-A3B9-504DD12F8250}
|
||||||
|
{EAD6CF0B-2979-462C-BBB9-AF723B1EB570} = {BAA5312D-B54C-42D6-A3B9-504DD12F8250}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
13
README.md
13
README.md
@@ -12,7 +12,7 @@ A list of Frequently Asked Questions about this repository can be found [here](h
|
|||||||
|
|
||||||
## eBook
|
## eBook
|
||||||
|
|
||||||
This reference application is meant to support the free .PDF download ebook: [Architecting Modern Web Applications with ASP.NET Core and Azure](https://aka.ms/webappebook), updated to **ASP.NET Core 6.0**. [Also available in ePub/mobi formats](https://dotnet.microsoft.com/learn/web/aspnet-architecture).
|
This reference application is meant to support the free .PDF download ebook: [Architecting Modern Web Applications with ASP.NET Core and Azure](https://aka.ms/webappebook), updated to **ASP.NET Core 7.0**. [Also available in ePub/mobi formats](https://dotnet.microsoft.com/learn/web/aspnet-architecture).
|
||||||
|
|
||||||
You can also read the book in online pages at the .NET docs here:
|
You can also read the book in online pages at the .NET docs here:
|
||||||
https://docs.microsoft.com/dotnet/architecture/modern-web-apps-azure/
|
https://docs.microsoft.com/dotnet/architecture/modern-web-apps-azure/
|
||||||
@@ -100,13 +100,12 @@ You can also run the samples in Docker (see below).
|
|||||||
|
|
||||||
### Configuring the sample to use SQL Server
|
### Configuring the sample to use SQL Server
|
||||||
|
|
||||||
1. By default, the project uses a real database. If you want an in memory database, you can add in `appsettings.json`
|
1. By default, the project uses a real database. If you want an in memory database, you can add in the `appsettings.json` file in the Web folder
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"UseOnlyInMemoryDatabase": true
|
"UseOnlyInMemoryDatabase": true
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Ensure your connection strings in `appsettings.json` point to a local SQL Server instance.
|
1. Ensure your connection strings in `appsettings.json` point to a local SQL Server instance.
|
||||||
@@ -140,6 +139,14 @@ You can also run the samples in Docker (see below).
|
|||||||
dotnet ef migrations add InitialIdentityModel --context appidentitydbcontext -p ../Infrastructure/Infrastructure.csproj -s Web.csproj -o Identity/Migrations
|
dotnet ef migrations add InitialIdentityModel --context appidentitydbcontext -p ../Infrastructure/Infrastructure.csproj -s Web.csproj -o Identity/Migrations
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Running the sample in the dev container
|
||||||
|
|
||||||
|
This project includes a `.devcontainer` folder with a [dev container configuration](https://containers.dev/), which lets you use a container as a full-featured dev environment.
|
||||||
|
|
||||||
|
You can use the dev container to build and run the app without needing to install any of its tools locally! You can work in GitHub Codespaces or the VS Code Dev Containers extension.
|
||||||
|
|
||||||
|
Learn more about using the dev container in its [readme](/.devcontainer/devcontainerreadme.md).
|
||||||
|
|
||||||
## Running the sample using Docker
|
## Running the sample using Docker
|
||||||
|
|
||||||
You can run the Web sample by running these commands from the root folder (where the .sln file is located):
|
You can run the Web sample by running these commands from the root folder (where the .sln file is located):
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ public static class Dependencies
|
|||||||
{
|
{
|
||||||
public static void ConfigureServices(IConfiguration configuration, IServiceCollection services)
|
public static void ConfigureServices(IConfiguration configuration, IServiceCollection services)
|
||||||
{
|
{
|
||||||
var useOnlyInMemoryDatabase = false;
|
bool useOnlyInMemoryDatabase = false;
|
||||||
if (configuration["UseOnlyInMemoryDatabase"] != null)
|
if (configuration["UseOnlyInMemoryDatabase"] != null)
|
||||||
{
|
{
|
||||||
useOnlyInMemoryDatabase = bool.Parse(configuration["UseOnlyInMemoryDatabase"]);
|
useOnlyInMemoryDatabase = bool.Parse(configuration["UseOnlyInMemoryDatabase"]!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useOnlyInMemoryDatabase)
|
if (useOnlyInMemoryDatabase)
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ public class AppIdentityDbContextSeed
|
|||||||
var adminUser = new ApplicationUser { UserName = adminUserName, Email = adminUserName };
|
var adminUser = new ApplicationUser { UserName = adminUserName, Email = adminUserName };
|
||||||
await userManager.CreateAsync(adminUser, AuthorizationConstants.DEFAULT_PASSWORD);
|
await userManager.CreateAsync(adminUser, AuthorizationConstants.DEFAULT_PASSWORD);
|
||||||
adminUser = await userManager.FindByNameAsync(adminUserName);
|
adminUser = await userManager.FindByNameAsync(adminUserName);
|
||||||
await userManager.AddToRoleAsync(adminUser, BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS);
|
if (adminUser != null)
|
||||||
|
{
|
||||||
|
await userManager.AddToRoleAsync(adminUser, BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public class IdentityTokenClaimService : ITokenClaimsService
|
|||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY);
|
var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY);
|
||||||
var user = await _userManager.FindByNameAsync(userName);
|
var user = await _userManager.FindByNameAsync(userName);
|
||||||
|
if (user == null) throw new UserNotFoundException(userName);
|
||||||
var roles = await _userManager.GetRolesAsync(user);
|
var roles = await _userManager.GetRolesAsync(user);
|
||||||
var claims = new List<Claim> { new Claim(ClaimTypes.Name, userName) };
|
var claims = new List<Claim> { new Claim(ClaimTypes.Name, userName) };
|
||||||
|
|
||||||
|
|||||||
10
src/Infrastructure/Identity/UserNotFoundException.cs
Normal file
10
src/Infrastructure/Identity/UserNotFoundException.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.Infrastructure.Identity;
|
||||||
|
|
||||||
|
public class UserNotFoundException : Exception
|
||||||
|
{
|
||||||
|
public UserNotFoundException(string userName) : base($"No user found with username: {userName}")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
using System;
|
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints;
|
|
||||||
|
|
||||||
public class ClaimValue
|
public class ClaimValue
|
||||||
{
|
{
|
||||||
@@ -17,6 +12,6 @@ public class ClaimValue
|
|||||||
Value = value;
|
Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Type { get; set; }
|
public string Type { get; set; } = string.Empty;
|
||||||
public string Value { get; set; }
|
public string Value { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints;
|
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints;
|
||||||
|
|
||||||
@@ -9,7 +6,7 @@ public class UserInfo
|
|||||||
{
|
{
|
||||||
public static readonly UserInfo Anonymous = new UserInfo();
|
public static readonly UserInfo Anonymous = new UserInfo();
|
||||||
public bool IsAuthenticated { get; set; }
|
public bool IsAuthenticated { get; set; }
|
||||||
public string NameClaimType { get; set; }
|
public string NameClaimType { get; set; } = string.Empty;
|
||||||
public string RoleClaimType { get; set; }
|
public string RoleClaimType { get; set; } = string.Empty;
|
||||||
public IEnumerable<ClaimValue> Claims { get; set; }
|
public IEnumerable<ClaimValue> Claims { get; set; } = new List<ClaimValue>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ public class AuthenticateEndpoint : EndpointBaseAsync
|
|||||||
OperationId = "auth.authenticate",
|
OperationId = "auth.authenticate",
|
||||||
Tags = new[] { "AuthEndpoints" })
|
Tags = new[] { "AuthEndpoints" })
|
||||||
]
|
]
|
||||||
public override async Task<ActionResult<AuthenticateResponse>> HandleAsync(AuthenticateRequest request, CancellationToken cancellationToken = default)
|
public override async Task<ActionResult<AuthenticateResponse>> HandleAsync(AuthenticateRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var response = new AuthenticateResponse(request.CorrelationId());
|
var response = new AuthenticateResponse(request.CorrelationId());
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
public class ListPagedCatalogItemRequest : BaseRequest
|
public class ListPagedCatalogItemRequest : BaseRequest
|
||||||
{
|
{
|
||||||
public int? PageSize { get; init; }
|
public int PageSize { get; init; }
|
||||||
public int? PageIndex { get; init; }
|
public int PageIndex { get; init; }
|
||||||
public int? CatalogBrandId { get; init; }
|
public int? CatalogBrandId { get; init; }
|
||||||
public int? CatalogTypeId { get; init; }
|
public int? CatalogTypeId { get; init; }
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ public class CatalogItemListPagedEndpoint : IEndpoint<IResult, ListPagedCatalogI
|
|||||||
int totalItems = await itemRepository.CountAsync(filterSpec);
|
int totalItems = await itemRepository.CountAsync(filterSpec);
|
||||||
|
|
||||||
var pagedSpec = new CatalogFilterPaginatedSpecification(
|
var pagedSpec = new CatalogFilterPaginatedSpecification(
|
||||||
skip: request.PageIndex.Value * request.PageSize.Value,
|
skip: request.PageIndex * request.PageSize,
|
||||||
take: request.PageSize.Value,
|
take: request.PageSize,
|
||||||
brandId: request.CatalogBrandId,
|
brandId: request.CatalogBrandId,
|
||||||
typeId: request.CatalogTypeId);
|
typeId: request.CatalogTypeId);
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ public class CatalogItemListPagedEndpoint : IEndpoint<IResult, ListPagedCatalogI
|
|||||||
|
|
||||||
if (request.PageSize > 0)
|
if (request.PageSize > 0)
|
||||||
{
|
{
|
||||||
response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize.Value).ToString());
|
response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize).ToString());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ public class UpdateCatalogItemEndpoint : IEndpoint<IResult, UpdateCatalogItemReq
|
|||||||
var response = new UpdateCatalogItemResponse(request.CorrelationId());
|
var response = new UpdateCatalogItemResponse(request.CorrelationId());
|
||||||
|
|
||||||
var existingItem = await itemRepository.GetByIdAsync(request.Id);
|
var existingItem = await itemRepository.GetByIdAsync(request.Id);
|
||||||
|
if (existingItem == null)
|
||||||
|
{
|
||||||
|
return Results.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
CatalogItem.CatalogItemDetails details = new(request.Name, request.Description, request.Price);
|
CatalogItem.CatalogItemDetails details = new(request.Name, request.Description, request.Price);
|
||||||
existingItem.UpdateDetails(details);
|
existingItem.UpdateDetails(details);
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ public class ExceptionMiddleware
|
|||||||
Message = duplicationException.Message
|
Message = duplicationException.Message
|
||||||
}.ToString());
|
}.ToString());
|
||||||
}
|
}
|
||||||
|
else
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
|
||||||
await context.Response.WriteAsync(new ErrorDetails()
|
|
||||||
{
|
{
|
||||||
StatusCode = context.Response.StatusCode,
|
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
Message = exception.Message
|
await context.Response.WriteAsync(new ErrorDetails()
|
||||||
}.ToString());
|
{
|
||||||
|
StatusCode = context.Response.StatusCode,
|
||||||
|
Message = exception.Message
|
||||||
|
}.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using BlazorShared;
|
using BlazorShared;
|
||||||
using BlazorShared.Models;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
@@ -41,7 +40,8 @@ builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
|
|||||||
builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
|
builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
|
||||||
builder.Services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>));
|
builder.Services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>));
|
||||||
builder.Services.Configure<CatalogSettings>(builder.Configuration);
|
builder.Services.Configure<CatalogSettings>(builder.Configuration);
|
||||||
builder.Services.AddSingleton<IUriComposer>(new UriComposer(builder.Configuration.Get<CatalogSettings>()));
|
var catalogSettings = builder.Configuration.Get<CatalogSettings>() ?? new CatalogSettings();
|
||||||
|
builder.Services.AddSingleton<IUriComposer>(new UriComposer(catalogSettings));
|
||||||
builder.Services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
|
builder.Services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
|
||||||
builder.Services.AddScoped<ITokenClaimsService, IdentityTokenClaimService>();
|
builder.Services.AddScoped<ITokenClaimsService, IdentityTokenClaimService>();
|
||||||
|
|
||||||
@@ -73,12 +73,12 @@ const string CORS_POLICY = "CorsPolicy";
|
|||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy(name: CORS_POLICY,
|
options.AddPolicy(name: CORS_POLICY,
|
||||||
corsPolicyBuilder =>
|
corsPolicyBuilder =>
|
||||||
{
|
{
|
||||||
corsPolicyBuilder.WithOrigins(baseUrlConfig.WebBase.Replace("host.docker.internal", "localhost").TrimEnd('/'));
|
corsPolicyBuilder.WithOrigins(baseUrlConfig!.WebBase.Replace("host.docker.internal", "localhost").TrimEnd('/'));
|
||||||
corsPolicyBuilder.AllowAnyMethod();
|
corsPolicyBuilder.AllowAnyMethod();
|
||||||
corsPolicyBuilder.AllowAnyHeader();
|
corsPolicyBuilder.AllowAnyHeader();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
@@ -172,12 +172,9 @@ app.UseSwaggerUI(c =>
|
|||||||
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
|
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.UseEndpoints(endpoints =>
|
app.MapControllers();
|
||||||
{
|
|
||||||
endpoints.MapControllers();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.MapEndpoints();
|
app.MapEndpoints();
|
||||||
|
|
||||||
app.Logger.LogInformation("LAUNCHING PublicApi");
|
app.Logger.LogInformation("LAUNCHING PublicApi");
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class LoginModel : PageModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BindProperty]
|
[BindProperty]
|
||||||
public InputModel? Input { get; set; }
|
public required InputModel Input { get; set; }
|
||||||
|
|
||||||
public IList<AuthenticationScheme>? ExternalLogins { get; set; }
|
public IList<AuthenticationScheme>? ExternalLogins { get; set; }
|
||||||
|
|
||||||
@@ -74,7 +74,8 @@ public class LoginModel : PageModel
|
|||||||
// This doesn't count login failures towards account lockout
|
// This doesn't count login failures towards account lockout
|
||||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||||
//var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
|
//var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
|
||||||
var result = await _signInManager.PasswordSignInAsync(Input?.Email, Input?.Password, false, true);
|
var result = await _signInManager.PasswordSignInAsync(Input!.Email!, Input!.Password!,
|
||||||
|
false, true);
|
||||||
|
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class RegisterModel : PageModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BindProperty]
|
[BindProperty]
|
||||||
public InputModel? Input { get; set; }
|
public required InputModel Input { get; set; }
|
||||||
|
|
||||||
public string? ReturnUrl { get; set; }
|
public string? ReturnUrl { get; set; }
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ public class RegisterModel : PageModel
|
|||||||
if (ModelState.IsValid)
|
if (ModelState.IsValid)
|
||||||
{
|
{
|
||||||
var user = new ApplicationUser { UserName = Input?.Email, Email = Input?.Email };
|
var user = new ApplicationUser { UserName = Input?.Email, Email = Input?.Email };
|
||||||
var result = await _userManager.CreateAsync(user, Input?.Password);
|
var result = await _userManager.CreateAsync(user, Input?.Password!);
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("User created a new account with password.");
|
_logger.LogInformation("User created a new account with password.");
|
||||||
@@ -82,7 +82,7 @@ public class RegisterModel : PageModel
|
|||||||
protocol: Request.Scheme);
|
protocol: Request.Scheme);
|
||||||
|
|
||||||
Guard.Against.Null(callbackUrl, nameof(callbackUrl));
|
Guard.Against.Null(callbackUrl, nameof(callbackUrl));
|
||||||
await _emailSender.SendEmailAsync(Input?.Email, "Confirm your email",
|
await _emailSender.SendEmailAsync(Input!.Email!, "Confirm your email",
|
||||||
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||||
|
|
||||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ using Microsoft.eShopWeb.Infrastructure.Data;
|
|||||||
using Microsoft.eShopWeb.Infrastructure.Data.Queries;
|
using Microsoft.eShopWeb.Infrastructure.Data.Queries;
|
||||||
using Microsoft.eShopWeb.Infrastructure.Logging;
|
using Microsoft.eShopWeb.Infrastructure.Logging;
|
||||||
using Microsoft.eShopWeb.Infrastructure.Services;
|
using Microsoft.eShopWeb.Infrastructure.Services;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.Configuration;
|
namespace Microsoft.eShopWeb.Web.Configuration;
|
||||||
|
|
||||||
@@ -20,7 +18,10 @@ public static class ConfigureCoreServices
|
|||||||
services.AddScoped<IBasketService, BasketService>();
|
services.AddScoped<IBasketService, BasketService>();
|
||||||
services.AddScoped<IOrderService, OrderService>();
|
services.AddScoped<IOrderService, OrderService>();
|
||||||
services.AddScoped<IBasketQueryService, BasketQueryService>();
|
services.AddScoped<IBasketQueryService, BasketQueryService>();
|
||||||
services.AddSingleton<IUriComposer>(new UriComposer(configuration.Get<CatalogSettings>()));
|
|
||||||
|
var catalogSettings = configuration.Get<CatalogSettings>() ?? new CatalogSettings();
|
||||||
|
services.AddSingleton<IUriComposer>(new UriComposer(catalogSettings));
|
||||||
|
|
||||||
services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
|
services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
|
||||||
services.AddTransient<IEmailSender, EmailSender>();
|
services.AddTransient<IEmailSender, EmailSender>();
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ public static class ConfigureWebServices
|
|||||||
{
|
{
|
||||||
public static IServiceCollection AddWebServices(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddWebServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddMediatR(typeof(BasketViewModelService).Assembly);
|
services.AddMediatR(cfg =>
|
||||||
|
cfg.RegisterServicesFromAssembly(typeof(BasketViewModelService).Assembly));
|
||||||
services.AddScoped<IBasketViewModelService, BasketViewModelService>();
|
services.AddScoped<IBasketViewModelService, BasketViewModelService>();
|
||||||
services.AddScoped<CatalogViewModelService>();
|
services.AddScoped<CatalogViewModelService>();
|
||||||
services.AddScoped<ICatalogItemViewModelService, CatalogItemViewModelService>();
|
services.AddScoped<ICatalogItemViewModelService, CatalogItemViewModelService>();
|
||||||
|
|||||||
@@ -122,6 +122,11 @@ public class ManageController : Controller
|
|||||||
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
|
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
|
||||||
Guard.Against.Null(callbackUrl, nameof(callbackUrl));
|
Guard.Against.Null(callbackUrl, nameof(callbackUrl));
|
||||||
var email = user.Email;
|
var email = user.Email;
|
||||||
|
if (email == null)
|
||||||
|
{
|
||||||
|
throw new ApplicationException($"No email associated with user {user.UserName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
|
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
|
||||||
|
|
||||||
StatusMessage = "Verification email sent. Please check your email.";
|
StatusMessage = "Verification email sent. Please check your email.";
|
||||||
@@ -162,7 +167,8 @@ public class ManageController : Controller
|
|||||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var changePasswordResult = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
|
var changePasswordResult = await _userManager
|
||||||
|
.ChangePasswordAsync(user, model.OldPassword!, model.NewPassword!);
|
||||||
if (!changePasswordResult.Succeeded)
|
if (!changePasswordResult.Succeeded)
|
||||||
{
|
{
|
||||||
AddErrors(changePasswordResult);
|
AddErrors(changePasswordResult);
|
||||||
@@ -211,7 +217,7 @@ public class ManageController : Controller
|
|||||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword);
|
var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword!);
|
||||||
if (!addPasswordResult.Succeeded)
|
if (!addPasswordResult.Succeeded)
|
||||||
{
|
{
|
||||||
AddErrors(addPasswordResult);
|
AddErrors(addPasswordResult);
|
||||||
@@ -293,6 +299,10 @@ public class ManageController : Controller
|
|||||||
{
|
{
|
||||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
}
|
}
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey);
|
var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey);
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
@@ -407,7 +417,7 @@ public class ManageController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Strip spaces and hypens
|
// Strip spaces and hypens
|
||||||
var verificationCode = model.Code?.Replace(" ", string.Empty).Replace("-", string.Empty);
|
string verificationCode = model.Code?.Replace(" ", string.Empty).Replace("-", string.Empty) ?? "";
|
||||||
|
|
||||||
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
||||||
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||||
@@ -421,7 +431,7 @@ public class ManageController : Controller
|
|||||||
|
|
||||||
await _userManager.SetTwoFactorEnabledAsync(user, true);
|
await _userManager.SetTwoFactorEnabledAsync(user, true);
|
||||||
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
|
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
|
||||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10) ?? new List<string>();
|
||||||
TempData[RecoveryCodesKey] = recoveryCodes.ToArray();
|
TempData[RecoveryCodesKey] = recoveryCodes.ToArray();
|
||||||
|
|
||||||
return RedirectToAction(nameof(ShowRecoveryCodes));
|
return RedirectToAction(nameof(ShowRecoveryCodes));
|
||||||
@@ -465,7 +475,7 @@ public class ManageController : Controller
|
|||||||
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
|
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10) ?? new List<string>();
|
||||||
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
|
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
|
||||||
|
|
||||||
var model = new ShowRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() };
|
var model = new ShowRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() };
|
||||||
@@ -533,8 +543,8 @@ public class ManageController : Controller
|
|||||||
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
model.SharedKey = FormatKey(unformattedKey);
|
model.SharedKey = FormatKey(unformattedKey!);
|
||||||
model.AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey);
|
model.AuthenticatorUri = GenerateQrCodeUri(user.Email!, unformattedKey!);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class IndexModel : PageModel
|
|||||||
_catalogViewModelService = catalogViewModelService;
|
_catalogViewModelService = catalogViewModelService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel();
|
public required CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel();
|
||||||
|
|
||||||
public async Task OnGet(CatalogIndexViewModel catalogModel, int? pageId)
|
public async Task OnGet(CatalogIndexViewModel catalogModel, int? pageId)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ var baseUrlConfig = configSection.Get<BaseUrlConfiguration>();
|
|||||||
// Blazor Admin Required Services for Prerendering
|
// Blazor Admin Required Services for Prerendering
|
||||||
builder.Services.AddScoped<HttpClient>(s => new HttpClient
|
builder.Services.AddScoped<HttpClient>(s => new HttpClient
|
||||||
{
|
{
|
||||||
BaseAddress = new Uri(baseUrlConfig.WebBase)
|
BaseAddress = new Uri(baseUrlConfig!.WebBase)
|
||||||
});
|
});
|
||||||
|
|
||||||
// add blazor services
|
// add blazor services
|
||||||
@@ -195,15 +195,13 @@ app.UseCookiePolicy();
|
|||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.UseEndpoints(endpoints =>
|
|
||||||
{
|
app.MapControllerRoute("default", "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
|
||||||
endpoints.MapControllerRoute("default", "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
|
app.MapRazorPages();
|
||||||
endpoints.MapRazorPages();
|
app.MapHealthChecks("home_page_health_check", new HealthCheckOptions { Predicate = check => check.Tags.Contains("homePageHealthCheck") });
|
||||||
endpoints.MapHealthChecks("home_page_health_check", new HealthCheckOptions { Predicate = check => check.Tags.Contains("homePageHealthCheck") });
|
app.MapHealthChecks("api_health_check", new HealthCheckOptions { Predicate = check => check.Tags.Contains("apiHealthCheck") });
|
||||||
endpoints.MapHealthChecks("api_health_check", new HealthCheckOptions { Predicate = check => check.Tags.Contains("apiHealthCheck") });
|
//endpoints.MapBlazorHub("/admin");
|
||||||
//endpoints.MapBlazorHub("/admin");
|
app.MapFallbackToFile("index.html");
|
||||||
endpoints.MapFallbackToFile("index.html");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.Logger.LogInformation("LAUNCHING");
|
app.Logger.LogInformation("LAUNCHING");
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -21,30 +21,30 @@ public class CachedCatalogViewModelService : ICatalogViewModelService
|
|||||||
|
|
||||||
public async Task<IEnumerable<SelectListItem>> GetBrands()
|
public async Task<IEnumerable<SelectListItem>> GetBrands()
|
||||||
{
|
{
|
||||||
return await _cache.GetOrCreateAsync(CacheHelpers.GenerateBrandsCacheKey(), async entry =>
|
return (await _cache.GetOrCreateAsync(CacheHelpers.GenerateBrandsCacheKey(), async entry =>
|
||||||
{
|
{
|
||||||
entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration;
|
entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration;
|
||||||
return await _catalogViewModelService.GetBrands();
|
return await _catalogViewModelService.GetBrands();
|
||||||
});
|
})) ?? new List<SelectListItem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CatalogIndexViewModel> GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId)
|
public async Task<CatalogIndexViewModel> GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId)
|
||||||
{
|
{
|
||||||
var cacheKey = CacheHelpers.GenerateCatalogItemCacheKey(pageIndex, Constants.ITEMS_PER_PAGE, brandId, typeId);
|
var cacheKey = CacheHelpers.GenerateCatalogItemCacheKey(pageIndex, Constants.ITEMS_PER_PAGE, brandId, typeId);
|
||||||
|
|
||||||
return await _cache.GetOrCreateAsync(cacheKey, async entry =>
|
return (await _cache.GetOrCreateAsync(cacheKey, async entry =>
|
||||||
{
|
{
|
||||||
entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration;
|
entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration;
|
||||||
return await _catalogViewModelService.GetCatalogItems(pageIndex, itemsPage, brandId, typeId);
|
return await _catalogViewModelService.GetCatalogItems(pageIndex, itemsPage, brandId, typeId);
|
||||||
});
|
})) ?? new CatalogIndexViewModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<SelectListItem>> GetTypes()
|
public async Task<IEnumerable<SelectListItem>> GetTypes()
|
||||||
{
|
{
|
||||||
return await _cache.GetOrCreateAsync(CacheHelpers.GenerateTypesCacheKey(), async entry =>
|
return (await _cache.GetOrCreateAsync(CacheHelpers.GenerateTypesCacheKey(), async entry =>
|
||||||
{
|
{
|
||||||
entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration;
|
entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration;
|
||||||
return await _catalogViewModelService.GetTypes();
|
return await _catalogViewModelService.GetTypes();
|
||||||
});
|
})) ?? new List<SelectListItem>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
using System.Collections.Generic;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.ViewModels;
|
namespace Microsoft.eShopWeb.Web.ViewModels;
|
||||||
|
|
||||||
public class CatalogIndexViewModel
|
public class CatalogIndexViewModel
|
||||||
{
|
{
|
||||||
public List<CatalogItemViewModel>? CatalogItems { get; set; }
|
public List<CatalogItemViewModel> CatalogItems { get; set; } = new List<CatalogItemViewModel>();
|
||||||
public List<SelectListItem>? Brands { get; set; }
|
public List<SelectListItem>? Brands { get; set; } = new List<SelectListItem>();
|
||||||
public List<SelectListItem>? Types { get; set; }
|
public List<SelectListItem>? Types { get; set; } = new List<SelectListItem>();
|
||||||
public int? BrandFilterApplied { get; set; }
|
public int? BrandFilterApplied { get; set; }
|
||||||
public int? TypesFilterApplied { get; set; }
|
public int? TypesFilterApplied { get; set; }
|
||||||
public PaginationInfoViewModel? PaginationInfo { get; set; }
|
public PaginationInfoViewModel? PaginationInfo { get; set; }
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
namespace Microsoft.eShopWeb.Web.ViewModels.Manage;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.Web.ViewModels.Manage;
|
||||||
|
|
||||||
public class RemoveLoginViewModel
|
public class RemoveLoginViewModel
|
||||||
{
|
{
|
||||||
public string? LoginProvider { get; set; }
|
[Required]
|
||||||
public string? ProviderKey { get; set; }
|
public string LoginProvider { get; set; } = string.Empty;
|
||||||
|
[Required]
|
||||||
|
public string ProviderKey { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.ViewModels;
|
namespace Microsoft.eShopWeb.Web.ViewModels;
|
||||||
|
|
||||||
@@ -13,5 +11,5 @@ public class OrderViewModel
|
|||||||
public decimal Total { get; set; }
|
public decimal Total { get; set; }
|
||||||
public string Status => DEFAULT_STATUS;
|
public string Status => DEFAULT_STATUS;
|
||||||
public Address? ShippingAddress { get; set; }
|
public Address? ShippingAddress { get; set; }
|
||||||
public List<OrderItemViewModel> OrderItems { get; set; } = new List<OrderItemViewModel>();
|
public List<OrderItemViewModel> OrderItems { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
@for (var row = 0; row < Model.RecoveryCodes.Length; row += 2)
|
@if (Model.RecoveryCodes != null)
|
||||||
{
|
{
|
||||||
<code>@Model.RecoveryCodes[row]</code><text> </text><code>@Model.RecoveryCodes[row + 1]</code><br />
|
@for (var row = 0; row < Model.RecoveryCodes.Length; row += 2)
|
||||||
|
{
|
||||||
|
<code>@Model.RecoveryCodes[row]</code><text> </text><code>@Model.RecoveryCodes[row + 1]</code><br />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
© 2021 GitHub, Inc.
|
© 2023 GitHub, Inc.
|
||||||
@@ -30,15 +30,15 @@
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-orders-detail-items row">
|
<article class="esh-orders-detail-items row">
|
||||||
<section class="esh-orders-detail-item col-xs-12">@Model.ShippingAddress.Street</section>
|
<section class="esh-orders-detail-item col-xs-12">@Model.ShippingAddress?.Street</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-orders-detail-items row">
|
<article class="esh-orders-detail-items row">
|
||||||
<section class="esh-orders-detail-item col-xs-12">@Model.ShippingAddress.City</section>
|
<section class="esh-orders-detail-item col-xs-12">@Model.ShippingAddress?.City</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="esh-orders-detail-items row">
|
<article class="esh-orders-detail-items row">
|
||||||
<section class="esh-orders-detail-item col-xs-12">@Model.ShippingAddress.Country</section>
|
<section class="esh-orders-detail-item col-xs-12">@Model.ShippingAddress?.Country</section>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@if (Context.User.Identity.IsAuthenticated)
|
@if (Context!.User!.Identity!.IsAuthenticated)
|
||||||
{
|
{
|
||||||
<section class="col-lg-4 col-md-5 col-xs-12">
|
<section class="col-lg-4 col-md-5 col-xs-12">
|
||||||
<div class="esh-identity">
|
<div class="esh-identity">
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
|
||||||
<PackageReference Include="Azure.Identity" />
|
<PackageReference Include="Azure.Identity" />
|
||||||
<PackageReference Include="MediatR" />
|
<PackageReference Include="MediatR" />
|
||||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" />
|
|
||||||
<PackageReference Include="BuildBundlerMinifier" Condition="'$(Configuration)'=='Release'" />
|
<PackageReference Include="BuildBundlerMinifier" Condition="'$(Configuration)'=='Release'" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"CatalogConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;",
|
"CatalogConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;TrustServerCertificate=true;",
|
||||||
"IdentityConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;"
|
"IdentityConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;TrustServerCertificate=true;"
|
||||||
},
|
},
|
||||||
"baseUrls": {
|
"baseUrls": {
|
||||||
"apiBase": "http://localhost:5200/api/",
|
"apiBase": "http://localhost:5200/api/",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class OrderIndexOnGet : IClassFixture<TestApplication>
|
|||||||
public async Task ReturnsRedirectGivenAnonymousUser()
|
public async Task ReturnsRedirectGivenAnonymousUser()
|
||||||
{
|
{
|
||||||
var response = await Client.GetAsync("/order/my-orders");
|
var response = await Client.GetAsync("/order/my-orders");
|
||||||
var redirectLocation = response.Headers.Location.OriginalString;
|
var redirectLocation = response!.Headers.Location!.OriginalString;
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
||||||
Assert.Contains("/Account/Login", redirectLocation);
|
Assert.Contains("/Account/Login", redirectLocation);
|
||||||
|
|||||||
@@ -45,6 +45,6 @@ public class BasketPageCheckout : IClassFixture<TestApplication>
|
|||||||
|
|
||||||
formContent = new FormUrlEncodedContent(keyValues);
|
formContent = new FormUrlEncodedContent(keyValues);
|
||||||
var postResponse2 = await Client.PostAsync("/Basket/Checkout", formContent);
|
var postResponse2 = await Client.PostAsync("/Basket/Checkout", formContent);
|
||||||
Assert.Contains("/Identity/Account/Login", postResponse2.RequestMessage.RequestUri.ToString());
|
Assert.Contains("/Identity/Account/Login", postResponse2!.RequestMessage!.RequestUri!.ToString()!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public class CheckoutTest : IClassFixture<TestApplication>
|
|||||||
var checkOutResponse = await Client.PostAsync("/basket/checkout", checkOutContent);
|
var checkOutResponse = await Client.PostAsync("/basket/checkout", checkOutContent);
|
||||||
var stringCheckOutResponse = await checkOutResponse.Content.ReadAsStringAsync();
|
var stringCheckOutResponse = await checkOutResponse.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
Assert.Contains("/Basket/Success", checkOutResponse.RequestMessage.RequestUri.ToString());
|
Assert.Contains("/Basket/Success", checkOutResponse.RequestMessage!.RequestUri!.ToString());
|
||||||
Assert.Contains("Thanks for your Order!", stringCheckOutResponse);
|
Assert.Contains("Thanks for your Order!", stringCheckOutResponse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class IndexTest : IClassFixture<TestApplication>
|
|||||||
|
|
||||||
var stringUpdateResponse = await updateResponse.Content.ReadAsStringAsync();
|
var stringUpdateResponse = await updateResponse.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
Assert.Contains("/basket/update", updateResponse.RequestMessage.RequestUri.ToString());
|
Assert.Contains("/basket/update", updateResponse!.RequestMessage!.RequestUri!.ToString()!);
|
||||||
decimal expectedTotalAmount = 416.50M;
|
decimal expectedTotalAmount = 416.50M;
|
||||||
Assert.Contains(expectedTotalAmount.ToString("N2"), stringUpdateResponse);
|
Assert.Contains(expectedTotalAmount.ToString("N2"), stringUpdateResponse);
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ public class IndexTest : IClassFixture<TestApplication>
|
|||||||
|
|
||||||
var stringUpdateResponse = await updateResponse.Content.ReadAsStringAsync();
|
var stringUpdateResponse = await updateResponse.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
Assert.Contains("/basket/update", updateResponse.RequestMessage.RequestUri.ToString());
|
Assert.Contains("/basket/update", updateResponse!.RequestMessage!.RequestUri!.ToString()!);
|
||||||
Assert.Contains("Basket is empty", stringUpdateResponse);
|
Assert.Contains("Basket is empty", stringUpdateResponse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ public static class WebPageHelpers
|
|||||||
{
|
{
|
||||||
var regex = new Regex(regexpression);
|
var regex = new Regex(regexpression);
|
||||||
var match = regex.Match(input);
|
var match = regex.Match(input);
|
||||||
return match.Groups.Values.LastOrDefault().Value;
|
return match!.Groups!.Values!.LastOrDefault()!.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ public class TestApplication : WebApplicationFactory<IBasketViewModelService>
|
|||||||
// Add mock/test services to the builder here
|
// Add mock/test services to the builder here
|
||||||
builder.ConfigureServices(services =>
|
builder.ConfigureServices(services =>
|
||||||
{
|
{
|
||||||
|
var descriptors = services.Where(d =>
|
||||||
|
d.ServiceType == typeof(DbContextOptions<CatalogContext>) ||
|
||||||
|
d.ServiceType == typeof(DbContextOptions<AppIdentityDbContext>))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var descriptor in descriptors)
|
||||||
|
{
|
||||||
|
services.Remove(descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
services.AddScoped(sp =>
|
services.AddScoped(sp =>
|
||||||
{
|
{
|
||||||
// Replace SQLite with in-memory database for tests
|
// Replace SQLite with in-memory database for tests
|
||||||
|
|||||||
@@ -7,29 +7,28 @@ using Microsoft.eShopWeb.ApplicationCore.Constants;
|
|||||||
using Microsoft.eShopWeb.PublicApi.AuthEndpoints;
|
using Microsoft.eShopWeb.PublicApi.AuthEndpoints;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
namespace PublicApiIntegrationTests.AuthEndpoints
|
namespace PublicApiIntegrationTests.AuthEndpoints;
|
||||||
{
|
|
||||||
[TestClass]
|
|
||||||
public class AuthenticateEndpoint
|
|
||||||
{
|
|
||||||
[TestMethod]
|
|
||||||
[DataRow("demouser@microsoft.com", AuthorizationConstants.DEFAULT_PASSWORD, true)]
|
|
||||||
[DataRow("demouser@microsoft.com", "badpassword", false)]
|
|
||||||
[DataRow("baduser@microsoft.com", "badpassword", false)]
|
|
||||||
public async Task ReturnsExpectedResultGivenCredentials(string testUsername, string testPassword, bool expectedResult)
|
|
||||||
{
|
|
||||||
var request = new AuthenticateRequest()
|
|
||||||
{
|
|
||||||
Username = testUsername,
|
|
||||||
Password = testPassword
|
|
||||||
};
|
|
||||||
var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
|
|
||||||
var response = await ProgramTest.NewClient.PostAsync("api/authenticate", jsonContent);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
var stringResponse = await response.Content.ReadAsStringAsync();
|
|
||||||
var model = stringResponse.FromJson<AuthenticateResponse>();
|
|
||||||
|
|
||||||
Assert.AreEqual(expectedResult, model.Result);
|
[TestClass]
|
||||||
}
|
public class AuthenticateEndpoint
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow("demouser@microsoft.com", AuthorizationConstants.DEFAULT_PASSWORD, true)]
|
||||||
|
[DataRow("demouser@microsoft.com", "badpassword", false)]
|
||||||
|
[DataRow("baduser@microsoft.com", "badpassword", false)]
|
||||||
|
public async Task ReturnsExpectedResultGivenCredentials(string testUsername, string testPassword, bool expectedResult)
|
||||||
|
{
|
||||||
|
var request = new AuthenticateRequest()
|
||||||
|
{
|
||||||
|
Username = testUsername,
|
||||||
|
Password = testPassword
|
||||||
|
};
|
||||||
|
var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
|
||||||
|
var response = await ProgramTest.NewClient.PostAsync("api/authenticate", jsonContent);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var stringResponse = await response.Content.ReadAsStringAsync();
|
||||||
|
var model = stringResponse.FromJson<AuthenticateResponse>();
|
||||||
|
|
||||||
|
Assert.AreEqual(expectedResult, model!.Result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,29 +4,28 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace PublicApiIntegrationTests.CatalogItemEndpoints
|
namespace PublicApiIntegrationTests.CatalogItemEndpoints;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class CatalogItemGetByIdEndpointTest
|
||||||
{
|
{
|
||||||
[TestClass]
|
[TestMethod]
|
||||||
public class CatalogItemGetByIdEndpointTest
|
public async Task ReturnsItemGivenValidId()
|
||||||
{
|
{
|
||||||
[TestMethod]
|
var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/5");
|
||||||
public async Task ReturnsItemGivenValidId()
|
response.EnsureSuccessStatusCode();
|
||||||
{
|
var stringResponse = await response.Content.ReadAsStringAsync();
|
||||||
var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/5");
|
var model = stringResponse.FromJson<GetByIdCatalogItemResponse>();
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
var stringResponse = await response.Content.ReadAsStringAsync();
|
|
||||||
var model = stringResponse.FromJson<GetByIdCatalogItemResponse>();
|
|
||||||
|
|
||||||
Assert.AreEqual(5, model.CatalogItem.Id);
|
Assert.AreEqual(5, model!.CatalogItem.Id);
|
||||||
Assert.AreEqual("Roslyn Red Sheet", model.CatalogItem.Name);
|
Assert.AreEqual("Roslyn Red Sheet", model.CatalogItem.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task ReturnsNotFoundGivenInvalidId()
|
public async Task ReturnsNotFoundGivenInvalidId()
|
||||||
{
|
{
|
||||||
var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/0");
|
var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/0");
|
||||||
|
|
||||||
Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode);
|
Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,66 +8,65 @@ using System.Net.Http;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace PublicApiIntegrationTests.CatalogItemEndpoints
|
namespace PublicApiIntegrationTests.CatalogItemEndpoints;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class CatalogItemListPagedEndpoint
|
||||||
{
|
{
|
||||||
[TestClass]
|
[TestMethod]
|
||||||
public class CatalogItemListPagedEndpoint
|
public async Task ReturnsFirst10CatalogItems()
|
||||||
{
|
{
|
||||||
[TestMethod]
|
var client = ProgramTest.NewClient;
|
||||||
public async Task ReturnsFirst10CatalogItems()
|
var response = await client.GetAsync("/api/catalog-items?pageSize=10");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var stringResponse = await response.Content.ReadAsStringAsync();
|
||||||
|
var model = stringResponse.FromJson<CatalogIndexViewModel>();
|
||||||
|
|
||||||
|
Assert.AreEqual(10, model!.CatalogItems.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ReturnsCorrectCatalogItemsGivenPageIndex1()
|
||||||
|
{
|
||||||
|
|
||||||
|
var pageSize = 10;
|
||||||
|
var pageIndex = 1;
|
||||||
|
|
||||||
|
var client = ProgramTest.NewClient;
|
||||||
|
var response = await client.GetAsync($"/api/catalog-items");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var stringResponse = await response.Content.ReadAsStringAsync();
|
||||||
|
var model = stringResponse.FromJson<ListPagedCatalogItemResponse>();
|
||||||
|
var totalItem = model!.CatalogItems.Count();
|
||||||
|
|
||||||
|
var response2 = await client.GetAsync($"/api/catalog-items?pageSize={pageSize}&pageIndex={pageIndex}");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var stringResponse2 = await response2.Content.ReadAsStringAsync();
|
||||||
|
var model2 = stringResponse2.FromJson<ListPagedCatalogItemResponse>();
|
||||||
|
|
||||||
|
var totalExpected = totalItem - (pageSize * pageIndex);
|
||||||
|
|
||||||
|
Assert.AreEqual(totalExpected, model2!.CatalogItems.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataTestMethod]
|
||||||
|
[DataRow("catalog-items")]
|
||||||
|
[DataRow("catalog-brands")]
|
||||||
|
[DataRow("catalog-types")]
|
||||||
|
[DataRow("catalog-items/1")]
|
||||||
|
public async Task SuccessFullMutipleParallelCall(string endpointName)
|
||||||
|
{
|
||||||
|
var client = ProgramTest.NewClient;
|
||||||
|
var tasks = new List<Task<HttpResponseMessage>>();
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
{
|
{
|
||||||
var client = ProgramTest.NewClient;
|
var task = client.GetAsync($"/api/{endpointName}");
|
||||||
var response = await client.GetAsync("/api/catalog-items?pageSize=10");
|
tasks.Add(task);
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
var stringResponse = await response.Content.ReadAsStringAsync();
|
|
||||||
var model = stringResponse.FromJson<CatalogIndexViewModel>();
|
|
||||||
|
|
||||||
Assert.AreEqual(10, model.CatalogItems.Count());
|
|
||||||
}
|
}
|
||||||
|
await Task.WhenAll(tasks.ToList());
|
||||||
|
var totalKO = tasks.Count(t => t.Result.StatusCode != HttpStatusCode.OK);
|
||||||
|
|
||||||
[TestMethod]
|
Assert.AreEqual(0, totalKO);
|
||||||
public async Task ReturnsCorrectCatalogItemsGivenPageIndex1()
|
|
||||||
{
|
|
||||||
|
|
||||||
var pageSize = 10;
|
|
||||||
var pageIndex = 1;
|
|
||||||
|
|
||||||
var client = ProgramTest.NewClient;
|
|
||||||
var response = await client.GetAsync($"/api/catalog-items");
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
var stringResponse = await response.Content.ReadAsStringAsync();
|
|
||||||
var model = stringResponse.FromJson<ListPagedCatalogItemResponse>();
|
|
||||||
var totalItem = model.CatalogItems.Count();
|
|
||||||
|
|
||||||
var response2 = await client.GetAsync($"/api/catalog-items?pageSize={pageSize}&pageIndex={pageIndex}");
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
var stringResponse2 = await response2.Content.ReadAsStringAsync();
|
|
||||||
var model2 = stringResponse2.FromJson<ListPagedCatalogItemResponse>();
|
|
||||||
|
|
||||||
var totalExpected = totalItem - (pageSize * pageIndex);
|
|
||||||
|
|
||||||
Assert.AreEqual(totalExpected, model2.CatalogItems.Count());
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataTestMethod]
|
|
||||||
[DataRow("catalog-items")]
|
|
||||||
[DataRow("catalog-brands")]
|
|
||||||
[DataRow("catalog-types")]
|
|
||||||
[DataRow("catalog-items/1")]
|
|
||||||
public async Task SuccessFullMutipleParallelCall(string endpointName)
|
|
||||||
{
|
|
||||||
var client = ProgramTest.NewClient;
|
|
||||||
var tasks = new List<Task<HttpResponseMessage>>();
|
|
||||||
|
|
||||||
for (int i = 0; i < 100; i++)
|
|
||||||
{
|
|
||||||
var task = client.GetAsync($"/api/{endpointName}");
|
|
||||||
tasks.Add(task);
|
|
||||||
}
|
|
||||||
await Task.WhenAll(tasks.ToList());
|
|
||||||
var totalKO = tasks.Count(t => t.Result.StatusCode != HttpStatusCode.OK);
|
|
||||||
|
|
||||||
Assert.AreEqual(0, totalKO);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,62 +8,61 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace PublicApiIntegrationTests.AuthEndpoints
|
namespace PublicApiIntegrationTests.AuthEndpoints;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class CreateCatalogItemEndpointTest
|
||||||
{
|
{
|
||||||
[TestClass]
|
private int _testBrandId = 1;
|
||||||
public class CreateCatalogItemEndpointTest
|
private int _testTypeId = 2;
|
||||||
|
private string _testDescription = "test description";
|
||||||
|
private string _testName = "test name";
|
||||||
|
private decimal _testPrice = 1.23m;
|
||||||
|
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ReturnsNotAuthorizedGivenNormalUserToken()
|
||||||
{
|
{
|
||||||
private int _testBrandId = 1;
|
var jsonContent = GetValidNewItemJson();
|
||||||
private int _testTypeId = 2;
|
var token = ApiTokenHelper.GetNormalUserToken();
|
||||||
private string _testDescription = "test description";
|
var client = ProgramTest.NewClient;
|
||||||
private string _testName = "test name";
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
private decimal _testPrice = 1.23m;
|
var response = await client.PostAsync("api/catalog-items", jsonContent);
|
||||||
|
|
||||||
|
Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task ReturnsNotAuthorizedGivenNormalUserToken()
|
public async Task ReturnsSuccessGivenValidNewItemAndAdminUserToken()
|
||||||
|
{
|
||||||
|
var jsonContent = GetValidNewItemJson();
|
||||||
|
var adminToken = ApiTokenHelper.GetAdminUserToken();
|
||||||
|
var client = ProgramTest.NewClient;
|
||||||
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
||||||
|
var response = await client.PostAsync("api/catalog-items", jsonContent);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var stringResponse = await response.Content.ReadAsStringAsync();
|
||||||
|
var model = stringResponse.FromJson<CreateCatalogItemResponse>();
|
||||||
|
|
||||||
|
Assert.AreEqual(_testBrandId, model!.CatalogItem.CatalogBrandId);
|
||||||
|
Assert.AreEqual(_testTypeId, model.CatalogItem.CatalogTypeId);
|
||||||
|
Assert.AreEqual(_testDescription, model.CatalogItem.Description);
|
||||||
|
Assert.AreEqual(_testName, model.CatalogItem.Name);
|
||||||
|
Assert.AreEqual(_testPrice, model.CatalogItem.Price);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StringContent GetValidNewItemJson()
|
||||||
|
{
|
||||||
|
var request = new CreateCatalogItemRequest()
|
||||||
{
|
{
|
||||||
var jsonContent = GetValidNewItemJson();
|
CatalogBrandId = _testBrandId,
|
||||||
var token = ApiTokenHelper.GetNormalUserToken();
|
CatalogTypeId = _testTypeId,
|
||||||
var client = ProgramTest.NewClient;
|
Description = _testDescription,
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
Name = _testName,
|
||||||
var response = await client.PostAsync("api/catalog-items", jsonContent);
|
Price = _testPrice
|
||||||
|
};
|
||||||
|
var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode);
|
return jsonContent;
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public async Task ReturnsSuccessGivenValidNewItemAndAdminUserToken()
|
|
||||||
{
|
|
||||||
var jsonContent = GetValidNewItemJson();
|
|
||||||
var adminToken = ApiTokenHelper.GetAdminUserToken();
|
|
||||||
var client = ProgramTest.NewClient;
|
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
|
||||||
var response = await client.PostAsync("api/catalog-items", jsonContent);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
var stringResponse = await response.Content.ReadAsStringAsync();
|
|
||||||
var model = stringResponse.FromJson<CreateCatalogItemResponse>();
|
|
||||||
|
|
||||||
Assert.AreEqual(_testBrandId, model.CatalogItem.CatalogBrandId);
|
|
||||||
Assert.AreEqual(_testTypeId, model.CatalogItem.CatalogTypeId);
|
|
||||||
Assert.AreEqual(_testDescription, model.CatalogItem.Description);
|
|
||||||
Assert.AreEqual(_testName, model.CatalogItem.Name);
|
|
||||||
Assert.AreEqual(_testPrice, model.CatalogItem.Price);
|
|
||||||
}
|
|
||||||
|
|
||||||
private StringContent GetValidNewItemJson()
|
|
||||||
{
|
|
||||||
var request = new CreateCatalogItemRequest()
|
|
||||||
{
|
|
||||||
CatalogBrandId = _testBrandId,
|
|
||||||
CatalogTypeId = _testTypeId,
|
|
||||||
Description = _testDescription,
|
|
||||||
Name = _testName,
|
|
||||||
Price = _testPrice
|
|
||||||
};
|
|
||||||
var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
|
|
||||||
|
|
||||||
return jsonContent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,34 +5,33 @@ using System.Net;
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace PublicApiIntegrationTests.CatalogItemEndpoints
|
namespace PublicApiIntegrationTests.CatalogItemEndpoints;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class DeleteCatalogItemEndpointTest
|
||||||
{
|
{
|
||||||
[TestClass]
|
[TestMethod]
|
||||||
public class DeleteCatalogItemEndpointTest
|
public async Task ReturnsSuccessGivenValidIdAndAdminUserToken()
|
||||||
{
|
{
|
||||||
[TestMethod]
|
var adminToken = ApiTokenHelper.GetAdminUserToken();
|
||||||
public async Task ReturnsSuccessGivenValidIdAndAdminUserToken()
|
var client = ProgramTest.NewClient;
|
||||||
{
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
||||||
var adminToken = ApiTokenHelper.GetAdminUserToken();
|
var response = await client.DeleteAsync("api/catalog-items/12");
|
||||||
var client = ProgramTest.NewClient;
|
response.EnsureSuccessStatusCode();
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
var stringResponse = await response.Content.ReadAsStringAsync();
|
||||||
var response = await client.DeleteAsync("api/catalog-items/12");
|
var model = stringResponse.FromJson<DeleteCatalogItemResponse>();
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
var stringResponse = await response.Content.ReadAsStringAsync();
|
|
||||||
var model = stringResponse.FromJson<DeleteCatalogItemResponse>();
|
|
||||||
|
|
||||||
Assert.AreEqual("Deleted", model.Status);
|
Assert.AreEqual("Deleted", model!.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task ReturnsNotFoundGivenInvalidIdAndAdminUserToken()
|
public async Task ReturnsNotFoundGivenInvalidIdAndAdminUserToken()
|
||||||
{
|
{
|
||||||
var adminToken = ApiTokenHelper.GetAdminUserToken();
|
var adminToken = ApiTokenHelper.GetAdminUserToken();
|
||||||
var client = ProgramTest.NewClient;
|
var client = ProgramTest.NewClient;
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
||||||
var response = await client.DeleteAsync("api/catalog-items/0");
|
var response = await client.DeleteAsync("api/catalog-items/0");
|
||||||
|
|
||||||
Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode);
|
Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,25 @@
|
|||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
||||||
namespace PublicApiIntegrationTests
|
namespace PublicApiIntegrationTests;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class ProgramTest
|
||||||
{
|
{
|
||||||
[TestClass]
|
private static WebApplicationFactory<Program> _application = new();
|
||||||
public class ProgramTest
|
|
||||||
|
public static HttpClient NewClient
|
||||||
{
|
{
|
||||||
private static WebApplicationFactory<Program> _application;
|
get
|
||||||
|
|
||||||
public static HttpClient NewClient
|
|
||||||
{
|
{
|
||||||
get
|
return _application.CreateClient();
|
||||||
{
|
|
||||||
return _application.CreateClient();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[AssemblyInitialize]
|
|
||||||
public static void AssemblyInitialize(TestContext _)
|
|
||||||
{
|
|
||||||
_application = new WebApplicationFactory<Program>();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AssemblyInitialize]
|
||||||
|
public static void AssemblyInitialize(TestContext _)
|
||||||
|
{
|
||||||
|
_application = new WebApplicationFactory<Program>();
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user