Browse Source
* 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>main
committed by
GitHub
49 changed files with 1024 additions and 75 deletions
@ -0,0 +1,9 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Microsoft.eShopWeb.ApplicationCore.Interfaces |
|||
{ |
|||
public interface ITokenClaimsService |
|||
{ |
|||
Task<string> GetTokenAsync(string userName); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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,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,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 |
|||
{ |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
@ -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"] |
|||
@ -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>(); |
|||
} |
|||
} |
|||
} |
|||
@ -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>(); |
|||
}); |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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(); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
{ |
|||
"Logging": { |
|||
"LogLevel": { |
|||
"Default": "Information", |
|||
"Microsoft": "Warning", |
|||
"Microsoft.Hosting.Lifetime": "Information" |
|||
} |
|||
} |
|||
} |
|||
@ -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,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); |
|||
} |
|||
} |
|||
} |
|||
@ -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}"); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue