Add Blazor WebAssembly Admin Page (#426)

* Added Blazor Client
Configured PublicAPI CORS to allow traffic from client

* Make admin page home page; remove extra pages
Add CatalogType list endpoint

* Wired up Types and Brands in the API and the admin list page

* Adding a custom HttpClient to talk securely to API

* Ardalis/blazor (#419)

* Login added

* AuthService will handel http request secure and not secure.

* Logout added

* CatalogBrandService in it is own service

* Get token from localstorage when refresh.

* used GetAsync

* Fixed Login and Logout switch.

* CatalogItemService added

* CatalogTypeService added & Auth for CatalogType.
using not used removed.

* Made BlazorComponent and BlazorLayoutComponent for refresh.
Index now small enough to be in one file.

* Removed the service from program main and use lazy singleton.

* used OnInitialized

* Refactoring and detecting login status in login.razor

* Refactoring login to redirect if user is already logged in

* Blazor login with MVC (#420)

* Blazor login with MVC

* return back the PasswordSignInAsync in Login page

* CRUD added (#422)

* CRUD added

* Unit Test changed to meet new redirect /admin

* CreateCatalogItemRequest added.

* Action caption added.

* Validation added for name and price.

* Updated port of api
Redirect to returnUrl from login

* Add username to /admin; link to my profile

* Working on authorization of /admin

* Working on custom auth locking down /admin page

* Microsoft authorize working.Login.razor removed.Login from SignInMana… (#425)

* Microsoft authorize working.Login.razor removed.Login from SignInManager and create token from it.unit test fixed.

* GetTokenFromController function used in CustomAuthStateProvider

* Cleaned up button styles
Refactored to use codebehind for List component
Updated Not Authorized view

Co-authored-by: Shady Nagy <shadynagi@gmail.com>
This commit is contained in:
Steve Smith
2020-07-24 12:36:47 -04:00
committed by GitHub
parent 4253660bc3
commit 8d3ac693d4
86 changed files with 3268 additions and 82 deletions

View File

@@ -11,8 +11,11 @@ namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints
public AuthenticateResponse()
{
}
public bool Result { get; set; }
public string Token { get; set; }
public bool Result { get; set; } = false;
public string Token { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public bool IsLockedOut { get; set; } = false;
public bool IsNotAllowed { get; set; } = false;
public bool RequiresTwoFactor { get; set; } = false;
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints
{
public class ClaimValue
{
public ClaimValue()
{
}
public ClaimValue(string type, string value)
{
Type = type;
Value = value;
}
public string Type { get; set; }
public string Value { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints
{
public class UserInfo
{
public static readonly UserInfo Anonymous = new UserInfo();
public bool IsAuthenticated { get; set; }
public string NameClaimType { get; set; }
public string RoleClaimType { get; set; }
public IEnumerable<ClaimValue> Claims { get; set; }
}
}

View File

@@ -38,10 +38,16 @@ namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints
{
var response = new AuthenticateResponse(request.CorrelationId());
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
//var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
var result = await _signInManager.PasswordSignInAsync(request.Username, request.Password, false, true);
response.Result = result.Succeeded;
response.IsLockedOut = result.IsLockedOut;
response.IsNotAllowed = result.IsNotAllowed;
response.RequiresTwoFactor = result.RequiresTwoFactor;
response.Username = request.Username;
response.Token = await _tokenClaimsService.GetTokenAsync(request.Username);
return response;

View File

@@ -0,0 +1,8 @@
namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints
{
public class CatalogBrandDto
{
public int Id { get; set; }
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints
{
public class ListCatalogBrandsResponse : BaseResponse
{
public ListCatalogBrandsResponse(Guid correlationId) : base(correlationId)
{
}
public ListCatalogBrandsResponse()
{
}
public List<CatalogBrandDto> CatalogBrands { get; set; } = new List<CatalogBrandDto>();
}
}

View File

@@ -0,0 +1,42 @@
using Ardalis.ApiEndpoints;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Swashbuckle.AspNetCore.Annotations;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints
{
public class List : BaseAsyncEndpoint<ListCatalogBrandsResponse>
{
private readonly IAsyncRepository<CatalogBrand> _catalogBrandRepository;
private readonly IMapper _mapper;
public List(IAsyncRepository<CatalogBrand> catalogBrandRepository,
IMapper mapper)
{
_catalogBrandRepository = catalogBrandRepository;
_mapper = mapper;
}
[HttpGet("api/catalog-brands")]
[SwaggerOperation(
Summary = "List Catalog Brands",
Description = "List Catalog Brands",
OperationId = "catalog-brands.List",
Tags = new[] { "CatalogBrandEndpoints" })
]
public override async Task<ActionResult<ListCatalogBrandsResponse>> HandleAsync()
{
var response = new ListCatalogBrandsResponse();
var items = await _catalogBrandRepository.ListAllAsync();
response.CatalogBrands.AddRange(items.Select(_mapper.Map<CatalogBrandDto>));
return Ok(response);
}
}
}

View File

@@ -15,10 +15,12 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
public class Create : BaseAsyncEndpoint<CreateCatalogItemRequest, CreateCatalogItemResponse>
{
private readonly IAsyncRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;
public Create(IAsyncRepository<CatalogItem> itemRepository)
public Create(IAsyncRepository<CatalogItem> itemRepository, IUriComposer uriComposer)
{
_itemRepository = itemRepository;
_uriComposer = uriComposer;
}
[HttpPost("api/catalog-items")]
@@ -43,7 +45,7 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
CatalogTypeId = newItem.CatalogTypeId,
Description = newItem.Description,
Name = newItem.Name,
PictureUri = newItem.PictureUri,
PictureUri = _uriComposer.ComposePicUri(newItem.PictureUri),
Price = newItem.Price
};
response.CatalogItem = dto;

View File

@@ -10,10 +10,12 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
public class GetById : BaseAsyncEndpoint<GetByIdCatalogItemRequest, GetByIdCatalogItemResponse>
{
private readonly IAsyncRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;
public GetById(IAsyncRepository<CatalogItem> itemRepository)
public GetById(IAsyncRepository<CatalogItem> itemRepository, IUriComposer uriComposer)
{
_itemRepository = itemRepository;
_uriComposer = uriComposer;
}
[HttpGet("api/catalog-items/{CatalogItemId}")]
@@ -23,7 +25,7 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
OperationId = "catalog-items.GetById",
Tags = new[] { "CatalogItemEndpoints" })
]
public override async Task<ActionResult<GetByIdCatalogItemResponse>> HandleAsync([FromRoute]GetByIdCatalogItemRequest request)
public override async Task<ActionResult<GetByIdCatalogItemResponse>> HandleAsync([FromRoute] GetByIdCatalogItemRequest request)
{
var response = new GetByIdCatalogItemResponse(request.CorrelationId());
@@ -37,7 +39,7 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
CatalogTypeId = item.CatalogTypeId,
Description = item.Description,
Name = item.Name,
PictureUri = item.PictureUri,
PictureUri = _uriComposer.ComposePicUri(item.PictureUri),
Price = item.Price
};
return Ok(response);

View File

@@ -16,10 +16,13 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
public class Update : BaseAsyncEndpoint<UpdateCatalogItemRequest, UpdateCatalogItemResponse>
{
private readonly IAsyncRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;
public Update(IAsyncRepository<CatalogItem> itemRepository)
public Update(IAsyncRepository<CatalogItem> itemRepository, IUriComposer uriComposer)
{
_itemRepository = itemRepository;
_uriComposer = uriComposer;
}
[HttpPut("api/catalog-items")]
@@ -48,7 +51,7 @@ namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
CatalogTypeId = existingItem.CatalogTypeId,
Description = existingItem.Description,
Name = existingItem.Name,
PictureUri = existingItem.PictureUri,
PictureUri = _uriComposer.ComposePicUri(existingItem.PictureUri),
Price = existingItem.Price
};
response.CatalogItem = dto;

View File

@@ -0,0 +1,8 @@
namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints
{
public class CatalogTypeDto
{
public int Id { get; set; }
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints
{
public class ListCatalogTypesResponse : BaseResponse
{
public ListCatalogTypesResponse(Guid correlationId) : base(correlationId)
{
}
public ListCatalogTypesResponse()
{
}
public List<CatalogTypeDto> CatalogTypes { get; set; } = new List<CatalogTypeDto>();
}
}

View File

@@ -0,0 +1,42 @@
using Ardalis.ApiEndpoints;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Swashbuckle.AspNetCore.Annotations;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints
{
public class List : BaseAsyncEndpoint<ListCatalogTypesResponse>
{
private readonly IAsyncRepository<CatalogType> _catalogTypeRepository;
private readonly IMapper _mapper;
public List(IAsyncRepository<CatalogType> catalogTypeRepository,
IMapper mapper)
{
_catalogTypeRepository = catalogTypeRepository;
_mapper = mapper;
}
[HttpGet("api/catalog-types")]
[SwaggerOperation(
Summary = "List Catalog Types",
Description = "List Catalog Types",
OperationId = "catalog-types.List",
Tags = new[] { "CatalogTypeEndpoints" })
]
public override async Task<ActionResult<ListCatalogTypesResponse>> HandleAsync()
{
var response = new ListCatalogTypesResponse();
var items = await _catalogTypeRepository.ListAllAsync();
response.CatalogTypes.AddRange(items.Select(_mapper.Map<CatalogTypeDto>));
return Ok(response);
}
}
}

View File

@@ -1,6 +1,8 @@
using AutoMapper;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints;
using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
using Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints;
namespace Microsoft.eShopWeb.PublicApi
{
@@ -9,6 +11,10 @@ namespace Microsoft.eShopWeb.PublicApi
public MappingProfile()
{
CreateMap<CatalogItem, CatalogItemDto>();
CreateMap<CatalogType, CatalogTypeDto>()
.ForMember(dto => dto.Name, options => options.MapFrom(src => src.Type));
CreateMap<CatalogBrand, CatalogBrandDto>()
.ForMember(dto => dto.Name, options => options.MapFrom(src => src.Brand));
}
}
}

View File

@@ -27,6 +27,7 @@ namespace Microsoft.eShopWeb.PublicApi
{
public class Startup
{
private const string CORS_POLICY = "CorsPolicy";
public Startup(IConfiguration configuration)
{
Configuration = configuration;
@@ -120,7 +121,19 @@ namespace Microsoft.eShopWeb.PublicApi
});
services.AddCors(options =>
{
options.AddPolicy(name: CORS_POLICY,
builder =>
{
builder.WithOrigins("http://localhost:44319",
"https://localhost:44319",
"http://localhost:44315",
"https://localhost:44315");
builder.AllowAnyMethod();
builder.AllowAnyHeader();
});
});
services.AddControllers();
services.AddMediatR(typeof(CatalogItem).Assembly);
@@ -175,6 +188,8 @@ namespace Microsoft.eShopWeb.PublicApi
app.UseRouting();
app.UseCors(CORS_POLICY);
app.UseAuthorization();
// Enable middleware to serve generated Swagger as a JSON endpoint.