Adding Endpoints with Authorization in separate PublicApi project (#413)

* Adding tests for GetById endpoint

* Updating tests and messages

* Adding paged endpoint and also AutoMapper

* Authenticate endpoint works as bool with tests

* Got JWT token security working with Create and Delete endpoints and Swashbuckle.

* Working on getting cookie and jwt token auth working in the same app
All tests are passing

* Creating new project and moving APIs
Build succeeds; tests need updated.

* all tests passing after moving services to PublicApi project

* Fix authorize attributes

* Uncomment and update ApiCatalogControllerLists tests

Co-authored-by: Eric Fleming <eric-fleming18@hotmail.com>
This commit is contained in:
Steve Smith
2020-06-30 11:36:28 -04:00
committed by GitHub
parent e5e9868003
commit dd5a435cb4
49 changed files with 1024 additions and 75 deletions

View File

@@ -30,12 +30,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject EndProject
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{1FCBE191-34FE-4B2E-8915-CA81553958AD}" Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{1FCBE191-34FE-4B2E-8915-CA81553958AD}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicApi", "src\PublicApi\PublicApi.csproj", "{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{227CF035-29B0-448D-97E4-944F9EA850E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{227CF035-29B0-448D-97E4-944F9EA850E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{227CF035-29B0-448D-97E4-944F9EA850E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{227CF035-29B0-448D-97E4-944F9EA850E5}.Release|Any CPU.Build.0 = Release|Any CPU
{7C461394-ABDC-43CD-A798-71249C58BA67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7C461394-ABDC-43CD-A798-71249C58BA67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C461394-ABDC-43CD-A798-71249C58BA67}.Debug|Any CPU.Build.0 = Debug|Any CPU {7C461394-ABDC-43CD-A798-71249C58BA67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C461394-ABDC-43CD-A798-71249C58BA67}.Release|Any CPU.ActiveCfg = Release|Any CPU {7C461394-ABDC-43CD-A798-71249C58BA67}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -56,25 +62,26 @@ Global
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Debug|Any CPU.Build.0 = Debug|Any CPU {7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Release|Any CPU.Build.0 = Release|Any CPU {7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Release|Any CPU.Build.0 = Release|Any CPU
{227CF035-29B0-448D-97E4-944F9EA850E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{227CF035-29B0-448D-97E4-944F9EA850E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{227CF035-29B0-448D-97E4-944F9EA850E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{227CF035-29B0-448D-97E4-944F9EA850E5}.Release|Any CPU.Build.0 = Release|Any CPU
{1FCBE191-34FE-4B2E-8915-CA81553958AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1FCBE191-34FE-4B2E-8915-CA81553958AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1FCBE191-34FE-4B2E-8915-CA81553958AD}.Debug|Any CPU.Build.0 = Debug|Any CPU {1FCBE191-34FE-4B2E-8915-CA81553958AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1FCBE191-34FE-4B2E-8915-CA81553958AD}.Release|Any CPU.ActiveCfg = Release|Any CPU {1FCBE191-34FE-4B2E-8915-CA81553958AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1FCBE191-34FE-4B2E-8915-CA81553958AD}.Release|Any CPU.Build.0 = Release|Any CPU {1FCBE191-34FE-4B2E-8915-CA81553958AD}.Release|Any CPU.Build.0 = Release|Any CPU
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{227CF035-29B0-448D-97E4-944F9EA850E5} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
{7C461394-ABDC-43CD-A798-71249C58BA67} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {7C461394-ABDC-43CD-A798-71249C58BA67} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
{7FED7440-2311-4D1E-958B-3E887C585CD2} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {7FED7440-2311-4D1E-958B-3E887C585CD2} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
{EF6877E6-59CB-43A7-8C2C-E70DD70CC5B6} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF} {EF6877E6-59CB-43A7-8C2C-E70DD70CC5B6} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF}
{0F576306-7E2D-49B7-87B1-EB5D94CFD5FC} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF} {0F576306-7E2D-49B7-87B1-EB5D94CFD5FC} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF}
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF} {7EFB5482-F942-4C3D-94B0-9B70596E6D0A} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF}
{227CF035-29B0-448D-97E4-944F9EA850E5} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {B5E4F33C-4667-4A55-AF6A-740F84C4CF3A} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {49813262-5DA3-4D61-ABD3-493C74CE8C2B} SolutionGuid = {49813262-5DA3-4D61-ABD3-493C74CE8C2B}

View File

@@ -7,6 +7,10 @@
public const string ADMINISTRATORS = "Administrators"; public const string ADMINISTRATORS = "Administrators";
} }
// TODO: Don't use this in production
public const string DEFAULT_PASSWORD = "Pass@word1"; public const string DEFAULT_PASSWORD = "Pass@word1";
// TODO: Change this to an environment variable
public const string JWT_SECRET_KEY = "SecretKeyOfDoomThatMustBeAMinimumNumberOfBytes";
} }
} }

View File

@@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.ApplicationCore.Interfaces
{
public interface ITokenClaimsService
{
Task<string> GetTokenAsync(string userName);
}
}

View File

@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.eShopWeb.ApplicationCore.Constants;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Infrastructure.Identity
{
public class IdentityTokenClaimService : ITokenClaimsService
{
private readonly UserManager<ApplicationUser> _userManager;
public IdentityTokenClaimService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task<string> GetTokenAsync(string userName)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY);
var user = await _userManager.FindByNameAsync(userName);
var roles = await _userManager.GetRolesAsync(user);
var claims = new List<Claim> { new Claim(ClaimTypes.Name, userName) };
foreach(var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims.ToArray()),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
}

View File

@@ -12,6 +12,7 @@
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.5" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5" PrivateAssets="All" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5" PrivateAssets="All" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.7.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ApplicationCore\ApplicationCore.csproj" /> <ProjectReference Include="..\ApplicationCore\ApplicationCore.csproj" />

View File

@@ -0,0 +1,8 @@
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints
{
public class AuthenticateRequest : BaseRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints
{
public class AuthenticateResponse : BaseResponse
{
public AuthenticateResponse(Guid correlationId) : base(correlationId)
{
}
public AuthenticateResponse()
{
}
public bool Result { get; set; }
public string Token { get; set; }
}
}

View File

@@ -0,0 +1,50 @@
using Ardalis.ApiEndpoints;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Constants;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.IdentityModel.Tokens;
using Swashbuckle.AspNetCore.Annotations;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints
{
public class Authenticate : BaseAsyncEndpoint<AuthenticateRequest, AuthenticateResponse>
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ITokenClaimsService _tokenClaimsService;
public Authenticate(SignInManager<ApplicationUser> signInManager,
ITokenClaimsService tokenClaimsService)
{
_signInManager = signInManager;
_tokenClaimsService = tokenClaimsService;
}
[HttpPost("api/authenticate")]
[SwaggerOperation(
Summary = "Authenticates a user",
Description = "Authenticates a user",
OperationId = "auth.authenticate",
Tags = new[] { "AuthEndpoints" })
]
public override async Task<ActionResult<AuthenticateResponse>> HandleAsync(AuthenticateRequest request)
{
var response = new AuthenticateResponse(request.CorrelationId());
var result = await _signInManager.PasswordSignInAsync(request.Username, request.Password, false, true);
response.Result = result.Succeeded;
response.Token = await _tokenClaimsService.GetTokenAsync(request.Username);
return response;
}
}
}

View File

@@ -1,6 +1,6 @@
using System; using System;
namespace Microsoft.eShopWeb.Web.API namespace Microsoft.eShopWeb.PublicApi
{ {
/// <summary> /// <summary>
/// Base class used by API requests /// Base class used by API requests
@@ -13,8 +13,4 @@ namespace Microsoft.eShopWeb.Web.API
protected Guid _correlationId = Guid.NewGuid(); protected Guid _correlationId = Guid.NewGuid();
public Guid CorrelationId() => _correlationId; public Guid CorrelationId() => _correlationId;
} }
public abstract class BaseRequest : BaseMessage
{
}
} }

View File

@@ -0,0 +1,9 @@
namespace Microsoft.eShopWeb.PublicApi
{
/// <summary>
/// Base class used by API requests
/// </summary>
public abstract class BaseRequest : BaseMessage
{
}
}

View File

@@ -1,6 +1,6 @@
using System; using System;
namespace Microsoft.eShopWeb.Web.API namespace Microsoft.eShopWeb.PublicApi
{ {
/// <summary> /// <summary>
/// Base class used by API responses /// Base class used by API responses

View File

@@ -1,4 +1,4 @@
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{ {
public class CatalogItemDto public class CatalogItemDto
{ {

View File

@@ -1,4 +1,4 @@
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{ {
public class CreateCatalogItemRequest : BaseRequest public class CreateCatalogItemRequest : BaseRequest
{ {

View File

@@ -1,6 +1,6 @@
using System; using System;
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{ {
public class CreateCatalogItemResponse : BaseResponse public class CreateCatalogItemResponse : BaseResponse
{ {
@@ -8,6 +8,10 @@ namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
{ {
} }
public CreateCatalogItemResponse()
{
}
public CatalogItemDto CatalogItem { get; set; } public CatalogItemDto CatalogItem { get; set; }
} }
} }

View File

@@ -1,12 +1,16 @@
using Ardalis.ApiEndpoints; using Ardalis.ApiEndpoints;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Constants;
using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.Annotations;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{ {
[Authorize(Roles = AuthorizationConstants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class Create : BaseAsyncEndpoint<CreateCatalogItemRequest, CreateCatalogItemResponse> public class Create : BaseAsyncEndpoint<CreateCatalogItemRequest, CreateCatalogItemResponse>
{ {
private readonly IAsyncRepository<CatalogItem> _itemRepository; private readonly IAsyncRepository<CatalogItem> _itemRepository;

View File

@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{ {
public class DeleteCatalogItemRequest : BaseRequest public class DeleteCatalogItemRequest : BaseRequest
{ {

View File

@@ -1,6 +1,6 @@
using System; using System;
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{ {
public class DeleteCatalogItemResponse : BaseResponse public class DeleteCatalogItemResponse : BaseResponse
{ {
@@ -8,6 +8,10 @@ namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
{ {
} }
public DeleteCatalogItemResponse()
{
}
public string Status { get; set; } = "Deleted"; public string Status { get; set; } = "Deleted";
} }
} }

View File

@@ -1,12 +1,16 @@
using Ardalis.ApiEndpoints; using Ardalis.ApiEndpoints;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Constants;
using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.Annotations;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{ {
[Authorize(Roles = AuthorizationConstants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class Delete : BaseAsyncEndpoint<DeleteCatalogItemRequest, DeleteCatalogItemResponse> public class Delete : BaseAsyncEndpoint<DeleteCatalogItemRequest, DeleteCatalogItemResponse>
{ {
private readonly IAsyncRepository<CatalogItem> _itemRepository; private readonly IAsyncRepository<CatalogItem> _itemRepository;

View File

@@ -1,4 +1,4 @@
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{ {
public class GetByIdCatalogItemRequest : BaseRequest public class GetByIdCatalogItemRequest : BaseRequest
{ {

View File

@@ -1,6 +1,6 @@
using System; using System;
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{ {
public class GetByIdCatalogItemResponse : BaseResponse public class GetByIdCatalogItemResponse : BaseResponse
{ {
@@ -8,6 +8,10 @@ namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
{ {
} }
public GetByIdCatalogItemResponse()
{
}
public CatalogItemDto CatalogItem { get; set; } public CatalogItemDto CatalogItem { get; set; }
} }
} }

View File

@@ -5,7 +5,7 @@ using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.Annotations;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{ {
public class GetById : BaseAsyncEndpoint<GetByIdCatalogItemRequest, GetByIdCatalogItemResponse> public class GetById : BaseAsyncEndpoint<GetByIdCatalogItemRequest, GetByIdCatalogItemResponse>
{ {

View File

@@ -0,0 +1,10 @@
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{
public class ListPagedCatalogItemRequest : BaseRequest
{
public int PageSize { get; set; }
public int PageIndex { get; set; }
public int? CatalogBrandId { get; set; }
public int? CatalogTypeId { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{
public class ListPagedCatalogItemResponse : BaseResponse
{
public ListPagedCatalogItemResponse(Guid correlationId) : base(correlationId)
{
}
public ListPagedCatalogItemResponse()
{
}
public List<CatalogItemDto> CatalogItems { get; set; } = new List<CatalogItemDto>();
public int PageCount { get; set; }
}
}

View File

@@ -0,0 +1,61 @@
using Ardalis.ApiEndpoints;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Specifications;
using Swashbuckle.AspNetCore.Annotations;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
{
public class ListPaged : BaseAsyncEndpoint<ListPagedCatalogItemRequest, ListPagedCatalogItemResponse>
{
private readonly IAsyncRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;
private readonly IMapper _mapper;
public ListPaged(IAsyncRepository<CatalogItem> itemRepository,
IUriComposer uriComposer,
IMapper mapper)
{
_itemRepository = itemRepository;
_uriComposer = uriComposer;
_mapper = mapper;
}
[HttpGet("api/catalog-items")]
[SwaggerOperation(
Summary = "List Catalog Items (paged)",
Description = "List Catalog Items (paged)",
OperationId = "catalog-items.ListPaged",
Tags = new[] { "CatalogItemEndpoints" })
]
public override async Task<ActionResult<ListPagedCatalogItemResponse>> HandleAsync([FromQuery]ListPagedCatalogItemRequest request)
{
var response = new ListPagedCatalogItemResponse(request.CorrelationId());
var filterSpec = new CatalogFilterSpecification(request.CatalogBrandId, request.CatalogTypeId);
int totalItems = await _itemRepository.CountAsync(filterSpec);
var pagedSpec = new CatalogFilterPaginatedSpecification(
skip: request.PageIndex * request.PageSize,
take: request.PageSize,
brandId: request.CatalogBrandId,
typeId: request.CatalogTypeId);
var items = await _itemRepository.ListAsync(pagedSpec);
response.CatalogItems.AddRange(items.Select(_mapper.Map<CatalogItemDto>));
foreach (CatalogItemDto item in response.CatalogItems)
{
item.PictureUri = _uriComposer.ComposePicUri(item.PictureUri);
}
response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize).ToString());
return Ok(response);
}
}
}

View File

@@ -1,7 +1,7 @@
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
namespace Microsoft.eShopWeb.Web.API namespace Microsoft.eShopWeb.PublicApi
{ {
public class CustomSchemaFilters : ISchemaFilter public class CustomSchemaFilters : ISchemaFilter
{ {

22
src/PublicApi/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["src/PublicApi/PublicApi.csproj", "src/PublicApi/"]
RUN dotnet restore "src/PublicApi/PublicApi.csproj"
COPY . .
WORKDIR "/src/src/PublicApi"
RUN dotnet build "PublicApi.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "PublicApi.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "PublicApi.dll"]

View File

@@ -0,0 +1,14 @@
using AutoMapper;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
namespace Microsoft.eShopWeb.PublicApi
{
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<CatalogItem, CatalogItemDto>();
}
}
}

51
src/PublicApi/Program.cs Normal file
View File

@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.PublicApi
{
public class Program
{
public async static Task Main(string[] args)
{
var host = CreateHostBuilder(args)
.Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
try
{
var catalogContext = services.GetRequiredService<CatalogContext>();
await CatalogContextSeed.SeedAsync(catalogContext, loggerFactory);
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
await AppIdentityDbContextSeed.SeedAsync(userManager, roleManager);
}
catch (Exception ex)
{
var logger = loggerFactory.CreateLogger<Program>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}

View File

@@ -0,0 +1,37 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:52023",
"sslPort": 44339
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"PublicApi": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
"publishAllPorts": true,
"useSSL": true
}
}
}

View File

@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>Microsoft.eShopWeb.PublicApi</RootNamespace>
<UserSecretsId>5b662463-1efd-4bae-bde4-befe0be3e8ff</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ardalis.ApiEndpoints" Version="1.0.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="5.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.9" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.7.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationCore\ApplicationCore.csproj" />
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
</ItemGroup>
</Project>

193
src/PublicApi/Startup.cs Normal file
View File

@@ -0,0 +1,193 @@
using System.Collections.Generic;
using System.Text;
using AutoMapper;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb.ApplicationCore.Constants;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Services;
using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Infrastructure.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
namespace Microsoft.eShopWeb.PublicApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureDevelopmentServices(IServiceCollection services)
{
// use in-memory database
ConfigureInMemoryDatabases(services);
// use real database
//ConfigureProductionServices(services);
}
private void ConfigureInMemoryDatabases(IServiceCollection services)
{
// use in-memory database
services.AddDbContext<CatalogContext>(c =>
c.UseInMemoryDatabase("Catalog"));
// Add Identity DbContext
services.AddDbContext<AppIdentityDbContext>(options =>
options.UseInMemoryDatabase("Identity"));
ConfigureServices(services);
}
public void ConfigureProductionServices(IServiceCollection services)
{
// use real database
// Requires LocalDB which can be installed with SQL Server Express 2016
// https://www.microsoft.com/en-us/download/details.aspx?id=54284
services.AddDbContext<CatalogContext>(c =>
c.UseSqlServer(Configuration.GetConnectionString("CatalogConnection")));
// Add Identity DbContext
services.AddDbContext<AppIdentityDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("IdentityConnection")));
ConfigureServices(services);
}
public void ConfigureTestingServices(IServiceCollection services)
{
ConfigureInMemoryDatabases(services);
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
services.AddScoped(typeof(IAsyncRepository<>), typeof(EfRepository<>));
services.Configure<CatalogSettings>(Configuration);
services.AddSingleton<IUriComposer>(new UriComposer(Configuration.Get<CatalogSettings>()));
services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
services.AddScoped<ITokenClaimsService, IdentityTokenClaimService>();
// Add memory cache services
services.AddMemoryCache();
// https://stackoverflow.com/questions/46938248/asp-net-core-2-0-combining-cookies-and-bearer-authorization-for-the-same-endpoin
var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY);
services.AddAuthentication(config =>
{
//config.DefaultScheme = "smart";
//config.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
//config.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
config.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
config.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
config.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
config.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddControllers();
services.AddAutoMapper(typeof(Startup).Assembly);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
c.EnableAnnotations();
c.SchemaFilter<CustomSchemaFilters>();
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n
Enter 'Bearer' [space] and then your token in the text input below.
\r\n\r\nExample: 'Bearer 12345abcdef'",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@@ -0,0 +1,16 @@
{
"ConnectionStrings": {
"CatalogConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;",
"IdentityConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;"
},
"CatalogBaseUrl": "",
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"System": "Warning"
},
"AllowedHosts": "*"
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using AutoMapper.Configuration.Annotations;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -9,6 +10,7 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
{ {
[AllowAnonymous] [AllowAnonymous]
[IgnoreAntiforgeryToken]
public class LogoutModel : PageModel public class LogoutModel : PageModel
{ {
private readonly SignInManager<ApplicationUser> _signInManager; private readonly SignInManager<ApplicationUser> _signInManager;

View File

@@ -2,6 +2,7 @@
namespace Microsoft.eShopWeb.Web.Controllers.Api namespace Microsoft.eShopWeb.Web.Controllers.Api
{ {
// No longer used - shown for reference only if using full controllers instead of Endpoints for APIs
[Route("api/[controller]/[action]")] [Route("api/[controller]/[action]")]
[ApiController] [ApiController]
public class BaseApiController : ControllerBase public class BaseApiController : ControllerBase

View File

@@ -1,21 +0,0 @@
using Microsoft.eShopWeb.Web.Services;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Controllers.Api
{
public class CatalogController : BaseApiController
{
private readonly ICatalogViewModelService _catalogViewModelService;
public CatalogController(ICatalogViewModelService catalogViewModelService) => _catalogViewModelService = catalogViewModelService;
[HttpGet]
public async Task<IActionResult> List(int? brandFilterApplied, int? typesFilterApplied, int? page)
{
var itemsPage = 10;
var catalogModel = await _catalogViewModelService.GetCatalogItems(page ?? 0, itemsPage, brandFilterApplied, typesFilterApplied);
return Ok(catalogModel);
}
}
}

View File

@@ -1,4 +1,5 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.Web.Features.MyOrders; using Microsoft.eShopWeb.Web.Features.MyOrders;

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;

View File

@@ -8,7 +8,10 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web.API; using Microsoft.eShopWeb.Infrastructure.Logging;
using Microsoft.eShopWeb.Infrastructure.Services;
using Microsoft.eShopWeb.Web.Interfaces;
using Microsoft.eShopWeb.Web.Services;
using Microsoft.eShopWeb.Web.Configuration; using Microsoft.eShopWeb.Web.Configuration;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -107,12 +110,6 @@ namespace Microsoft.eShopWeb.Web
}); });
services.AddControllersWithViews(); services.AddControllersWithViews();
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApi.Models.OpenApiInfo { Title = "My API", Version = "v1" });
c.EnableAnnotations();
c.SchemaFilter<CustomSchemaFilters>();
});
services.AddHealthChecks(); services.AddHealthChecks();
services.Configure<ServiceConfig>(config => services.Configure<ServiceConfig>(config =>
{ {
@@ -166,15 +163,6 @@ namespace Microsoft.eShopWeb.Web
app.UseCookiePolicy(); app.UseCookiePolicy();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {

View File

@@ -23,6 +23,7 @@
<PackageReference Include="Ardalis.ApiEndpoints" Version="1.0.0" /> <PackageReference Include="Ardalis.ApiEndpoints" Version="1.0.0" />
<PackageReference Include="Ardalis.ListStartupServices" Version="1.1.3" /> <PackageReference Include="Ardalis.ListStartupServices" Version="1.1.3" />
<PackageReference Include="Ardalis.Specification" Version="3.0.0" /> <PackageReference Include="Ardalis.Specification" Version="3.0.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="7.0.0" />
<PackageReference Include="MediatR" Version="8.0.1" /> <PackageReference Include="MediatR" Version="8.0.1" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="8.0.0" /> <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="8.0.0" />
@@ -30,17 +31,17 @@
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.6.0" /> <PackageReference Include="Microsoft.CodeAnalysis" Version="3.6.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.5" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.3" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.0" /> <PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="2.1.76" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="5.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.5" /> <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.5" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.5" /> <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.7.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="wwwroot\fonts\" /> <Folder Include="wwwroot\fonts\" />

View File

@@ -26,6 +26,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\ApplicationCore\ApplicationCore.csproj" /> <ProjectReference Include="..\..\src\ApplicationCore\ApplicationCore.csproj" />
<ProjectReference Include="..\..\src\PublicApi\PublicApi.csproj" />
<ProjectReference Include="..\..\src\Web\Web.csproj" /> <ProjectReference Include="..\..\src\Web\Web.csproj" />
</ItemGroup> </ItemGroup>
@@ -33,8 +34,4 @@
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" /> <Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Web\ApiEndpoints\" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,79 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.PublicApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace Microsoft.eShopWeb.FunctionalTests.PublicApi
{
public class ApiTestFixture : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
services.AddEntityFrameworkInMemoryDatabase();
// Create a new service provider.
var provider = services
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Add a database context (ApplicationDbContext) using an in-memory
// database for testing.
services.AddDbContext<CatalogContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(provider);
});
services.AddDbContext<AppIdentityDbContext>(options =>
{
options.UseInMemoryDatabase("Identity");
options.UseInternalServiceProvider(provider);
});
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database
// context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<CatalogContext>();
var loggerFactory = scopedServices.GetRequiredService<ILoggerFactory>();
var logger = scopedServices
.GetRequiredService<ILogger<ApiTestFixture>>();
// Ensure the database is created.
db.Database.EnsureCreated();
try
{
// Seed the database with test data.
CatalogContextSeed.SeedAsync(db, loggerFactory).Wait();
// seed sample user data
var userManager = scopedServices.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scopedServices.GetRequiredService<RoleManager<IdentityRole>>();
AppIdentityDbContextSeed.SeedAsync(userManager, roleManager).Wait();
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred seeding the " +
"database with test messages. Error: {ex.Message}");
}
}
});
}
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.eShopWeb.ApplicationCore.Constants;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace Microsoft.eShopWeb.FunctionalTests.Web.Api
{
public class ApiTokenHelper
{
public static string GetAdminUserToken()
{
string userName = "admin@microsoft.com";
string[] roles = { "Administrators" };
return CreateToken(userName, roles);
}
public static string GetNormalUserToken()
{
string userName = "demouser@microsoft.com";
string[] roles = { };
return CreateToken(userName, roles);
}
private static string CreateToken(string userName, string[] roles)
{
var claims = new List<Claim> { new Claim(ClaimTypes.Name, userName) };
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims.ToArray()),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
}

View File

@@ -0,0 +1,43 @@
using Microsoft.eShopWeb.ApplicationCore.Constants;
using Microsoft.eShopWeb.FunctionalTests.PublicApi;
using Microsoft.eShopWeb.PublicApi.AuthEndpoints;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
{
[Collection("Sequential")]
public class AuthenticateEndpoint : IClassFixture<ApiTestFixture>
{
JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
public AuthenticateEndpoint(ApiTestFixture factory)
{
Client = factory.CreateClient();
}
public HttpClient Client { get; }
[Theory]
[InlineData("demouser@microsoft.com", AuthorizationConstants.DEFAULT_PASSWORD, true)]
[InlineData("demouser@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 Client.PostAsync("api/authenticate", jsonContent);
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
var model = stringResponse.FromJson<AuthenticateResponse>();
Assert.Equal(expectedResult, model.Result);
}
}
}

View File

@@ -1,18 +1,16 @@
using Microsoft.eShopWeb.Web.ViewModels; using Microsoft.eShopWeb.FunctionalTests.PublicApi;
using Microsoft.eShopWeb.Web.ViewModels;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
{ {
[Collection("Sequential")] [Collection("Sequential")]
public class ApiCatalogControllerList : IClassFixture<WebTestFixture> public class ApiCatalogControllerList : IClassFixture<ApiTestFixture>
{ {
JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; public ApiCatalogControllerList(ApiTestFixture factory)
public ApiCatalogControllerList(WebTestFixture factory)
{ {
Client = factory.CreateClient(); Client = factory.CreateClient();
} }
@@ -22,7 +20,7 @@ namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
[Fact] [Fact]
public async Task ReturnsFirst10CatalogItems() public async Task ReturnsFirst10CatalogItems()
{ {
var response = await Client.GetAsync("/api/catalog/list"); var response = await Client.GetAsync("/api/catalog-items?pageSize=10");
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync(); var stringResponse = await response.Content.ReadAsStringAsync();
var model = stringResponse.FromJson<CatalogIndexViewModel>(); var model = stringResponse.FromJson<CatalogIndexViewModel>();
@@ -33,7 +31,7 @@ namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
[Fact] [Fact]
public async Task ReturnsLast2CatalogItemsGivenPageIndex1() public async Task ReturnsLast2CatalogItemsGivenPageIndex1()
{ {
var response = await Client.GetAsync("/api/catalog/list?page=1"); var response = await Client.GetAsync("/api/catalog-items?pageSize=10&pageIndex=1");
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync(); var stringResponse = await response.Content.ReadAsStringAsync();
var model = stringResponse.FromJson<CatalogIndexViewModel>(); var model = stringResponse.FromJson<CatalogIndexViewModel>();

View File

@@ -0,0 +1,78 @@
using Microsoft.eShopWeb.FunctionalTests.PublicApi;
using Microsoft.eShopWeb.FunctionalTests.Web.Api;
using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
{
[Collection("Sequential")]
public class CreateEndpoint : IClassFixture<ApiTestFixture>
{
JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
private int _testBrandId = 1;
private int _testTypeId = 2;
private string _testDescription = "test description";
private string _testName = "test name";
private string _testUri = "test uri";
private decimal _testPrice = 1.23m;
public CreateEndpoint(ApiTestFixture factory)
{
Client = factory.CreateClient();
}
public HttpClient Client { get; }
[Fact]
public async Task ReturnsNotAuthorizedGivenNormalUserToken()
{
var jsonContent = GetValidNewItemJson();
var token = ApiTokenHelper.GetNormalUserToken();
Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await Client.PostAsync("api/catalog-items", jsonContent);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task ReturnsSuccessGivenValidNewItemAndAdminUserToken()
{
var jsonContent = GetValidNewItemJson();
var adminToken = ApiTokenHelper.GetAdminUserToken();
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.Equal(_testBrandId, model.CatalogItem.CatalogBrandId);
Assert.Equal(_testTypeId, model.CatalogItem.CatalogTypeId);
Assert.Equal(_testDescription, model.CatalogItem.Description);
Assert.Equal(_testName, model.CatalogItem.Name);
Assert.Equal(_testUri, model.CatalogItem.PictureUri);
Assert.Equal(_testPrice, model.CatalogItem.Price);
}
private StringContent GetValidNewItemJson()
{
var request = new CreateCatalogItemRequest()
{
CatalogBrandId = _testBrandId,
CatalogTypeId = _testTypeId,
Description = _testDescription,
Name = _testName,
PictureUri = _testUri,
Price = _testPrice
};
var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
return jsonContent;
}
}
}

View File

@@ -0,0 +1,48 @@
using Microsoft.eShopWeb.FunctionalTests.PublicApi;
using Microsoft.eShopWeb.FunctionalTests.Web.Api;
using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
{
[Collection("Sequential")]
public class DeleteEndpoint : IClassFixture<ApiTestFixture>
{
JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
public DeleteEndpoint(ApiTestFixture factory)
{
Client = factory.CreateClient();
}
public HttpClient Client { get; }
[Fact]
public async Task ReturnsSuccessGivenValidIdAndAdminUserToken()
{
var adminToken = ApiTokenHelper.GetAdminUserToken();
Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
var response = await Client.DeleteAsync("api/catalog-items/12");
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
var model = stringResponse.FromJson<DeleteCatalogItemResponse>();
Assert.Equal("Deleted", model.Status);
}
[Fact]
public async Task ReturnsNotFoundGivenInvalidIdAndAdminUserToken()
{
var adminToken = ApiTokenHelper.GetAdminUserToken();
Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
var response = await Client.DeleteAsync("api/catalog-items/0");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
}

View File

@@ -0,0 +1,43 @@
using Microsoft.eShopWeb.FunctionalTests.PublicApi;
using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
{
[Collection("Sequential")]
public class GetByIdEndpoint : IClassFixture<ApiTestFixture>
{
JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
public GetByIdEndpoint(ApiTestFixture factory)
{
Client = factory.CreateClient();
}
public HttpClient Client { get; }
[Fact]
public async Task ReturnsItemGivenValidId()
{
var response = await Client.GetAsync("api/catalog-items/5");
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
var model = stringResponse.FromJson<GetByIdCatalogItemResponse>();
Assert.Equal(5, model.CatalogItem.Id);
Assert.Equal("Roslyn Red Sheet", model.CatalogItem.Name);
}
[Fact]
public async Task ReturnsNotFoundGivenInvalidId()
{
var response = await Client.GetAsync("api/catalog-items/0");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
}