Updating Blazor Admin (#442)
* Updating Blazor services * Adding Settings and Refactoring Services * WIP - Fighting with DI * Configuring dependencies in both Web Startup and BlazorAdmin Program.cs has them working again. * Everything works; need to optimize calls to ListBrands * LocalStorageBrandService decorator working * Added cache duration of 1 minute * Refactoring to reduce token storage Fixed issue with dropdowns binding to int * Remove token stuff from login; moved to CustomAuthStateProvider * Migrated CatalogTypes to separate service Implemented cache decorator * Ardalis/blazor refactor (#440) * 1. Migrate CatalogItemServices -> CatalogItemService. 3. Add caching to CatalogItemService. * change to $"Loading {key} from local storage" ? * docker settings added. (#441) * docker settings added. * InDocker Removed * InDocker removed from web startup. * removed unused using * no reload list if close without save * startup patch for localhost * file name fixed * removed docker from launchSettings. * Configure logging via appsettings Co-authored-by: Shady Nagy <info@shadynagy.com>
This commit is contained in:
@@ -2,9 +2,8 @@ version: '3.4'
|
||||
services:
|
||||
eshopwebmvc:
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- ASPNETCORE_ENVIRONMENT=Docker
|
||||
- ASPNETCORE_URLS=http://+:80
|
||||
- DOTNET_RUNNING_IN_CONTAINER=true
|
||||
ports:
|
||||
- "5106:80"
|
||||
volumes:
|
||||
@@ -12,9 +11,8 @@ services:
|
||||
- ~/.microsoft/usersecrets:/root/.microsoft/usersecrets:ro
|
||||
eshoppublicapi:
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- ASPNETCORE_ENVIRONMENT=Docker
|
||||
- ASPNETCORE_URLS=http://+:80
|
||||
- DOTNET_RUNNING_IN_CONTAINER=true
|
||||
ports:
|
||||
- "5200:80"
|
||||
volumes:
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="3.1.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="3.1.6" />
|
||||
<PackageReference Include="System.Net.Http.Json" Version="3.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
using System;
|
||||
using BlazorAdmin.Services;
|
||||
using BlazorAdmin.Services;
|
||||
using BlazorShared.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BlazorShared.Authorization;
|
||||
|
||||
namespace BlazorAdmin
|
||||
{
|
||||
public class CustomAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
// TODO: Get Default Cache Duration from Config
|
||||
private static readonly TimeSpan UserCacheRefreshInterval = TimeSpan.FromSeconds(60);
|
||||
|
||||
private readonly AuthService _authService;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<CustomAuthStateProvider> _logger;
|
||||
|
||||
private DateTimeOffset _userLastCheck = DateTimeOffset.FromUnixTimeSeconds(0);
|
||||
private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
|
||||
public CustomAuthStateProvider(AuthService authService, ILogger<CustomAuthStateProvider> logger)
|
||||
public CustomAuthStateProvider(AuthService authService,
|
||||
HttpClient httpClient,
|
||||
ILogger<CustomAuthStateProvider> logger)
|
||||
{
|
||||
_authService = authService;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync() =>
|
||||
new AuthenticationState(await GetUser(useCache: true));
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
return new AuthenticationState(await GetUser(useCache: true));
|
||||
}
|
||||
|
||||
private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = false)
|
||||
{
|
||||
@@ -47,7 +57,8 @@ namespace BlazorAdmin
|
||||
|
||||
try
|
||||
{
|
||||
user = await _authService.GetTokenFromController();
|
||||
_logger.LogInformation("Fetching user details from web api.");
|
||||
user = await _httpClient.GetFromJsonAsync<UserInfo>("User");
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
@@ -56,7 +67,7 @@ namespace BlazorAdmin
|
||||
|
||||
if (user == null || !user.IsAuthenticated)
|
||||
{
|
||||
return new ClaimsPrincipal(new ClaimsIdentity());
|
||||
return null;
|
||||
}
|
||||
|
||||
var identity = new ClaimsIdentity(
|
||||
@@ -72,6 +83,8 @@ namespace BlazorAdmin
|
||||
}
|
||||
}
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", user.Token);
|
||||
|
||||
return new ClaimsPrincipal(identity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@inject ILogger<Create> Logger
|
||||
@inject AuthService Auth
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject ICatalogItemService CatalogItemService
|
||||
|
||||
@inherits BlazorAdmin.Helpers.BlazorComponent
|
||||
|
||||
@@ -130,7 +131,7 @@
|
||||
public IEnumerable<CatalogType> Types { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> OnCloseClick { get; set; }
|
||||
public EventCallback<string> OnSaveClick { get; set; }
|
||||
|
||||
private string LoadPicture => string.IsNullOrEmpty(_item.PictureBase64) ? string.Empty : $"data:image/png;base64, {_item.PictureBase64}";
|
||||
private bool HasPicture => !string.IsNullOrEmpty(_item.PictureBase64);
|
||||
@@ -142,8 +143,8 @@
|
||||
|
||||
private async Task CreateClick()
|
||||
{
|
||||
await new BlazorAdmin.Services.CatalogItemServices.Create(Auth).HandleAsync(_item);
|
||||
await OnCloseClick.InvokeAsync(null);
|
||||
await CatalogItemService.Create(_item);
|
||||
await OnSaveClick.InvokeAsync(null);
|
||||
await Close();
|
||||
}
|
||||
|
||||
@@ -172,7 +173,6 @@
|
||||
_modalDisplay = "none";
|
||||
_modalClass = "";
|
||||
_showCreateModal = false;
|
||||
await OnCloseClick.InvokeAsync(null);
|
||||
}
|
||||
|
||||
private async Task AddFile(IFileListEntry[] files)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@inject ILogger<Delete> Logger
|
||||
@inject AuthService Auth
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject ICatalogItemService CatalogItemService
|
||||
|
||||
@inherits BlazorAdmin.Helpers.BlazorComponent
|
||||
|
||||
@@ -50,7 +51,7 @@
|
||||
</dt>
|
||||
|
||||
<dd>
|
||||
@Services.CatalogBrandServices.List.GetBrandName(Brands, _item.CatalogBrandId)
|
||||
@_item.CatalogBrand
|
||||
</dd>
|
||||
|
||||
<dt>
|
||||
@@ -58,7 +59,7 @@
|
||||
</dt>
|
||||
|
||||
<dd>
|
||||
@Services.CatalogTypeServices.List.GetTypeName(Types, _item.CatalogTypeId)
|
||||
@_item.CatalogType
|
||||
</dd>
|
||||
<dt>
|
||||
Price
|
||||
@@ -97,7 +98,7 @@
|
||||
public IEnumerable<CatalogType> Types { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> OnCloseClick { get; set; }
|
||||
public EventCallback<string> OnSaveClick { get; set; }
|
||||
|
||||
private bool HasPicture => !string.IsNullOrEmpty(_item.PictureUri);
|
||||
private string _modalDisplay = "none;";
|
||||
@@ -109,9 +110,9 @@
|
||||
{
|
||||
// TODO: Add some kind of "are you sure" check before this
|
||||
|
||||
await new BlazorAdmin.Services.CatalogItemServices.Delete(Auth).HandleAsync(id);
|
||||
await CatalogItemService.Delete(id);
|
||||
|
||||
await OnCloseClick.InvokeAsync(null);
|
||||
await OnSaveClick.InvokeAsync(null);
|
||||
await Close();
|
||||
}
|
||||
|
||||
@@ -121,7 +122,7 @@
|
||||
|
||||
await new Css(JSRuntime).HideBodyOverflow();
|
||||
|
||||
_item = await new GetById(Auth).HandleAsync(id);
|
||||
_item = await CatalogItemService.GetById(id);
|
||||
|
||||
_modalDisplay = "block;";
|
||||
_modalClass = "Show";
|
||||
@@ -136,6 +137,5 @@
|
||||
_modalDisplay = "none";
|
||||
_modalClass = "";
|
||||
_showDeleteModal = false;
|
||||
await OnCloseClick.InvokeAsync(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@inject ILogger<Details> Logger
|
||||
@inject AuthService Auth
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject ICatalogItemService CatalogItemService
|
||||
|
||||
@inherits BlazorAdmin.Helpers.BlazorComponent
|
||||
|
||||
@@ -53,7 +54,7 @@
|
||||
</dt>
|
||||
|
||||
<dd>
|
||||
@Services.CatalogBrandServices.List.GetBrandName(Brands, _item.CatalogBrandId)
|
||||
@_item.CatalogBrand
|
||||
</dd>
|
||||
|
||||
<dt>
|
||||
@@ -61,7 +62,7 @@
|
||||
</dt>
|
||||
|
||||
<dd>
|
||||
@Services.CatalogTypeServices.List.GetTypeName(Types, _item.CatalogTypeId)
|
||||
@_item.CatalogType
|
||||
</dd>
|
||||
<dt>
|
||||
Price
|
||||
@@ -108,10 +109,10 @@
|
||||
private bool _showDetailsModal = false;
|
||||
private CatalogItem _item = new CatalogItem();
|
||||
|
||||
public void EditClick()
|
||||
public async Task EditClick()
|
||||
{
|
||||
OnEditClick.InvokeAsync(_item.Id);
|
||||
Close();
|
||||
await OnEditClick.InvokeAsync(_item.Id);
|
||||
await Close();
|
||||
}
|
||||
|
||||
public async Task Open(int id)
|
||||
@@ -121,7 +122,7 @@
|
||||
|
||||
await new Css(JSRuntime).HideBodyOverflow();
|
||||
|
||||
_item = await new GetById(Auth).HandleAsync(id);
|
||||
_item = await CatalogItemService.GetById(id);
|
||||
|
||||
_modalDisplay = "block;";
|
||||
_modalClass = "Show";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@inject ILogger<Edit> Logger
|
||||
@inject AuthService Auth
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject ICatalogItemService CatalogItemService
|
||||
|
||||
@inherits BlazorAdmin.Helpers.BlazorComponent
|
||||
|
||||
@@ -54,12 +55,12 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-6">Brand</label>
|
||||
<div class="col-md-12">
|
||||
<InputSelect @bind-Value="_item.CatalogBrandId" class="form-control">
|
||||
<CustomInputSelect @bind-Value="_item.CatalogBrandId" class="form-control">
|
||||
@foreach (var brand in Brands)
|
||||
{
|
||||
<option value="@brand.Id">@brand.Name</option>
|
||||
<option value="@brand.Id.ToString()">@brand.Name</option>
|
||||
}
|
||||
</InputSelect>
|
||||
</CustomInputSelect>
|
||||
<ValidationMessage For="(() => _item.CatalogBrandId)" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,12 +68,12 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-6">Type</label>
|
||||
<div class="col-md-12">
|
||||
<InputSelect @bind-Value="_item.CatalogTypeId" class="form-control">
|
||||
<CustomInputSelect @bind-Value="_item.CatalogTypeId" class="form-control">
|
||||
@foreach (var type in Types)
|
||||
{
|
||||
<option value="@type.Id">@type.Name</option>
|
||||
}
|
||||
</InputSelect>
|
||||
</CustomInputSelect>
|
||||
<ValidationMessage For="(() => _item.CatalogTypeId)" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,7 +131,7 @@
|
||||
public IEnumerable<CatalogType> Types { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> OnCloseClick { get; set; }
|
||||
public EventCallback<string> OnSaveClick { get; set; }
|
||||
|
||||
private string LoadPicture => string.IsNullOrEmpty(_item.PictureBase64) ? string.IsNullOrEmpty(_item.PictureUri) ? string.Empty : $"{_item.PictureUri}" : $"data:image/png;base64, {_item.PictureBase64}";
|
||||
private bool HasPicture => !(string.IsNullOrEmpty(_item.PictureBase64) && string.IsNullOrEmpty(_item.PictureUri));
|
||||
@@ -142,7 +143,8 @@
|
||||
|
||||
private async Task SaveClick()
|
||||
{
|
||||
await new BlazorAdmin.Services.CatalogItemServices.Edit(Auth).HandleAsync(_item);
|
||||
await CatalogItemService.Edit(_item);
|
||||
await OnSaveClick.InvokeAsync(null);
|
||||
await Close();
|
||||
}
|
||||
|
||||
@@ -152,7 +154,7 @@
|
||||
|
||||
await new Css(JSRuntime).HideBodyOverflow();
|
||||
|
||||
_item = await new GetById(Auth).HandleAsync(id);
|
||||
_item = await CatalogItemService.GetById(id);
|
||||
|
||||
_modalDisplay = "block;";
|
||||
_modalClass = "Show";
|
||||
@@ -168,7 +170,6 @@
|
||||
_modalDisplay = "none";
|
||||
_modalClass = "";
|
||||
_showEditModal = false;
|
||||
await OnCloseClick.InvokeAsync(null);
|
||||
}
|
||||
|
||||
private async Task ChangeFile(IFileListEntry[] files)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
@page "/admin"
|
||||
@attribute [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)]
|
||||
@inject AuthService Auth
|
||||
@inject BlazorAdmin.Services.CatalogItemServices.ListPaged CatalogItemListPaged
|
||||
@inject BlazorAdmin.Services.CatalogTypeServices.List TypeList
|
||||
@inject BlazorAdmin.Services.CatalogBrandServices.List BrandList
|
||||
@inherits BlazorAdmin.Helpers.BlazorComponent
|
||||
@namespace BlazorAdmin.Pages.CatalogItemPage
|
||||
|
||||
@@ -42,8 +39,8 @@ else
|
||||
<td>
|
||||
<img class="img-thumbnail" src="@($"{item.PictureUri}")">
|
||||
</td>
|
||||
<td>@Services.CatalogTypeServices.List.GetTypeName(catalogTypes, item.CatalogTypeId)</td>
|
||||
<td>@Services.CatalogBrandServices.List.GetBrandName(catalogBrands, item.CatalogBrandId)</td>
|
||||
<td>@item.CatalogType</td>
|
||||
<td>@item.CatalogBrand</td>
|
||||
<td>@item.Id</td>
|
||||
<td>@item.Name</td>
|
||||
<td>@item.Description</td>
|
||||
@@ -63,7 +60,7 @@ else
|
||||
</table>
|
||||
|
||||
<Details Brands="@catalogBrands" Types="@catalogTypes" OnEditClick="EditClick" @ref="DetailsComponent"></Details>
|
||||
<Edit Brands="@catalogBrands" Types="@catalogTypes" OnCloseClick="ReloadCatalogItems" @ref="EditComponent"></Edit>
|
||||
<Create Brands="@catalogBrands" Types="@catalogTypes" OnCloseClick="ReloadCatalogItems" @ref="CreateComponent"></Create>
|
||||
<Delete Brands="@catalogBrands" Types="@catalogTypes" OnCloseClick="ReloadCatalogItems" @ref="DeleteComponent"></Delete>
|
||||
<Edit Brands="@catalogBrands" Types="@catalogTypes" OnSaveClick="ReloadCatalogItems" @ref="EditComponent"></Edit>
|
||||
<Create Brands="@catalogBrands" Types="@catalogTypes" OnSaveClick="ReloadCatalogItems" @ref="CreateComponent"></Create>
|
||||
<Delete Brands="@catalogBrands" Types="@catalogTypes" OnSaveClick="ReloadCatalogItems" @ref="DeleteComponent"></Delete>
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
using BlazorAdmin.Helpers;
|
||||
using BlazorAdmin.Services.CatalogBrandServices;
|
||||
using BlazorAdmin.Services.CatalogItemServices;
|
||||
using BlazorAdmin.Services.CatalogTypeServices;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BlazorShared.Interfaces;
|
||||
using BlazorShared.Models;
|
||||
|
||||
namespace BlazorAdmin.Pages.CatalogItemPage
|
||||
{
|
||||
public partial class List : BlazorComponent
|
||||
{
|
||||
[Microsoft.AspNetCore.Components.Inject]
|
||||
public ICatalogItemService CatalogItemService { get; set; }
|
||||
|
||||
[Microsoft.AspNetCore.Components.Inject]
|
||||
public ICatalogBrandService CatalogBrandService { get; set; }
|
||||
|
||||
[Microsoft.AspNetCore.Components.Inject]
|
||||
public ICatalogTypeService CatalogTypeService { get; set; }
|
||||
|
||||
private List<CatalogItem> catalogItems = new List<CatalogItem>();
|
||||
private List<CatalogType> catalogTypes = new List<CatalogType>();
|
||||
private List<CatalogBrand> catalogBrands = new List<CatalogBrand>();
|
||||
@@ -22,9 +30,9 @@ namespace BlazorAdmin.Pages.CatalogItemPage
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
catalogItems = await CatalogItemListPaged.HandleAsync(50);
|
||||
catalogTypes = await TypeList.HandleAsync();
|
||||
catalogBrands = await BrandList.HandleAsync();
|
||||
catalogItems = await CatalogItemService.List();
|
||||
catalogTypes = await CatalogTypeService.List();
|
||||
catalogBrands = await CatalogBrandService.List();
|
||||
|
||||
CallRequestRefresh();
|
||||
}
|
||||
@@ -54,7 +62,7 @@ namespace BlazorAdmin.Pages.CatalogItemPage
|
||||
|
||||
private async Task ReloadCatalogItems()
|
||||
{
|
||||
catalogItems = await new BlazorAdmin.Services.CatalogItemServices.ListPaged(Auth).HandleAsync(50);
|
||||
catalogItems = await CatalogItemService.List();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using BlazorAdmin.Services;
|
||||
using Blazored.LocalStorage;
|
||||
using BlazorShared;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using BlazorAdmin.Services;
|
||||
using Blazored.LocalStorage;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
|
||||
namespace BlazorAdmin
|
||||
{
|
||||
@@ -16,18 +19,36 @@ namespace BlazorAdmin
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("admin");
|
||||
|
||||
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
var baseUrlConfig = new BaseUrlConfiguration();
|
||||
builder.Configuration.Bind(BaseUrlConfiguration.CONFIG_NAME, baseUrlConfig);
|
||||
builder.Services.AddScoped<BaseUrlConfiguration>(sp => baseUrlConfig);
|
||||
|
||||
builder.Services.AddSingleton<ILocalStorageService, LocalStorageService>();
|
||||
builder.Services.AddSingleton<AuthService>();
|
||||
builder.Services.AddScoped(sp => new HttpClient() { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
|
||||
builder.Services.AddScoped<HttpService>();
|
||||
|
||||
builder.Services.AddScoped<ILocalStorageService, LocalStorageService>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
|
||||
builder.Services.AddAuthorizationCore();
|
||||
builder.Services.AddSingleton<AuthenticationStateProvider, CustomAuthStateProvider>();
|
||||
builder.Services.AddSingleton(sp => (CustomAuthStateProvider)sp.GetRequiredService<AuthenticationStateProvider>());
|
||||
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
|
||||
builder.Services.AddScoped(sp => (CustomAuthStateProvider)sp.GetRequiredService<AuthenticationStateProvider>());
|
||||
|
||||
builder.Services.AddBlazorServices();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
|
||||
|
||||
await ClearLocalStorageCache(builder.Services);
|
||||
|
||||
builder.Build().RunAsync();
|
||||
}
|
||||
|
||||
private static async Task ClearLocalStorageCache(IServiceCollection services)
|
||||
{
|
||||
var sp = services.BuildServiceProvider();
|
||||
var localStorageService = sp.GetRequiredService<ILocalStorageService>();
|
||||
|
||||
await localStorageService.RemoveItemAsync("brands");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
using BlazorAdmin.JavaScript;
|
||||
using Blazored.LocalStorage;
|
||||
using Microsoft.JSInterop;
|
||||
using BlazorShared.Authorization;
|
||||
|
||||
namespace BlazorAdmin.Services
|
||||
{
|
||||
@@ -14,143 +11,32 @@ namespace BlazorAdmin.Services
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILocalStorageService _localStorage;
|
||||
private readonly IJSRuntime _jSRuntime;
|
||||
private static bool InDocker { get; set; }
|
||||
|
||||
public string ApiUrl => Constants.GetApiUrl(InDocker);
|
||||
public bool IsLoggedIn { get; set; }
|
||||
public string UserName { get; set; }
|
||||
|
||||
public AuthService(HttpClient httpClient, ILocalStorageService localStorage, IJSRuntime jSRuntime)
|
||||
public AuthService(HttpClient httpClient,
|
||||
ILocalStorageService localStorage,
|
||||
IJSRuntime jSRuntime)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_localStorage = localStorage;
|
||||
_jSRuntime = jSRuntime;
|
||||
}
|
||||
|
||||
public HttpClient GetHttpClient()
|
||||
{
|
||||
return _httpClient;
|
||||
}
|
||||
|
||||
public async Task Logout()
|
||||
{
|
||||
await DeleteLocalStorage();
|
||||
await DeleteCookies();
|
||||
RemoveAuthorizationHeader();
|
||||
UserName = null;
|
||||
IsLoggedIn = false;
|
||||
await LogoutIdentityManager();
|
||||
}
|
||||
|
||||
public async Task RefreshLoginInfo()
|
||||
{
|
||||
await SetLoginData();
|
||||
}
|
||||
|
||||
public async Task RefreshLoginInfoFromCookie()
|
||||
{
|
||||
var token = await new Cookies(_jSRuntime).GetCookie("token");
|
||||
await SaveTokenInLocalStorage(token);
|
||||
|
||||
var username = await new Cookies(_jSRuntime).GetCookie("username");
|
||||
await SaveUsernameInLocalStorage(username);
|
||||
|
||||
var inDocker = await new Cookies(_jSRuntime).GetCookie("inDocker");
|
||||
await SaveInDockerInLocalStorage(inDocker);
|
||||
|
||||
await RefreshLoginInfo();
|
||||
}
|
||||
|
||||
public async Task<string> GetToken()
|
||||
{
|
||||
|
||||
var token = await _localStorage.GetItemAsync<string>("authToken");
|
||||
return token;
|
||||
}
|
||||
|
||||
public async Task<UserInfo> GetTokenFromController()
|
||||
{
|
||||
return await _httpClient.GetFromJsonAsync<UserInfo>("User");
|
||||
}
|
||||
|
||||
public async Task<string> GetUsername()
|
||||
{
|
||||
var username = await _localStorage.GetItemAsync<string>("username");
|
||||
return username;
|
||||
}
|
||||
|
||||
public async Task<bool> GetInDocker()
|
||||
{
|
||||
return (await _localStorage.GetItemAsync<string>("inDocker")).ToLower() == "true";
|
||||
}
|
||||
|
||||
private async Task LogoutIdentityManager()
|
||||
{
|
||||
await _httpClient.PostAsync("Identity/Account/Logout", null);
|
||||
}
|
||||
|
||||
private async Task DeleteLocalStorage()
|
||||
{
|
||||
await _localStorage.RemoveItemAsync("authToken");
|
||||
await _localStorage.RemoveItemAsync("username");
|
||||
await _localStorage.RemoveItemAsync("inDocker");
|
||||
}
|
||||
|
||||
private async Task DeleteCookies()
|
||||
{
|
||||
await new Cookies(_jSRuntime).DeleteCookie("token");
|
||||
await new Cookies(_jSRuntime).DeleteCookie("username");
|
||||
await new Cookies(_jSRuntime).DeleteCookie("inDocker");
|
||||
}
|
||||
|
||||
private async Task SetLoginData()
|
||||
{
|
||||
IsLoggedIn = !string.IsNullOrEmpty(await GetToken());
|
||||
UserName = await GetUsername();
|
||||
InDocker = await GetInDocker();
|
||||
await SetAuthorizationHeader();
|
||||
}
|
||||
|
||||
private void RemoveAuthorizationHeader()
|
||||
{
|
||||
if (_httpClient.DefaultRequestHeaders.Contains("Authorization"))
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Remove("Authorization");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveTokenInLocalStorage(string token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _localStorage.SetItemAsync("authToken", token);
|
||||
}
|
||||
|
||||
private async Task SaveUsernameInLocalStorage(string username)
|
||||
{
|
||||
if (string.IsNullOrEmpty(username))
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _localStorage.SetItemAsync("username", username);
|
||||
}
|
||||
|
||||
private async Task SaveInDockerInLocalStorage(string inDocker)
|
||||
{
|
||||
if (string.IsNullOrEmpty(inDocker))
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _localStorage.SetItemAsync("inDocker", inDocker);
|
||||
}
|
||||
|
||||
private async Task SetAuthorizationHeader()
|
||||
{
|
||||
var token = await GetToken();
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
19
src/BlazorAdmin/Services/CacheEntry.cs
Normal file
19
src/BlazorAdmin/Services/CacheEntry.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace BlazorAdmin.Services
|
||||
{
|
||||
public class CacheEntry<T>
|
||||
{
|
||||
public CacheEntry(T item)
|
||||
{
|
||||
Value = item;
|
||||
}
|
||||
public CacheEntry()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public T Value { get; set; }
|
||||
public DateTime DateCreated { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Blazored.LocalStorage;
|
||||
using BlazorShared.Interfaces;
|
||||
using BlazorShared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazorAdmin.Services
|
||||
{
|
||||
public class CachedCatalogBrandServiceDecorator : ICatalogBrandService
|
||||
{
|
||||
// TODO: Make a generic decorator for any LookupData type
|
||||
private readonly ILocalStorageService _localStorageService;
|
||||
private readonly CatalogBrandService _catalogBrandService;
|
||||
private ILogger<CachedCatalogBrandServiceDecorator> _logger;
|
||||
|
||||
public CachedCatalogBrandServiceDecorator(ILocalStorageService localStorageService,
|
||||
CatalogBrandService catalogBrandService,
|
||||
ILogger<CachedCatalogBrandServiceDecorator> logger)
|
||||
{
|
||||
_localStorageService = localStorageService;
|
||||
_catalogBrandService = catalogBrandService;
|
||||
_logger = logger;
|
||||
|
||||
}
|
||||
|
||||
public async Task<CatalogBrand> GetById(int id)
|
||||
{
|
||||
return (await List()).FirstOrDefault(x => x.Id == id);
|
||||
}
|
||||
|
||||
public async Task<List<CatalogBrand>> List()
|
||||
{
|
||||
string key = "brands";
|
||||
var cacheEntry = await _localStorageService.GetItemAsync<CacheEntry<List<CatalogBrand>>>(key);
|
||||
if (cacheEntry != null)
|
||||
{
|
||||
_logger.LogInformation("Loading brands from local storage.");
|
||||
// TODO: Get Default Cache Duration from Config
|
||||
if (cacheEntry.DateCreated.AddMinutes(1) > DateTime.UtcNow)
|
||||
{
|
||||
return cacheEntry.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Cache expired; removing brands from local storage.");
|
||||
await _localStorageService.RemoveItemAsync(key);
|
||||
}
|
||||
}
|
||||
|
||||
var brands = await _catalogBrandService.List();
|
||||
var entry = new CacheEntry<List<CatalogBrand>>(brands);
|
||||
await _localStorageService.SetItemAsync(key, entry);
|
||||
return brands;
|
||||
}
|
||||
}
|
||||
}
|
||||
114
src/BlazorAdmin/Services/CachedCatalogItemServiceDecorator.cs
Normal file
114
src/BlazorAdmin/Services/CachedCatalogItemServiceDecorator.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Blazored.LocalStorage;
|
||||
using BlazorShared.Interfaces;
|
||||
using BlazorShared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazorAdmin.Services
|
||||
{
|
||||
public class CachedCatalogItemServiceDecorator : ICatalogItemService
|
||||
{
|
||||
private readonly ILocalStorageService _localStorageService;
|
||||
private readonly CatalogItemService _catalogItemService;
|
||||
private ILogger<CachedCatalogItemServiceDecorator> _logger;
|
||||
|
||||
public CachedCatalogItemServiceDecorator(ILocalStorageService localStorageService,
|
||||
CatalogItemService catalogItemService,
|
||||
ILogger<CachedCatalogItemServiceDecorator> logger)
|
||||
{
|
||||
_localStorageService = localStorageService;
|
||||
_catalogItemService = catalogItemService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<List<CatalogItem>> ListPaged(int pageSize)
|
||||
{
|
||||
string key = "items";
|
||||
var cacheEntry = await _localStorageService.GetItemAsync<CacheEntry<List<CatalogItem>>>(key);
|
||||
if (cacheEntry != null)
|
||||
{
|
||||
_logger.LogInformation("Loading items from local storage.");
|
||||
if (cacheEntry.DateCreated.AddMinutes(1) > DateTime.UtcNow)
|
||||
{
|
||||
return cacheEntry.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation($"Loading {key} from local storage.");
|
||||
await _localStorageService.RemoveItemAsync(key);
|
||||
}
|
||||
}
|
||||
|
||||
var items = await _catalogItemService.ListPaged(pageSize);
|
||||
var entry = new CacheEntry<List<CatalogItem>>(items);
|
||||
await _localStorageService.SetItemAsync(key, entry);
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task<List<CatalogItem>> List()
|
||||
{
|
||||
string key = "items";
|
||||
var cacheEntry = await _localStorageService.GetItemAsync<CacheEntry<List<CatalogItem>>>(key);
|
||||
if (cacheEntry != null)
|
||||
{
|
||||
_logger.LogInformation("Loading items from local storage.");
|
||||
if (cacheEntry.DateCreated.AddMinutes(1) > DateTime.UtcNow)
|
||||
{
|
||||
return cacheEntry.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation($"Loading {key} from local storage.");
|
||||
await _localStorageService.RemoveItemAsync(key);
|
||||
}
|
||||
}
|
||||
|
||||
var items = await _catalogItemService.List();
|
||||
var entry = new CacheEntry<List<CatalogItem>>(items);
|
||||
await _localStorageService.SetItemAsync(key, entry);
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task<CatalogItem> GetById(int id)
|
||||
{
|
||||
return (await List()).FirstOrDefault(x => x.Id == id);
|
||||
}
|
||||
|
||||
public async Task<CatalogItem> Create(CreateCatalogItemRequest catalogItem)
|
||||
{
|
||||
var result = await _catalogItemService.Create(catalogItem);
|
||||
await RefreshLocalStorageList();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<CatalogItem> Edit(CatalogItem catalogItem)
|
||||
{
|
||||
var result = await _catalogItemService.Edit(catalogItem);
|
||||
await RefreshLocalStorageList();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<string> Delete(int id)
|
||||
{
|
||||
var result = await _catalogItemService.Delete(id);
|
||||
await RefreshLocalStorageList();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task RefreshLocalStorageList()
|
||||
{
|
||||
string key = "items";
|
||||
|
||||
await _localStorageService.RemoveItemAsync(key);
|
||||
var items = await _catalogItemService.List();
|
||||
var entry = new CacheEntry<List<CatalogItem>>(items);
|
||||
await _localStorageService.SetItemAsync(key, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Blazored.LocalStorage;
|
||||
using BlazorShared.Interfaces;
|
||||
using BlazorShared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazorAdmin.Services
|
||||
{
|
||||
public class CachedCatalogTypeServiceDecorator : ICatalogTypeService
|
||||
{
|
||||
// TODO: Make a generic decorator for any LookupData type
|
||||
private readonly ILocalStorageService _localStorageService;
|
||||
private readonly CatalogTypeService _catalogTypeService;
|
||||
private ILogger<CachedCatalogTypeServiceDecorator> _logger;
|
||||
|
||||
public CachedCatalogTypeServiceDecorator(ILocalStorageService localStorageService,
|
||||
CatalogTypeService catalogTypeService,
|
||||
ILogger<CachedCatalogTypeServiceDecorator> logger)
|
||||
{
|
||||
_localStorageService = localStorageService;
|
||||
_catalogTypeService = catalogTypeService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<CatalogType> GetById(int id)
|
||||
{
|
||||
return (await List()).FirstOrDefault(x => x.Id == id);
|
||||
}
|
||||
|
||||
public async Task<List<CatalogType>> List()
|
||||
{
|
||||
string key = "types";
|
||||
var cacheEntry = await _localStorageService.GetItemAsync<CacheEntry<List<CatalogType>>>(key);
|
||||
if (cacheEntry != null)
|
||||
{
|
||||
_logger.LogInformation("Loading types from local storage.");
|
||||
if (cacheEntry.DateCreated.AddMinutes(1) > DateTime.UtcNow)
|
||||
{
|
||||
return cacheEntry.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Cache expired; removing types from local storage.");
|
||||
await _localStorageService.RemoveItemAsync(key);
|
||||
}
|
||||
}
|
||||
|
||||
var types = await _catalogTypeService.List();
|
||||
var entry = new CacheEntry<List<CatalogType>>(types);
|
||||
await _localStorageService.SetItemAsync(key, entry);
|
||||
return types;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/BlazorAdmin/Services/CatalogBrandService.cs
Normal file
41
src/BlazorAdmin/Services/CatalogBrandService.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using BlazorShared;
|
||||
using BlazorShared.Interfaces;
|
||||
using BlazorShared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
namespace BlazorAdmin.Services
|
||||
{
|
||||
public class CatalogBrandService : ICatalogBrandService
|
||||
{
|
||||
// TODO: Make a generic service for any LookupData type
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<CatalogBrandService> _logger;
|
||||
private string _apiUrl;
|
||||
|
||||
public CatalogBrandService(HttpClient httpClient,
|
||||
BaseUrlConfiguration baseUrlConfiguration,
|
||||
ILogger<CatalogBrandService> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_apiUrl = baseUrlConfiguration.ApiBase;
|
||||
}
|
||||
|
||||
public async Task<CatalogBrand> GetById(int id)
|
||||
{
|
||||
return (await List()).FirstOrDefault(x => x.Id == id);
|
||||
}
|
||||
|
||||
public async Task<List<CatalogBrand>> List()
|
||||
{
|
||||
_logger.LogInformation("Fetching brands from API.");
|
||||
return (await _httpClient.GetFromJsonAsync<CatalogBrandResponse>($"{_apiUrl}catalog-brands"))?.CatalogBrands;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace BlazorAdmin.Services.CatalogBrandServices
|
||||
{
|
||||
public class CatalogBrand
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazorAdmin.Services.CatalogBrandServices
|
||||
{
|
||||
public class List
|
||||
{
|
||||
private readonly AuthService _authService;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public List(AuthService authService, HttpClient httpClient)
|
||||
{
|
||||
_authService = authService;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<List<CatalogBrand>> HandleAsync()
|
||||
{
|
||||
return (await _httpClient.GetFromJsonAsync<CatalogBrandResult>($"{_authService.ApiUrl}catalog-brands"))?.CatalogBrands;
|
||||
}
|
||||
|
||||
public static string GetBrandName(IEnumerable<CatalogBrand> brands, int brandId)
|
||||
{
|
||||
var type = brands.FirstOrDefault(t => t.Id == brandId);
|
||||
|
||||
return type == null ? "None" : type.Name;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
102
src/BlazorAdmin/Services/CatalogItemService.cs
Normal file
102
src/BlazorAdmin/Services/CatalogItemService.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using BlazorShared;
|
||||
using BlazorShared.Interfaces;
|
||||
using BlazorShared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
namespace BlazorAdmin.Services
|
||||
{
|
||||
public class CatalogItemService : ICatalogItemService
|
||||
{
|
||||
private readonly ICatalogBrandService _brandService;
|
||||
private readonly ICatalogTypeService _typeService;
|
||||
private readonly HttpService _httpService;
|
||||
private readonly ILogger<CatalogItemService> _logger;
|
||||
private string _apiUrl;
|
||||
|
||||
public CatalogItemService(ICatalogBrandService brandService,
|
||||
ICatalogTypeService typeService,
|
||||
HttpService httpService,
|
||||
BaseUrlConfiguration baseUrlConfiguration,
|
||||
ILogger<CatalogItemService> logger)
|
||||
{
|
||||
_brandService = brandService;
|
||||
_typeService = typeService;
|
||||
|
||||
_httpService = httpService;
|
||||
_logger = logger;
|
||||
_apiUrl = baseUrlConfiguration.ApiBase;
|
||||
}
|
||||
|
||||
public async Task<CatalogItem> Create(CreateCatalogItemRequest catalogItem)
|
||||
{
|
||||
return (await _httpService.HttpPost<CreateCatalogItemResponse>("catalog-items", catalogItem)).CatalogItem;
|
||||
}
|
||||
|
||||
public async Task<CatalogItem> Edit(CatalogItem catalogItem)
|
||||
{
|
||||
return (await _httpService.HttpPut<EditCatalogItemResult>("catalog-items", catalogItem)).CatalogItem;
|
||||
}
|
||||
|
||||
public async Task<string> Delete(int catalogItemId)
|
||||
{
|
||||
return (await _httpService.HttpDelete<DeleteCatalogItemResponse>("catalog-items", catalogItemId)).Status;
|
||||
}
|
||||
|
||||
public async Task<CatalogItem> GetById(int id)
|
||||
{
|
||||
var brandListTask = _brandService.List();
|
||||
var typeListTask = _typeService.List();
|
||||
var itemGetTask = _httpService.HttpGet<EditCatalogItemResult>($"catalog-items/{id}");
|
||||
await Task.WhenAll(brandListTask, typeListTask, itemGetTask);
|
||||
var brands = brandListTask.Result;
|
||||
var types = typeListTask.Result;
|
||||
var catalogItem = itemGetTask.Result.CatalogItem;
|
||||
catalogItem.CatalogBrand = brands.FirstOrDefault(b => b.Id == catalogItem.CatalogBrandId)?.Name;
|
||||
catalogItem.CatalogType = types.FirstOrDefault(t => t.Id == catalogItem.CatalogTypeId)?.Name;
|
||||
return catalogItem;
|
||||
}
|
||||
|
||||
public async Task<List<CatalogItem>> ListPaged(int pageSize)
|
||||
{
|
||||
_logger.LogInformation("Fetching catalog items from API.");
|
||||
|
||||
var brandListTask = _brandService.List();
|
||||
var typeListTask = _typeService.List();
|
||||
var itemListTask = _httpService.HttpGet<PagedCatalogItemResponse>($"catalog-items?PageSize=10");
|
||||
await Task.WhenAll(brandListTask, typeListTask, itemListTask);
|
||||
var brands = brandListTask.Result;
|
||||
var types = typeListTask.Result;
|
||||
var items = itemListTask.Result.CatalogItems;
|
||||
foreach (var item in items)
|
||||
{
|
||||
item.CatalogBrand = brands.FirstOrDefault(b => b.Id == item.CatalogBrandId)?.Name;
|
||||
item.CatalogType = types.FirstOrDefault(t => t.Id == item.CatalogTypeId)?.Name;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task<List<CatalogItem>> List()
|
||||
{
|
||||
_logger.LogInformation("Fetching catalog items from API.");
|
||||
|
||||
var brandListTask = _brandService.List();
|
||||
var typeListTask = _typeService.List();
|
||||
//TODO: Need to change the api to support full list
|
||||
var itemListTask = _httpService.HttpGet<PagedCatalogItemResponse>($"catalog-items?PageSize=100");
|
||||
await Task.WhenAll(brandListTask, typeListTask, itemListTask);
|
||||
var brands = brandListTask.Result;
|
||||
var types = typeListTask.Result;
|
||||
var items = itemListTask.Result.CatalogItems;
|
||||
foreach (var item in items)
|
||||
{
|
||||
item.CatalogBrand = brands.FirstOrDefault(b => b.Id == item.CatalogBrandId)?.Name;
|
||||
item.CatalogType = types.FirstOrDefault(t => t.Id == item.CatalogTypeId)?.Name;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace BlazorAdmin.Services.CatalogItemServices
|
||||
{
|
||||
public class CreateCatalogItemResult
|
||||
{
|
||||
public CatalogItem CatalogItem { get; set; } = new CatalogItem();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazorAdmin.Services.CatalogItemServices
|
||||
{
|
||||
public class Create
|
||||
{
|
||||
private readonly HttpService _httpService;
|
||||
|
||||
public Create(AuthService authService)
|
||||
{
|
||||
_httpService = new HttpService(authService.GetHttpClient(), authService.ApiUrl);
|
||||
}
|
||||
|
||||
public async Task<CatalogItem> HandleAsync(CreateCatalogItemRequest catalogItem)
|
||||
{
|
||||
return (await _httpService.HttpPost<CreateCatalogItemResult>("catalog-items", catalogItem)).CatalogItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace BlazorAdmin.Services.CatalogItemServices
|
||||
{
|
||||
public class DeleteCatalogItemResult
|
||||
{
|
||||
public string Status { get; set; } = "Deleted";
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazorAdmin.Services.CatalogItemServices
|
||||
{
|
||||
public class Delete
|
||||
{
|
||||
private readonly HttpService _httpService;
|
||||
|
||||
public Delete(AuthService authService)
|
||||
{
|
||||
_httpService = new HttpService(authService.GetHttpClient(), authService.ApiUrl);
|
||||
}
|
||||
|
||||
public async Task<string> HandleAsync(int catalogItemId)
|
||||
{
|
||||
return (await _httpService.HttpDelete<DeleteCatalogItemResult>("catalog-items", catalogItemId)).Status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazorAdmin.Services.CatalogItemServices
|
||||
{
|
||||
public class Edit
|
||||
{
|
||||
private readonly HttpService _httpService;
|
||||
|
||||
public Edit(AuthService authService)
|
||||
{
|
||||
_httpService = new HttpService(authService.GetHttpClient(), authService.ApiUrl);
|
||||
}
|
||||
|
||||
public async Task<CatalogItem> HandleAsync(CatalogItem catalogItem)
|
||||
{
|
||||
return (await _httpService.HttpPut<EditCatalogItemResult>("catalog-items", catalogItem)).CatalogItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace BlazorAdmin.Services.CatalogItemServices
|
||||
{
|
||||
public class GetByIdCatalogItemResult
|
||||
{
|
||||
public CatalogItem CatalogItem { get; set; } = new CatalogItem();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazorAdmin.Services.CatalogItemServices
|
||||
{
|
||||
public class GetById
|
||||
{
|
||||
private readonly HttpService _httpService;
|
||||
|
||||
public GetById(AuthService authService)
|
||||
{
|
||||
_httpService = new HttpService(authService.GetHttpClient(), authService.ApiUrl);
|
||||
}
|
||||
|
||||
public async Task<CatalogItem> HandleAsync(int catalogItemId)
|
||||
{
|
||||
return (await _httpService.HttpGet<EditCatalogItemResult>($"catalog-items/{catalogItemId}")).CatalogItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazorAdmin.Services.CatalogItemServices
|
||||
{
|
||||
public class ListPaged
|
||||
{
|
||||
private readonly HttpService _httpService;
|
||||
|
||||
public ListPaged(AuthService authService)
|
||||
{
|
||||
_httpService = new HttpService(authService.GetHttpClient(), authService.ApiUrl);
|
||||
}
|
||||
|
||||
public async Task<List<CatalogItem>> HandleAsync(int pageSize)
|
||||
{
|
||||
return (await _httpService.HttpGet<PagedCatalogItemResult>($"catalog-items?PageSize={pageSize}")).CatalogItems;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
40
src/BlazorAdmin/Services/CatalogTypeService.cs
Normal file
40
src/BlazorAdmin/Services/CatalogTypeService.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using BlazorShared;
|
||||
using BlazorShared.Interfaces;
|
||||
using BlazorShared.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazorAdmin.Services
|
||||
{
|
||||
public class CatalogTypeService : ICatalogTypeService
|
||||
{
|
||||
// TODO: Make a generic service for any LookupData type
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<CatalogTypeService> _logger;
|
||||
private string _apiUrl;
|
||||
|
||||
public CatalogTypeService(HttpClient httpClient,
|
||||
BaseUrlConfiguration baseUrlConfiguration,
|
||||
ILogger<CatalogTypeService> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_apiUrl = baseUrlConfiguration.ApiBase;
|
||||
}
|
||||
|
||||
public async Task<CatalogType> GetById(int id)
|
||||
{
|
||||
return (await List()).FirstOrDefault(x => x.Id == id);
|
||||
}
|
||||
|
||||
public async Task<List<CatalogType>> List()
|
||||
{
|
||||
_logger.LogInformation("Fetching types from API.");
|
||||
return (await _httpClient.GetFromJsonAsync<CatalogTypeResponse>($"{_apiUrl}catalog-types"))?.CatalogTypes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace BlazorAdmin.Services.CatalogTypeServices
|
||||
{
|
||||
public class CatalogType
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazorAdmin.Services.CatalogTypeServices
|
||||
{
|
||||
public class List
|
||||
{
|
||||
private readonly AuthService _authService;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public List(AuthService authService, HttpClient httpClient)
|
||||
{
|
||||
_authService = authService;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<List<CatalogType>> HandleAsync()
|
||||
{
|
||||
return (await _httpClient.GetFromJsonAsync<CatalogTypeResult>($"{_authService.ApiUrl}catalog-types"))?.CatalogTypes;
|
||||
}
|
||||
|
||||
public static string GetTypeName(IEnumerable<CatalogType> types, int typeId)
|
||||
{
|
||||
var type = types.FirstOrDefault(t => t.Id == typeId);
|
||||
|
||||
return type == null ? "None" : type.Name;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Net.Http;
|
||||
using BlazorShared;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
@@ -10,10 +11,11 @@ namespace BlazorAdmin.Services
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _apiUrl;
|
||||
|
||||
public HttpService(HttpClient httpClient, string apiUrl)
|
||||
|
||||
public HttpService(HttpClient httpClient, BaseUrlConfiguration baseUrlConfiguration)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_apiUrl = apiUrl;
|
||||
_apiUrl = baseUrlConfiguration.ApiBase;
|
||||
}
|
||||
|
||||
public async Task<T> HttpGet<T>(string uri)
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
using BlazorAdmin.Services.CatalogItemServices;
|
||||
using BlazorAdmin.Services;
|
||||
using BlazorShared.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BlazorAdmin
|
||||
{
|
||||
public static class ServicesConfiguration
|
||||
{
|
||||
public static IServiceCollection AddBlazorServices(this IServiceCollection service)
|
||||
public static IServiceCollection AddBlazorServices(this IServiceCollection services)
|
||||
{
|
||||
service.AddScoped<Create>();
|
||||
service.AddScoped<ListPaged>();
|
||||
service.AddScoped<Delete>();
|
||||
service.AddScoped<Edit>();
|
||||
service.AddScoped<GetById>();
|
||||
services.AddScoped<ICatalogBrandService, CachedCatalogBrandServiceDecorator>();
|
||||
services.AddScoped<CatalogBrandService>();
|
||||
services.AddScoped<ICatalogTypeService, CachedCatalogTypeServiceDecorator>();
|
||||
services.AddScoped<CatalogTypeService>();
|
||||
services.AddScoped<ICatalogItemService, CachedCatalogItemServiceDecorator>();
|
||||
services.AddScoped<CatalogItemService>();
|
||||
|
||||
service.AddScoped<BlazorAdmin.Services.CatalogBrandServices.List>();
|
||||
service.AddScoped<BlazorAdmin.Services.CatalogTypeServices.List>();
|
||||
|
||||
return service;
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
src/BlazorAdmin/Shared/CustomInputSelect.cs
Normal file
38
src/BlazorAdmin/Shared/CustomInputSelect.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
|
||||
namespace BlazorAdmin.Shared
|
||||
{
|
||||
/// <summary>
|
||||
/// This is needed until 5.0 ships with native support
|
||||
/// https://www.pragimtech.com/blog/blazor/inputselect-does-not-support-system.int32/
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
public class CustomInputSelect<TValue> : InputSelect<TValue>
|
||||
{
|
||||
protected override bool TryParseValueFromString(string value, out TValue result,
|
||||
out string validationErrorMessage)
|
||||
{
|
||||
if (typeof(TValue) == typeof(int))
|
||||
{
|
||||
if (int.TryParse(value, out var resultInt))
|
||||
{
|
||||
result = (TValue)(object)resultInt;
|
||||
validationErrorMessage = null;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = default;
|
||||
validationErrorMessage =
|
||||
$"The selected value {value} is not a valid number.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return base.TryParseValueFromString(value, out result,
|
||||
out validationErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@inject AuthService Auth
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
@inherits BlazorAdmin.Helpers.BlazorLayoutComponent
|
||||
@@ -25,8 +25,9 @@
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await Auth.RefreshLoginInfoFromCookie();
|
||||
if (!Auth.IsLoggedIn)
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
|
||||
if(authState.User == null)
|
||||
{
|
||||
await new Route(JSRuntime).RouteOutside("/Identity/Account/Login");
|
||||
}
|
||||
@@ -35,5 +36,4 @@
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@inject AuthService Auth
|
||||
@inherits BlazorAdmin.Helpers.BlazorComponent
|
||||
@inherits BlazorAdmin.Helpers.BlazorComponent
|
||||
<div class="top-row pl-4 navbar navbar-dark">
|
||||
<a class="navbar-brand" href="">eShopOnWeb Admin</a>
|
||||
<button class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
@@ -14,28 +13,27 @@
|
||||
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="manage/my-account" Match="NavLinkMatch.All">
|
||||
<span class="oi oi-person" aria-hidden="true"></span> @Auth.UserName
|
||||
</NavLink>
|
||||
</li>
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="manage/my-account" Match="NavLinkMatch.All">
|
||||
<span class="oi oi-person" aria-hidden="true"></span> @context.User.Identity.Name
|
||||
|
||||
<li class="nav-item px-3">
|
||||
@if (Auth.IsLoggedIn)
|
||||
{
|
||||
<NavLink class="nav-link" href="logout">
|
||||
<span class="oi oi-account-logout" aria-hidden="true"></span> Logout
|
||||
</NavLink>
|
||||
}
|
||||
|
||||
</li>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="logout">
|
||||
<span class="oi oi-account-logout" aria-hidden="true"></span> Logout
|
||||
</NavLink>
|
||||
</li>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
private bool collapseNavMenu = true;
|
||||
public string UserName { get; set; }
|
||||
|
||||
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||
|
||||
|
||||
@@ -11,9 +11,8 @@
|
||||
@using BlazorAdmin
|
||||
@using BlazorAdmin.Shared
|
||||
@using BlazorAdmin.Services
|
||||
@using BlazorAdmin.Services.CatalogBrandServices
|
||||
@using BlazorAdmin.Services.CatalogItemServices
|
||||
@using BlazorAdmin.Services.CatalogTypeServices
|
||||
@using BlazorAdmin.JavaScript
|
||||
@using BlazorShared.Authorization
|
||||
@using BlazorShared.Interfaces
|
||||
@using BlazorInputFile
|
||||
@using BlazorShared.Models
|
||||
|
||||
6
src/BlazorAdmin/wwwroot/appsettings.Docker.json
Normal file
6
src/BlazorAdmin/wwwroot/appsettings.Docker.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"baseUrls": {
|
||||
"apiBase": "http://localhost:5200/api/",
|
||||
"webBase": "http://host.docker.internal:5106/"
|
||||
}
|
||||
}
|
||||
14
src/BlazorAdmin/wwwroot/appsettings.json
Normal file
14
src/BlazorAdmin/wwwroot/appsettings.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"baseUrls": {
|
||||
"apiBase": "https://localhost:5099/api/",
|
||||
"webBase": "https://localhost:44315/"
|
||||
},
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"System": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
[
|
||||
{
|
||||
"date": "2018-05-06",
|
||||
"temperatureC": 1,
|
||||
"summary": "Freezing"
|
||||
},
|
||||
{
|
||||
"date": "2018-05-07",
|
||||
"temperatureC": 14,
|
||||
"summary": "Bracing"
|
||||
},
|
||||
{
|
||||
"date": "2018-05-08",
|
||||
"temperatureC": -13,
|
||||
"summary": "Freezing"
|
||||
},
|
||||
{
|
||||
"date": "2018-05-09",
|
||||
"temperatureC": -16,
|
||||
"summary": "Balmy"
|
||||
},
|
||||
{
|
||||
"date": "2018-05-10",
|
||||
"temperatureC": -2,
|
||||
"summary": "Chilly"
|
||||
}
|
||||
]
|
||||
@@ -6,23 +6,5 @@
|
||||
{
|
||||
public const string ADMINISTRATORS = "Administrators";
|
||||
}
|
||||
public static string GetApiUrl(bool inDocker) =>
|
||||
inDocker ? DOCKER_API_URL : API_URL;
|
||||
|
||||
public static string GetWebUrl(bool inDocker) =>
|
||||
inDocker ? DOCKER_WEB_URL : WEB_URL;
|
||||
|
||||
public static string GetWebUrlInternal(bool inDocker) =>
|
||||
inDocker ? DOCKER_WEB_URL.Replace("localhost", "host.docker.internal") : WEB_URL;
|
||||
|
||||
public static string GetOriginWebUrl(bool inDocker) =>
|
||||
GetWebUrl(inDocker).TrimEnd('/');
|
||||
|
||||
private const string API_URL = "https://localhost:5099/api/";
|
||||
private const string DOCKER_API_URL = "http://localhost:5200/api/";
|
||||
|
||||
private const string WEB_URL = "https://localhost:44315/";
|
||||
private const string DOCKER_WEB_URL = "http://localhost:5106/";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace BlazorShared.Authorization
|
||||
public bool IsAuthenticated { get; set; }
|
||||
public string NameClaimType { get; set; }
|
||||
public string RoleClaimType { get; set; }
|
||||
public string Token { get; set; }
|
||||
public IEnumerable<ClaimValue> Claims { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
10
src/BlazorShared/BaseUrlConfiguration.cs
Normal file
10
src/BlazorShared/BaseUrlConfiguration.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace BlazorShared
|
||||
{
|
||||
public class BaseUrlConfiguration
|
||||
{
|
||||
public const string CONFIG_NAME = "baseUrls";
|
||||
|
||||
public string ApiBase { get; set; }
|
||||
public string WebBase { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,8 @@
|
||||
<AssemblyName>BlazorShared</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BlazorInputFile" Version="0.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
12
src/BlazorShared/Interfaces/ICatalogBrandService.cs
Normal file
12
src/BlazorShared/Interfaces/ICatalogBrandService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BlazorShared.Models;
|
||||
|
||||
namespace BlazorShared.Interfaces
|
||||
{
|
||||
public interface ICatalogBrandService
|
||||
{
|
||||
Task<List<CatalogBrand>> List();
|
||||
Task<CatalogBrand> GetById(int id);
|
||||
}
|
||||
}
|
||||
16
src/BlazorShared/Interfaces/ICatalogItemService.cs
Normal file
16
src/BlazorShared/Interfaces/ICatalogItemService.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BlazorShared.Models;
|
||||
|
||||
namespace BlazorShared.Interfaces
|
||||
{
|
||||
public interface ICatalogItemService
|
||||
{
|
||||
Task<CatalogItem> Create(CreateCatalogItemRequest catalogItem);
|
||||
Task<CatalogItem> Edit(CatalogItem catalogItem);
|
||||
Task<string> Delete(int id);
|
||||
Task<CatalogItem> GetById(int id);
|
||||
Task<List<CatalogItem>> ListPaged(int pageSize);
|
||||
Task<List<CatalogItem>> List();
|
||||
}
|
||||
}
|
||||
12
src/BlazorShared/Interfaces/ICatalogTypeService.cs
Normal file
12
src/BlazorShared/Interfaces/ICatalogTypeService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using BlazorShared.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazorShared.Interfaces
|
||||
{
|
||||
public interface ICatalogTypeService
|
||||
{
|
||||
Task<List<CatalogType>> List();
|
||||
Task<CatalogType> GetById(int id);
|
||||
}
|
||||
}
|
||||
6
src/BlazorShared/Models/CatalogBrand.cs
Normal file
6
src/BlazorShared/Models/CatalogBrand.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace BlazorShared.Models
|
||||
{
|
||||
public class CatalogBrand : LookupData
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BlazorAdmin.Services.CatalogBrandServices
|
||||
namespace BlazorShared.Models
|
||||
{
|
||||
public class CatalogBrandResult
|
||||
public class CatalogBrandResponse
|
||||
{
|
||||
public List<CatalogBrand> CatalogBrands { get; set; } = new List<CatalogBrand>();
|
||||
}
|
||||
@@ -4,15 +4,17 @@ using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using BlazorInputFile;
|
||||
|
||||
namespace BlazorAdmin.Services.CatalogItemServices
|
||||
namespace BlazorShared.Models
|
||||
{
|
||||
public class CatalogItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int CatalogTypeId { get; set; }
|
||||
public string CatalogType { get; set; } = "NotSet";
|
||||
|
||||
public int CatalogBrandId { get; set; }
|
||||
public string CatalogBrand { get; set; } = "NotSet";
|
||||
|
||||
[Required(ErrorMessage = "The Name field is required")]
|
||||
public string Name { get; set; }
|
||||
@@ -60,14 +62,17 @@ namespace BlazorAdmin.Services.CatalogItemServices
|
||||
|
||||
public static async Task<string> DataToBase64(IFileListEntry fileItem)
|
||||
{
|
||||
using var reader = new StreamReader(fileItem.Data);
|
||||
using ( var reader = new StreamReader(fileItem.Data))
|
||||
{
|
||||
using (var memStream = new MemoryStream())
|
||||
{
|
||||
await reader.BaseStream.CopyToAsync(memStream);
|
||||
var fileData = memStream.ToArray();
|
||||
var encodedBase64 = Convert.ToBase64String(fileData);
|
||||
|
||||
await using var memStream = new MemoryStream();
|
||||
await reader.BaseStream.CopyToAsync(memStream);
|
||||
var fileData = memStream.ToArray();
|
||||
var encodedBase64 = Convert.ToBase64String(fileData);
|
||||
|
||||
return encodedBase64;
|
||||
return encodedBase64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsExtensionValid(string fileName)
|
||||
6
src/BlazorShared/Models/CatalogType.cs
Normal file
6
src/BlazorShared/Models/CatalogType.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace BlazorShared.Models
|
||||
{
|
||||
public class CatalogType : LookupData
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BlazorAdmin.Services.CatalogTypeServices
|
||||
namespace BlazorShared.Models
|
||||
{
|
||||
public class CatalogTypeResult
|
||||
public class CatalogTypeResponse
|
||||
{
|
||||
public List<CatalogType> CatalogTypes { get; set; } = new List<CatalogType>();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BlazorAdmin.Services.CatalogItemServices
|
||||
namespace BlazorShared.Models
|
||||
{
|
||||
public class CreateCatalogItemRequest
|
||||
{
|
||||
7
src/BlazorShared/Models/CreateCatalogItemResponse.cs
Normal file
7
src/BlazorShared/Models/CreateCatalogItemResponse.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace BlazorShared.Models
|
||||
{
|
||||
public class CreateCatalogItemResponse
|
||||
{
|
||||
public CatalogItem CatalogItem { get; set; } = new CatalogItem();
|
||||
}
|
||||
}
|
||||
7
src/BlazorShared/Models/DeleteCatalogItemResponse.cs
Normal file
7
src/BlazorShared/Models/DeleteCatalogItemResponse.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace BlazorShared.Models
|
||||
{
|
||||
public class DeleteCatalogItemResponse
|
||||
{
|
||||
public string Status { get; set; } = "Deleted";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace BlazorAdmin.Services.CatalogItemServices
|
||||
namespace BlazorShared.Models
|
||||
{
|
||||
public class EditCatalogItemResult
|
||||
{
|
||||
8
src/BlazorShared/Models/LookupData.cs
Normal file
8
src/BlazorShared/Models/LookupData.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace BlazorShared.Models
|
||||
{
|
||||
public abstract class LookupData
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BlazorAdmin.Services.CatalogItemServices
|
||||
namespace BlazorShared.Models
|
||||
{
|
||||
public class PagedCatalogItemResult
|
||||
public class PagedCatalogItemResponse
|
||||
{
|
||||
public List<CatalogItem> CatalogItems { get; set; } = new List<CatalogItem>();
|
||||
public int PageCount { get; set; } = 0;
|
||||
@@ -1,10 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
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;
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.eShopWeb.PublicApi
|
||||
{
|
||||
@@ -43,6 +44,13 @@ namespace Microsoft.eShopWeb.PublicApi
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration((builderContext, config) =>
|
||||
{
|
||||
var env = builderContext.HostingEnvironment;
|
||||
config
|
||||
.AddJsonFile("appsettings.json")
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
|
||||
})
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
|
||||
@@ -25,13 +25,6 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:5099;http://localhost:5098"
|
||||
},
|
||||
"Docker": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using AutoMapper;
|
||||
using BlazorShared;
|
||||
using BlazorShared.Authorization;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
@@ -28,7 +29,6 @@ namespace Microsoft.eShopWeb.PublicApi
|
||||
public class Startup
|
||||
{
|
||||
private const string CORS_POLICY = "CorsPolicy";
|
||||
public static bool InDocker => Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true";
|
||||
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
@@ -46,6 +46,11 @@ namespace Microsoft.eShopWeb.PublicApi
|
||||
//ConfigureProductionServices(services);
|
||||
}
|
||||
|
||||
public void ConfigureDockerServices(IServiceCollection services)
|
||||
{
|
||||
ConfigureDevelopmentServices(services);
|
||||
}
|
||||
|
||||
private void ConfigureInMemoryDatabases(IServiceCollection services)
|
||||
{
|
||||
services.AddDbContext<CatalogContext>(c =>
|
||||
@@ -90,7 +95,10 @@ namespace Microsoft.eShopWeb.PublicApi
|
||||
services.AddSingleton<IUriComposer>(new UriComposer(Configuration.Get<CatalogSettings>()));
|
||||
services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
|
||||
services.AddScoped<ITokenClaimsService, IdentityTokenClaimService>();
|
||||
services.AddScoped<IFileSystem, WebFileSystem>(x => new WebFileSystem($"{Constants.GetWebUrlInternal(Startup.InDocker)}File"));
|
||||
|
||||
var baseUrlConfig = new BaseUrlConfiguration();
|
||||
Configuration.Bind(BaseUrlConfiguration.CONFIG_NAME, baseUrlConfig);
|
||||
services.AddScoped<IFileSystem, WebFileSystem>(x => new WebFileSystem($"{baseUrlConfig.WebBase}File"));
|
||||
|
||||
services.AddMemoryCache();
|
||||
|
||||
@@ -112,15 +120,12 @@ namespace Microsoft.eShopWeb.PublicApi
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy(name: CORS_POLICY,
|
||||
builder =>
|
||||
{
|
||||
builder.WithOrigins("http://localhost:44319",
|
||||
"https://localhost:44319",
|
||||
Constants.GetOriginWebUrl(InDocker));
|
||||
builder.WithOrigins(baseUrlConfig.WebBase.Replace("host.docker.internal", "localhost").TrimEnd('/'));
|
||||
builder.AllowAnyMethod();
|
||||
builder.AllowAnyHeader();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"baseUrls": {
|
||||
"apiBase": "https://localhost:5099/api/",
|
||||
"webBase": "https://localhost:44315/"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
17
src/PublicApi/appsettings.Docker.json
Normal file
17
src/PublicApi/appsettings.Docker.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"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;"
|
||||
},
|
||||
"baseUrls": {
|
||||
"apiBase": "http://localhost:5200/api/",
|
||||
"webBase": "http://host.docker.internal:5106/"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"baseUrls": {
|
||||
"apiBase": "https://localhost:5099/api/",
|
||||
"webBase": "https://localhost:44315/"
|
||||
},
|
||||
"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;"
|
||||
|
||||
@@ -22,16 +22,12 @@ namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly ILogger<LoginModel> _logger;
|
||||
private readonly IBasketService _basketService;
|
||||
private readonly AuthService _authService;
|
||||
private readonly ITokenClaimsService _tokenClaimsService;
|
||||
|
||||
public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger, IBasketService basketService, AuthService authService, ITokenClaimsService tokenClaimsService)
|
||||
public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger, IBasketService basketService)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
_basketService = basketService;
|
||||
_authService = authService;
|
||||
_tokenClaimsService = tokenClaimsService;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
@@ -88,8 +84,6 @@ namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var token = await _tokenClaimsService.GetTokenAsync(Input.Email);
|
||||
CreateAuthCookie(Input.Email, token, Startup.InDocker);
|
||||
_logger.LogInformation("User logged in.");
|
||||
await TransferAnonymousBasketToUserAsync(Input.Email);
|
||||
return LocalRedirect(returnUrl);
|
||||
@@ -114,15 +108,6 @@ namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
|
||||
return Page();
|
||||
}
|
||||
|
||||
private void CreateAuthCookie(string username, string token, bool inDocker)
|
||||
{
|
||||
var cookieOptions = new CookieOptions();
|
||||
cookieOptions.Expires = DateTime.Today.AddYears(10);
|
||||
Response.Cookies.Append("token", token, cookieOptions);
|
||||
Response.Cookies.Append("username", username, cookieOptions);
|
||||
Response.Cookies.Append("inDocker", inDocker.ToString(), cookieOptions);
|
||||
}
|
||||
|
||||
private async Task TransferAnonymousBasketToUserAsync(string userName)
|
||||
{
|
||||
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<environment include="Development">
|
||||
<environment include="Development,Docker">
|
||||
<script src="~/Identity/lib/jquery-validation/dist/jquery.validate.js"></script>
|
||||
<script src="~/Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
|
||||
</environment>
|
||||
<environment exclude="Development">
|
||||
<environment exclude="Development,Docker">
|
||||
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.17.0/jquery.validate.min.js"
|
||||
asp-fallback-src="~/Identity/lib/jquery-validation/dist/jquery.validate.min.js"
|
||||
asp-fallback-test="window.jQuery && window.jQuery.validator"
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BlazorShared.Authorization;
|
||||
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.eShopWeb.Web.Controllers
|
||||
{
|
||||
@@ -11,13 +13,20 @@ namespace Microsoft.eShopWeb.Web.Controllers
|
||||
[ApiController]
|
||||
public class UserController : ControllerBase
|
||||
{
|
||||
private readonly ITokenClaimsService _tokenClaimsService;
|
||||
|
||||
public UserController(ITokenClaimsService tokenClaimsService)
|
||||
{
|
||||
_tokenClaimsService = tokenClaimsService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
[AllowAnonymous]
|
||||
public IActionResult GetCurrentUser() =>
|
||||
Ok(User.Identity.IsAuthenticated ? CreateUserInfo(User) : UserInfo.Anonymous);
|
||||
public async Task<IActionResult> GetCurrentUser() =>
|
||||
Ok(User.Identity.IsAuthenticated ? await CreateUserInfo(User) : UserInfo.Anonymous);
|
||||
|
||||
private UserInfo CreateUserInfo(ClaimsPrincipal claimsPrincipal)
|
||||
private async Task<UserInfo> CreateUserInfo(ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
if (!claimsPrincipal.Identity.IsAuthenticated)
|
||||
{
|
||||
@@ -57,6 +66,9 @@ namespace Microsoft.eShopWeb.Web.Controllers
|
||||
userInfo.Claims = claims;
|
||||
}
|
||||
|
||||
var token = await _tokenClaimsService.GetTokenAsync(claimsPrincipal.Identity.Name);
|
||||
userInfo.Token = token;
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>@ViewData["Title"] - Microsoft.eShopOnWeb</title>
|
||||
<base href="~/" />
|
||||
<environment include="Development">
|
||||
<environment include="Development,Docker">
|
||||
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
||||
</environment>
|
||||
<environment exclude="Development">
|
||||
<environment exclude="Development,Docker">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
|
||||
asp-fallback-href="css/bootstrap/bootstrap.min.css"
|
||||
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.eShopWeb.Web
|
||||
{
|
||||
@@ -42,6 +43,13 @@ namespace Microsoft.eShopWeb.Web
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration((builderContext, config) =>
|
||||
{
|
||||
var env = builderContext.HostingEnvironment;
|
||||
config
|
||||
.AddJsonFile("appsettings.json")
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
|
||||
})
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
|
||||
@@ -25,13 +25,13 @@ using Blazored.LocalStorage;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
||||
using BlazorShared;
|
||||
|
||||
namespace Microsoft.eShopWeb.Web
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
private IServiceCollection _services;
|
||||
public static bool InDocker => Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true";
|
||||
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
@@ -49,6 +49,15 @@ namespace Microsoft.eShopWeb.Web
|
||||
//ConfigureProductionServices(services);
|
||||
}
|
||||
|
||||
public void ConfigureDockerServices(IServiceCollection services)
|
||||
{
|
||||
services.AddDataProtection()
|
||||
.SetApplicationName("eshopwebmvc")
|
||||
.PersistKeysToFileSystem(new DirectoryInfo(@"./"));
|
||||
|
||||
ConfigureDevelopmentServices(services);
|
||||
}
|
||||
|
||||
private void ConfigureInMemoryDatabases(IServiceCollection services)
|
||||
{
|
||||
// use in-memory database
|
||||
@@ -88,12 +97,6 @@ namespace Microsoft.eShopWeb.Web
|
||||
{
|
||||
services.AddCookieSettings();
|
||||
|
||||
if (InDocker)
|
||||
{
|
||||
services.AddDataProtection()
|
||||
.SetApplicationName("eshopwebmvc")
|
||||
.PersistKeysToFileSystem(new DirectoryInfo(@"./"));
|
||||
}
|
||||
|
||||
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(options =>
|
||||
@@ -141,15 +144,22 @@ namespace Microsoft.eShopWeb.Web
|
||||
config.Path = "/allservices";
|
||||
});
|
||||
|
||||
|
||||
var baseUrlConfig = new BaseUrlConfiguration();
|
||||
Configuration.Bind(BaseUrlConfiguration.CONFIG_NAME, baseUrlConfig);
|
||||
services.AddScoped<BaseUrlConfiguration>(sp => baseUrlConfig);
|
||||
// Blazor Admin Required Services for Prerendering
|
||||
services.AddScoped<HttpClient>(s => new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(BlazorShared.Authorization.Constants.GetWebUrl(InDocker))
|
||||
BaseAddress = new Uri(baseUrlConfig.WebBase)
|
||||
});
|
||||
|
||||
// add blazor services
|
||||
services.AddBlazoredLocalStorage();
|
||||
services.AddServerSideBlazor();
|
||||
services.AddScoped<AuthService>();
|
||||
|
||||
services.AddScoped<HttpService>();
|
||||
services.AddBlazorServices();
|
||||
|
||||
_services = services; // used to debug registered services
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] - Microsoft.eShopOnWeb</title>
|
||||
<environment names="Development">
|
||||
<environment names="Development,Docker">
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
|
||||
<link rel="stylesheet" href="~/css/app.css" />
|
||||
<link rel="stylesheet" href="~/css/app.component.css" />
|
||||
@@ -49,7 +49,7 @@
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<environment names="Development">
|
||||
<environment names="Development,Docker">
|
||||
<script src="~/lib/jquery/jquery.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<environment names="Development">
|
||||
<environment names="Development,Docker">
|
||||
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
|
||||
</environment>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"baseUrls": {
|
||||
"apiBase": "https://localhost:5099/api/",
|
||||
"webBase": "https://localhost:44315/"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
|
||||
17
src/Web/appsettings.Docker.json
Normal file
17
src/Web/appsettings.Docker.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"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;"
|
||||
},
|
||||
"baseUrls": {
|
||||
"apiBase": "http://localhost:5200/api/",
|
||||
"webBase": "http://host.docker.internal:5106/"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"baseUrls": {
|
||||
"apiBase": "https://localhost:5099/api/",
|
||||
"webBase": "https://localhost:44315/"
|
||||
},
|
||||
"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;"
|
||||
|
||||
2
src/Web/wwwroot/css/site.min.css
vendored
2
src/Web/wwwroot/css/site.min.css
vendored
File diff suppressed because one or more lines are too long
BIN
src/Web/wwwroot/images/products/5.jpg
Normal file
BIN
src/Web/wwwroot/images/products/5.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Reference in New Issue
Block a user