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:
@@ -30,12 +30,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
EndProject
|
||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{1FCBE191-34FE-4B2E-8915-CA81553958AD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicApi", "src\PublicApi\PublicApi.csproj", "{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
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.Build.0 = Debug|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}.Release|Any CPU.ActiveCfg = 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.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.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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{227CF035-29B0-448D-97E4-944F9EA850E5} = {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}
|
||||
{EF6877E6-59CB-43A7-8C2C-E70DD70CC5B6} = {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}
|
||||
{227CF035-29B0-448D-97E4-944F9EA850E5} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
|
||||
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {49813262-5DA3-4D61-ABD3-493C74CE8C2B}
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
public const string ADMINISTRATORS = "Administrators";
|
||||
}
|
||||
|
||||
// TODO: Don't use this in production
|
||||
public const string DEFAULT_PASSWORD = "Pass@word1";
|
||||
|
||||
// TODO: Change this to an environment variable
|
||||
public const string JWT_SECRET_KEY = "SecretKeyOfDoomThatMustBeAMinimumNumberOfBytes";
|
||||
}
|
||||
}
|
||||
|
||||
9
src/ApplicationCore/Interfaces/ITokenClaimsService.cs
Normal file
9
src/ApplicationCore/Interfaces/ITokenClaimsService.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.eShopWeb.ApplicationCore.Interfaces
|
||||
{
|
||||
public interface ITokenClaimsService
|
||||
{
|
||||
Task<string> GetTokenAsync(string userName);
|
||||
}
|
||||
}
|
||||
46
src/Infrastructure/Identity/IdentityTokenClaimService.cs
Normal file
46
src/Infrastructure/Identity/IdentityTokenClaimService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" 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="System.IdentityModel.Tokens.Jwt" Version="6.7.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationCore\ApplicationCore.csproj" />
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints
|
||||
{
|
||||
public class AuthenticateRequest : BaseRequest
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
50
src/PublicApi/AuthEndpoints/Authenticate.cs
Normal file
50
src/PublicApi/AuthEndpoints/Authenticate.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Microsoft.eShopWeb.Web.API
|
||||
namespace Microsoft.eShopWeb.PublicApi
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class used by API requests
|
||||
@@ -13,8 +13,4 @@ namespace Microsoft.eShopWeb.Web.API
|
||||
protected Guid _correlationId = Guid.NewGuid();
|
||||
public Guid CorrelationId() => _correlationId;
|
||||
}
|
||||
|
||||
public abstract class BaseRequest : BaseMessage
|
||||
{
|
||||
}
|
||||
}
|
||||
9
src/PublicApi/BaseRequest.cs
Normal file
9
src/PublicApi/BaseRequest.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Microsoft.eShopWeb.PublicApi
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class used by API requests
|
||||
/// </summary>
|
||||
public abstract class BaseRequest : BaseMessage
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Microsoft.eShopWeb.Web.API
|
||||
namespace Microsoft.eShopWeb.PublicApi
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class used by API responses
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
||||
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||
{
|
||||
public class CatalogItemDto
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
||||
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||
{
|
||||
public class CreateCatalogItemRequest : BaseRequest
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
||||
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||
{
|
||||
public class CreateCatalogItemResponse : BaseResponse
|
||||
{
|
||||
@@ -8,6 +8,10 @@ namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
||||
{
|
||||
}
|
||||
|
||||
public CreateCatalogItemResponse()
|
||||
{
|
||||
}
|
||||
|
||||
public CatalogItemDto CatalogItem { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
using Ardalis.ApiEndpoints;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.eShopWeb.ApplicationCore.Constants;
|
||||
using Microsoft.eShopWeb.ApplicationCore.Entities;
|
||||
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
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>
|
||||
{
|
||||
private readonly IAsyncRepository<CatalogItem> _itemRepository;
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
||||
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||
{
|
||||
public class DeleteCatalogItemRequest : BaseRequest
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
||||
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||
{
|
||||
public class DeleteCatalogItemResponse : BaseResponse
|
||||
{
|
||||
@@ -8,6 +8,10 @@ namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
||||
{
|
||||
}
|
||||
|
||||
public DeleteCatalogItemResponse()
|
||||
{
|
||||
}
|
||||
|
||||
public string Status { get; set; } = "Deleted";
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
using Ardalis.ApiEndpoints;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.eShopWeb.ApplicationCore.Constants;
|
||||
using Microsoft.eShopWeb.ApplicationCore.Entities;
|
||||
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
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>
|
||||
{
|
||||
private readonly IAsyncRepository<CatalogItem> _itemRepository;
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
||||
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||
{
|
||||
public class GetByIdCatalogItemRequest : BaseRequest
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
||||
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||
{
|
||||
public class GetByIdCatalogItemResponse : BaseResponse
|
||||
{
|
||||
@@ -8,6 +8,10 @@ namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
||||
{
|
||||
}
|
||||
|
||||
public GetByIdCatalogItemResponse()
|
||||
{
|
||||
}
|
||||
|
||||
public CatalogItemDto CatalogItem { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
||||
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||
{
|
||||
public class GetById : BaseAsyncEndpoint<GetByIdCatalogItemRequest, GetByIdCatalogItemResponse>
|
||||
{
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
61
src/PublicApi/CatalogItemEndpoints/ListPaged.cs
Normal file
61
src/PublicApi/CatalogItemEndpoints/ListPaged.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Microsoft.eShopWeb.Web.API
|
||||
namespace Microsoft.eShopWeb.PublicApi
|
||||
{
|
||||
public class CustomSchemaFilters : ISchemaFilter
|
||||
{
|
||||
22
src/PublicApi/Dockerfile
Normal file
22
src/PublicApi/Dockerfile
Normal 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"]
|
||||
14
src/PublicApi/MappingProfile.cs
Normal file
14
src/PublicApi/MappingProfile.cs
Normal 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
51
src/PublicApi/Program.cs
Normal 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>();
|
||||
});
|
||||
}
|
||||
}
|
||||
37
src/PublicApi/Properties/launchSettings.json
Normal file
37
src/PublicApi/Properties/launchSettings.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/PublicApi/PublicApi.csproj
Normal file
39
src/PublicApi/PublicApi.csproj
Normal 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
193
src/PublicApi/Startup.cs
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/PublicApi/appsettings.Development.json
Normal file
9
src/PublicApi/appsettings.Development.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/PublicApi/appsettings.json
Normal file
16
src/PublicApi/appsettings.json
Normal 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": "*"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using AutoMapper.Configuration.Annotations;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -9,6 +10,7 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public class LogoutModel : PageModel
|
||||
{
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
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]")]
|
||||
[ApiController]
|
||||
public class BaseApiController : ControllerBase
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.eShopWeb.Web.Features.MyOrders;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -8,7 +8,10 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.eShopWeb.Infrastructure.Data;
|
||||
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.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -107,12 +110,6 @@ namespace Microsoft.eShopWeb.Web
|
||||
});
|
||||
services.AddControllersWithViews();
|
||||
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.Configure<ServiceConfig>(config =>
|
||||
{
|
||||
@@ -166,15 +163,6 @@ namespace Microsoft.eShopWeb.Web
|
||||
app.UseCookiePolicy();
|
||||
app.UseAuthentication();
|
||||
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 =>
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<PackageReference Include="Ardalis.ApiEndpoints" Version="1.0.0" />
|
||||
<PackageReference Include="Ardalis.ListStartupServices" Version="1.1.3" />
|
||||
<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.Extensions.Microsoft.DependencyInjection" Version="8.0.0" />
|
||||
@@ -30,17 +31,17 @@
|
||||
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.6.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.5" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.3" />
|
||||
<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.Web.LibraryManager.Build" Version="2.1.76" />
|
||||
<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.SqlServer" Version="3.1.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.7.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\fonts\" />
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ApplicationCore\ApplicationCore.csproj" />
|
||||
<ProjectReference Include="..\..\src\PublicApi\PublicApi.csproj" />
|
||||
<ProjectReference Include="..\..\src\Web\Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -33,8 +34,4 @@
|
||||
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Web\ApiEndpoints\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
79
tests/FunctionalTests/PublicApi/ApiTestFixture.cs
Normal file
79
tests/FunctionalTests/PublicApi/ApiTestFixture.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
51
tests/FunctionalTests/PublicApi/ApiTokenHelper.cs
Normal file
51
tests/FunctionalTests/PublicApi/ApiTokenHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
using Microsoft.eShopWeb.Web.ViewModels;
|
||||
using Microsoft.eShopWeb.FunctionalTests.PublicApi;
|
||||
using Microsoft.eShopWeb.Web.ViewModels;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
|
||||
{
|
||||
[Collection("Sequential")]
|
||||
public class ApiCatalogControllerList : IClassFixture<WebTestFixture>
|
||||
public class ApiCatalogControllerList : IClassFixture<ApiTestFixture>
|
||||
{
|
||||
JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
|
||||
public ApiCatalogControllerList(WebTestFixture factory)
|
||||
public ApiCatalogControllerList(ApiTestFixture factory)
|
||||
{
|
||||
Client = factory.CreateClient();
|
||||
}
|
||||
@@ -22,7 +20,7 @@ namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
|
||||
[Fact]
|
||||
public async Task ReturnsFirst10CatalogItems()
|
||||
{
|
||||
var response = await Client.GetAsync("/api/catalog/list");
|
||||
var response = await Client.GetAsync("/api/catalog-items?pageSize=10");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var stringResponse = await response.Content.ReadAsStringAsync();
|
||||
var model = stringResponse.FromJson<CatalogIndexViewModel>();
|
||||
@@ -33,7 +31,7 @@ namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
|
||||
[Fact]
|
||||
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();
|
||||
var stringResponse = await response.Content.ReadAsStringAsync();
|
||||
var model = stringResponse.FromJson<CatalogIndexViewModel>();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user