Changed to hosted blazorwasm project.
Restructured project following changes. Moved shop assembly fetching to public facing Web API.
This commit is contained in:
parent
e07b234eb2
commit
6c684372df
@ -1,12 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\MultiShop.ShopFramework\MultiShop.ShopFramework.csproj" />
|
|
||||||
<ProjectReference Include="..\..\SimpleLogger\SimpleLogger.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Framework\MultiShop.Shop.Framework.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\SimpleLogger\SimpleLogger.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
@ -10,10 +10,10 @@ using System.Text.RegularExpressions;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GameServiceWarden.Core.Collection;
|
using GameServiceWarden.Core.Collection;
|
||||||
using MultiShop.ShopFramework;
|
using MultiShop.Shop.Framework;
|
||||||
using SimpleLogger;
|
using SimpleLogger;
|
||||||
|
|
||||||
namespace AliExpressShop
|
namespace MultiShop.Shop.AliExpressModule
|
||||||
{
|
{
|
||||||
public class Shop : IShop
|
public class Shop : IShop
|
||||||
{
|
{
|
@ -7,10 +7,10 @@ using System.Text.RegularExpressions;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GameServiceWarden.Core.Collection;
|
using GameServiceWarden.Core.Collection;
|
||||||
using MultiShop.ShopFramework;
|
using MultiShop.Shop.Framework;
|
||||||
using SimpleLogger;
|
using SimpleLogger;
|
||||||
|
|
||||||
namespace AliExpressShop
|
namespace MultiShop.Shop.AliExpressModule
|
||||||
{
|
{
|
||||||
class ShopEnumerator : IAsyncEnumerator<ProductListing>
|
class ShopEnumerator : IAsyncEnumerator<ProductListing>
|
||||||
{
|
{
|
@ -1,8 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MultiShop.ShopFramework\MultiShop.ShopFramework.csproj" />
|
<ProjectReference Include="..\Framework\MultiShop.Shop.Framework.csproj" />
|
||||||
<ProjectReference Include="..\..\SimpleLogger\SimpleLogger.csproj" />
|
<ProjectReference Include="..\..\..\SimpleLogger\SimpleLogger.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
@ -3,9 +3,9 @@ using System.Collections.Generic;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using MultiShop.ShopFramework;
|
using MultiShop.Shop.Framework;
|
||||||
|
|
||||||
namespace BanggoodShop
|
namespace MultiShop.Shop.BanggoodModule
|
||||||
{
|
{
|
||||||
public class Shop : IShop
|
public class Shop : IShop
|
||||||
{
|
{
|
@ -9,10 +9,10 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using MultiShop.ShopFramework;
|
using MultiShop.Shop.Framework;
|
||||||
using SimpleLogger;
|
using SimpleLogger;
|
||||||
|
|
||||||
namespace BanggoodShop
|
namespace MultiShop.Shop.BanggoodModule
|
||||||
{
|
{
|
||||||
class ShopEnumerator : IAsyncEnumerator<ProductListing>
|
class ShopEnumerator : IAsyncEnumerator<ProductListing>
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace MultiShop.ShopFramework
|
namespace MultiShop.Shop.Framework
|
||||||
{
|
{
|
||||||
public enum Currency
|
public enum Currency
|
||||||
{
|
{
|
@ -4,7 +4,7 @@ using System.Net.Http;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MultiShop.ShopFramework
|
namespace MultiShop.Shop.Framework
|
||||||
{
|
{
|
||||||
public interface IShop : IAsyncEnumerable<ProductListing>, IDisposable
|
public interface IShop : IAsyncEnumerable<ProductListing>, IDisposable
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace MultiShop.ShopFramework
|
namespace MultiShop.Shop.Framework
|
||||||
{
|
{
|
||||||
public struct ProductListing
|
public struct ProductListing
|
||||||
{
|
{
|
@ -1,10 +0,0 @@
|
|||||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
|
|
||||||
<Found Context="routeData">
|
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
|
||||||
</Found>
|
|
||||||
<NotFound>
|
|
||||||
<LayoutView Layout="@typeof(MainLayout)">
|
|
||||||
<p>Sorry, there's nothing at this address.</p>
|
|
||||||
</LayoutView>
|
|
||||||
</NotFound>
|
|
||||||
</Router>
|
|
39
src/MultiShop/Client/App.razor
Normal file
39
src/MultiShop/Client/App.razor
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
@using System.Reflection
|
||||||
|
@using Microsoft.Extensions.Configuration
|
||||||
|
@inject IHttpClientFactory HttpFactory
|
||||||
|
@inject IConfiguration Configuration
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
|
||||||
|
<CascadingAuthenticationState>
|
||||||
|
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
|
||||||
|
<Found Context="routeData">
|
||||||
|
@if (modulesLoaded)
|
||||||
|
{
|
||||||
|
<CascadingValue Value="shops" Name="Shops">
|
||||||
|
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" Context="authState">
|
||||||
|
<NotAuthorized>
|
||||||
|
@if (!authState.User.Identity.IsAuthenticated)
|
||||||
|
{
|
||||||
|
<RedirectToLogin />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p>You are not authorized to access this resource.</p>
|
||||||
|
}
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeRouteView>
|
||||||
|
</CascadingValue>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p>Loading modules...</p>
|
||||||
|
}
|
||||||
|
</Found>
|
||||||
|
<NotFound>
|
||||||
|
<LayoutView Layout="@typeof(MainLayout)">
|
||||||
|
<p>Sorry, there's nothing at this address.</p>
|
||||||
|
</LayoutView>
|
||||||
|
</NotFound>
|
||||||
|
</Router>
|
||||||
|
</CascadingAuthenticationState>
|
116
src/MultiShop/Client/App.razor.cs
Normal file
116
src/MultiShop/Client/App.razor.cs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.Loader;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MultiShop.Shop.Framework;
|
||||||
|
using SimpleLogger;
|
||||||
|
|
||||||
|
namespace MultiShop.Client
|
||||||
|
{
|
||||||
|
public partial class App
|
||||||
|
{
|
||||||
|
private bool modulesLoaded = false;
|
||||||
|
|
||||||
|
private Dictionary<string, IShop> shops = new Dictionary<string, IShop>();
|
||||||
|
private Dictionary<string, byte[]> assemblyData = new Dictionary<string, byte[]>();
|
||||||
|
private Dictionary<string, Assembly> assemblyCache = new Dictionary<string, Assembly>();
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await DownloadShopModules();
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
private async Task DownloadShopModules()
|
||||||
|
{
|
||||||
|
HttpClient http = HttpFactory.CreateClient("Public-MultiShop.ServerAPI");
|
||||||
|
Logger.Log($"Fetching shop modules.", LogLevel.Debug);
|
||||||
|
string[] assemblyFileNames = await http.GetFromJsonAsync<string[]>("ShopModules");
|
||||||
|
Dictionary<Task<byte[]>, string> downloadTasks = new Dictionary<Task<byte[]>, string>(assemblyFileNames.Length);
|
||||||
|
|
||||||
|
foreach (string assemblyFileName in assemblyFileNames)
|
||||||
|
{
|
||||||
|
Logger.Log($"Downloading \"{assemblyFileName}\"...", LogLevel.Debug);
|
||||||
|
downloadTasks.Add(http.GetByteArrayAsync(Path.Join("ShopModules", assemblyFileName + ".dll")), assemblyFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (downloadTasks.Count != 0)
|
||||||
|
{
|
||||||
|
Task<byte[]> data = await Task.WhenAny(downloadTasks.Keys);
|
||||||
|
string assemblyFileName = downloadTasks[data];
|
||||||
|
Logger.Log($"\"{assemblyFileName}\" completed downloading.", LogLevel.Debug);
|
||||||
|
assemblyData.Add(assemblyFileName, data.Result);
|
||||||
|
downloadTasks.Remove(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyDependencyRequest;
|
||||||
|
|
||||||
|
foreach (string assemblyFileName in assemblyData.Keys)
|
||||||
|
{
|
||||||
|
Assembly assembly = AppDomain.CurrentDomain.Load(assemblyData[assemblyFileName]);
|
||||||
|
bool used = false;
|
||||||
|
foreach (Type type in assembly.GetTypes())
|
||||||
|
{
|
||||||
|
if (typeof(IShop).IsAssignableFrom(type))
|
||||||
|
{
|
||||||
|
IShop shop = Activator.CreateInstance(type) as IShop;
|
||||||
|
if (shop != null)
|
||||||
|
{
|
||||||
|
shop.Initialize();
|
||||||
|
shops.Add(shop.ShopName, shop);
|
||||||
|
Logger.Log($"Registered and started lifetime of module for \"{shop.ShopName}\".", LogLevel.Debug);
|
||||||
|
used = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!used) {
|
||||||
|
Logger.Log($"Since unused, caching \"{assemblyFileName}\".", LogLevel.Debug);
|
||||||
|
assemblyCache.Add(assemblyFileName, assembly);
|
||||||
|
}
|
||||||
|
assemblyData.Remove(assemblyFileName);
|
||||||
|
}
|
||||||
|
foreach (string assembly in assemblyData.Keys)
|
||||||
|
{
|
||||||
|
Logger.Log($"{assembly} was unused.", LogLevel.Warning);
|
||||||
|
}
|
||||||
|
foreach (string assembly in assemblyCache.Keys)
|
||||||
|
{
|
||||||
|
Logger.Log($"\"{assembly}\" was unused.", LogLevel.Warning);
|
||||||
|
}
|
||||||
|
assemblyData.Clear();
|
||||||
|
assemblyCache.Clear();
|
||||||
|
modulesLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Assembly OnAssemblyDependencyRequest(object sender, ResolveEventArgs args)
|
||||||
|
{
|
||||||
|
string dependencyName = args.Name.Substring(0, args.Name.IndexOf(','));
|
||||||
|
Logger.Log($"Assembly \"{args.RequestingAssembly.GetName().Name}\" is requesting dependency assembly \"{dependencyName}\".", LogLevel.Debug);
|
||||||
|
if (assemblyCache.ContainsKey(dependencyName)) {
|
||||||
|
Logger.Log($"Found \"{dependencyName}\" in cache.", LogLevel.Debug);
|
||||||
|
Assembly dep = assemblyCache[dependencyName];
|
||||||
|
assemblyCache.Remove(dependencyName);
|
||||||
|
return dep;
|
||||||
|
} else if (assemblyData.ContainsKey(dependencyName)) {
|
||||||
|
return AppDomain.CurrentDomain.Load(assemblyData[dependencyName]);
|
||||||
|
} else {
|
||||||
|
Logger.Log($"No dependency under name \"{args.Name}\"", LogLevel.Warning);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (string name in shops.Keys)
|
||||||
|
{
|
||||||
|
shops[name].Dispose();
|
||||||
|
Logger.Log($"Ending lifetime of shop module for \"{name}\".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,12 +7,15 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.5" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.5" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.5" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="5.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
|
<PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MultiShop.ShopFramework\MultiShop.ShopFramework.csproj" />
|
<ProjectReference Include="..\Shared\MultiShop.Shared.csproj" />
|
||||||
<ProjectReference Include="..\..\SimpleLogger\SimpleLogger.csproj" />
|
<ProjectReference Include="..\..\MultiShop.Shop\Framework\MultiShop.Shop.Framework.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\SimpleLogger\SimpleLogger.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
7
src/MultiShop/Client/Pages/Authentication.razor
Normal file
7
src/MultiShop/Client/Pages/Authentication.razor
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
@page "/authentication/{action}"
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
<RemoteAuthenticatorView Action="@Action" />
|
||||||
|
|
||||||
|
@code{
|
||||||
|
[Parameter] public string Action { get; set; }
|
||||||
|
}
|
16
src/MultiShop/Client/Pages/Counter.razor
Normal file
16
src/MultiShop/Client/Pages/Counter.razor
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
@page "/counter"
|
||||||
|
|
||||||
|
<h1>Counter</h1>
|
||||||
|
|
||||||
|
<p>Current count: @currentCount</p>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private int currentCount = 0;
|
||||||
|
|
||||||
|
private void IncrementCount()
|
||||||
|
{
|
||||||
|
currentCount++;
|
||||||
|
}
|
||||||
|
}
|
56
src/MultiShop/Client/Pages/FetchData.razor
Normal file
56
src/MultiShop/Client/Pages/FetchData.razor
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
@page "/fetchdata"
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
@using MultiShop.Shared
|
||||||
|
@attribute [Authorize]
|
||||||
|
@inject HttpClient Http
|
||||||
|
|
||||||
|
<h1>Weather forecast</h1>
|
||||||
|
|
||||||
|
<p>This component demonstrates fetching data from the server.</p>
|
||||||
|
|
||||||
|
@if (forecasts == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Temp. (C)</th>
|
||||||
|
<th>Temp. (F)</th>
|
||||||
|
<th>Summary</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var forecast in forecasts)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@forecast.Date.ToShortDateString()</td>
|
||||||
|
<td>@forecast.TemperatureC</td>
|
||||||
|
<td>@forecast.TemperatureF</td>
|
||||||
|
<td>@forecast.Summary</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private WeatherForecast[] forecasts;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
|
||||||
|
}
|
||||||
|
catch (AccessTokenNotAvailableException exception)
|
||||||
|
{
|
||||||
|
exception.Redirect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
@page "/info"
|
@page "/info"
|
||||||
@using ShopFramework
|
@using Shop.Framework
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -1,13 +1,10 @@
|
|||||||
@page "/search/{Query?}"
|
@page "/search/{Query?}"
|
||||||
@using Microsoft.Extensions.Configuration
|
@using Microsoft.Extensions.Configuration
|
||||||
@using ShopFramework
|
@using MultiShop.Shared
|
||||||
@using SimpleLogger
|
|
||||||
@using DataStructures
|
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
@inject IConfiguration Configuration
|
@inject IConfiguration Configuration
|
||||||
@inject IJSRuntime js
|
@inject IJSRuntime js
|
||||||
|
|
||||||
@* TODO: Split C# code into a partial class. *@
|
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
<div class="input-group my-2">
|
<div class="input-group my-2">
|
||||||
<input type="text" class="form-control" placeholder="What are you looking for?" aria-label="What are you looking for?" id="search-input" @bind="Query" @onkeyup="@(async (a) => {if (a.Code == "Enter" || a.Code == "NumpadEnter") await PerformSearch(Query);})" disabled="@searching">
|
<input type="text" class="form-control" placeholder="What are you looking for?" aria-label="What are you looking for?" id="search-input" @bind="Query" @onkeyup="@(async (a) => {if (a.Code == "Enter" || a.Code == "NumpadEnter") await PerformSearch(Query);})" disabled="@searching">
|
||||||
@ -242,171 +239,76 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ListingTableView Products="@listings" />
|
@if (!organizing && listings.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-top-borderless">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Price</th>
|
||||||
|
<th scope="col">Shipping</th>
|
||||||
|
<th scope="col">Purchases</th>
|
||||||
|
<th scope="col">Rating</th>
|
||||||
|
<th scope="col">Reviews</th>
|
||||||
|
<th scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
@if (!showSearchConfiguration && !searching) {
|
||||||
|
<tbody>
|
||||||
|
<Virtualize Items="@listings" Context="product">
|
||||||
|
<tr>
|
||||||
|
<th scope="row" @key="product.Listing">
|
||||||
|
<div class="text-truncate">@product.Listing.Name</div>
|
||||||
|
<small>From @product.ShopName</small>
|
||||||
|
@if (product.Listing.ConvertedPrices)
|
||||||
|
{
|
||||||
|
<span class="ml-3 mr-1 badge badge-warning">Converted price</span>
|
||||||
|
}
|
||||||
|
@foreach (ResultsProfile.Category c in product.Tops)
|
||||||
|
{
|
||||||
|
<span class="mx-1 badge badge-primary">@CategoryTags(c)</span>
|
||||||
|
}
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
@if (product.Listing.UpperPrice != product.Listing.LowerPrice)
|
||||||
|
{
|
||||||
|
<div class="text-truncate">
|
||||||
|
@product.Listing.LowerPrice to @product.Listing.UpperPrice
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="text-truncate">
|
||||||
|
@GetOrNA(product.Listing.LowerPrice)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="text-truncate">
|
||||||
|
@GetOrNA(product.Listing.Shipping)
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="text-truncate">
|
||||||
|
@GetOrNA(product.Listing.PurchaseCount)
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="text-truncate">
|
||||||
|
@(product.Listing.Rating != null ? string.Format("{0:P2}", product.Listing.Rating) : "N/A")
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>@GetOrNA(product.Listing.ReviewCount)</td>
|
||||||
|
<td>
|
||||||
|
<a href="@product.Listing.URL" class="btn btn-primary" target="_blank">View</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</Virtualize>
|
||||||
|
</tbody>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[CascadingParameter(Name = "Shops")]
|
|
||||||
public Dictionary<string, IShop> Shops { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public string Query { get; set; }
|
|
||||||
|
|
||||||
private SearchProfile activeProfile = new SearchProfile();
|
|
||||||
private ResultsProfile activeResultsProfile = new ResultsProfile();
|
|
||||||
|
|
||||||
private bool showSearchConfiguration = false;
|
|
||||||
private bool showResultsConfiguration = false;
|
|
||||||
|
|
||||||
private string ToggleSearchConfigButtonCss
|
|
||||||
{
|
|
||||||
get => "btn btn-outline-secondary" + (showSearchConfiguration ? " active" : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ToggleResultsConfigurationcss {
|
|
||||||
get => "btn btn-outline-secondary btn-tab" + (showResultsConfiguration ? " active" : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool searched = false;
|
|
||||||
private bool searching = false;
|
|
||||||
private bool organizing = false;
|
|
||||||
|
|
||||||
private int resultsChecked = 0;
|
|
||||||
private List<ProductListingInfo> listings = new List<ProductListingInfo>();
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
foreach (string shop in Shops.Keys)
|
|
||||||
{
|
|
||||||
activeProfile.shopStates[shop] = true;
|
|
||||||
}
|
|
||||||
base.OnInitialized();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
await base.OnInitializedAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(Query))
|
|
||||||
{
|
|
||||||
await PerformSearch(Query);
|
|
||||||
}
|
|
||||||
await base.OnParametersSetAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PerformSearch(string query)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(query)) return;
|
|
||||||
if (searching) return;
|
|
||||||
searching = true;
|
|
||||||
Logger.Log($"Received search request for \"{query}\".", LogLevel.Debug);
|
|
||||||
resultsChecked = 0;
|
|
||||||
listings.Clear();
|
|
||||||
Dictionary<ResultsProfile.Category, List<ProductListingInfo>> greatest = new Dictionary<ResultsProfile.Category,
|
|
||||||
List<ProductListingInfo>>();
|
|
||||||
foreach (string shopName in Shops.Keys)
|
|
||||||
{
|
|
||||||
if (activeProfile.shopStates[shopName])
|
|
||||||
{
|
|
||||||
Logger.Log($"Querying \"{shopName}\" for products.");
|
|
||||||
Shops[shopName].SetupSession(query, activeProfile.currency);
|
|
||||||
int shopViableResults = 0;
|
|
||||||
await foreach (ProductListing listing in Shops[shopName])
|
|
||||||
{
|
|
||||||
resultsChecked += 1;
|
|
||||||
if (resultsChecked % 50 == 0)
|
|
||||||
{
|
|
||||||
StateHasChanged();
|
|
||||||
await Task.Yield();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (listing.Shipping == null && !activeProfile.keepUnknownShipping || (activeProfile.enableMaxShippingFee && listing.Shipping > activeProfile.MaxShippingFee)) continue;
|
|
||||||
float shippingDifference = listing.Shipping != null ? listing.Shipping.Value : 0;
|
|
||||||
if (!(listing.LowerPrice + shippingDifference >= activeProfile.lowerPrice && (!activeProfile.enableUpperPrice || listing.UpperPrice + shippingDifference <= activeProfile.UpperPrice))) continue;
|
|
||||||
if ((listing.Rating == null && !activeProfile.keepUnrated) && activeProfile.minRating > (listing.Rating == null ? 0 : listing.Rating)) continue;
|
|
||||||
if ((listing.PurchaseCount == null && !activeProfile.keepUnknownPurchaseCount) || activeProfile.minPurchases > (listing.PurchaseCount == null ? 0 : listing.PurchaseCount)) continue;
|
|
||||||
if ((listing.ReviewCount == null && !activeProfile.keepUnknownRatingCount) || activeProfile.minReviews > (listing.ReviewCount == null ? 0 : listing.ReviewCount)) continue;
|
|
||||||
|
|
||||||
ProductListingInfo info = new ProductListingInfo(listing, shopName);
|
|
||||||
listings.Add(info);
|
|
||||||
await Task.Yield();
|
|
||||||
foreach (ResultsProfile.Category c in Enum.GetValues<ResultsProfile.Category>())
|
|
||||||
{
|
|
||||||
if (!greatest.ContainsKey(c)) greatest[c] = new List<ProductListingInfo>();
|
|
||||||
if (greatest[c].Count > 0)
|
|
||||||
{
|
|
||||||
int? compResult = c.CompareListings(info, greatest[c][0]);
|
|
||||||
if (compResult.HasValue)
|
|
||||||
{
|
|
||||||
if (compResult > 0) greatest[c].Clear();
|
|
||||||
if (compResult >= 0) greatest[c].Add(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (c.CompareListings(info, info).HasValue)
|
|
||||||
{
|
|
||||||
greatest[c].Add(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shopViableResults += 1;
|
|
||||||
if (shopViableResults >= activeProfile.maxResults) break;
|
|
||||||
}
|
|
||||||
Logger.Log($"\"{shopName}\" has completed. There are {listings.Count} results in total.", LogLevel.Debug);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logger.Log($"Skipping {shopName} since it's disabled.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
searching = false;
|
|
||||||
searched = true;
|
|
||||||
|
|
||||||
foreach (ResultsProfile.Category c in greatest.Keys)
|
|
||||||
{
|
|
||||||
foreach (ProductListingInfo info in greatest[c])
|
|
||||||
{
|
|
||||||
info.Tops.Add(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Organize(activeResultsProfile.Order);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Organize(List<ResultsProfile.Category> order)
|
|
||||||
{
|
|
||||||
if (searching) return;
|
|
||||||
organizing = true;
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
List<ProductListingInfo> sortedResults = await Task.Run<List<ProductListingInfo>>(() =>
|
|
||||||
{
|
|
||||||
List<ProductListingInfo> sorted = new List<ProductListingInfo>(listings);
|
|
||||||
sorted.Sort((a, b) =>
|
|
||||||
{
|
|
||||||
foreach (ResultsProfile.Category category in activeResultsProfile.Order)
|
|
||||||
{
|
|
||||||
int? compareResult = category.CompareListings(a, b);
|
|
||||||
if (compareResult.HasValue && compareResult.Value != 0)
|
|
||||||
{
|
|
||||||
return -compareResult.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
return sorted;
|
|
||||||
});
|
|
||||||
listings.Clear();
|
|
||||||
listings.AddRange(sortedResults);
|
|
||||||
organizing = false;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
|
196
src/MultiShop/Client/Pages/Search.razor.cs
Normal file
196
src/MultiShop/Client/Pages/Search.razor.cs
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MultiShop.Shared;
|
||||||
|
using MultiShop.Shop.Framework;
|
||||||
|
using SimpleLogger;
|
||||||
|
|
||||||
|
namespace MultiShop.Client.Pages
|
||||||
|
{
|
||||||
|
public partial class Search
|
||||||
|
{
|
||||||
|
[CascadingParameter(Name = "Shops")]
|
||||||
|
public Dictionary<string, IShop> Shops { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Query { get; set; }
|
||||||
|
|
||||||
|
private SearchProfile activeProfile = new SearchProfile();
|
||||||
|
private ResultsProfile activeResultsProfile = new ResultsProfile();
|
||||||
|
|
||||||
|
private bool showSearchConfiguration = false;
|
||||||
|
private bool showResultsConfiguration = false;
|
||||||
|
|
||||||
|
private string ToggleSearchConfigButtonCss
|
||||||
|
{
|
||||||
|
get => "btn btn-outline-secondary" + (showSearchConfiguration ? " active" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ToggleResultsConfigurationcss {
|
||||||
|
get => "btn btn-outline-secondary btn-tab" + (showResultsConfiguration ? " active" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool searched = false;
|
||||||
|
private bool searching = false;
|
||||||
|
private bool organizing = false;
|
||||||
|
|
||||||
|
private int resultsChecked = 0;
|
||||||
|
private List<ProductListingInfo> listings = new List<ProductListingInfo>();
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
foreach (string shop in Shops.Keys)
|
||||||
|
{
|
||||||
|
activeProfile.shopStates[shop] = true;
|
||||||
|
}
|
||||||
|
base.OnInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(Query))
|
||||||
|
{
|
||||||
|
await PerformSearch(Query);
|
||||||
|
}
|
||||||
|
await base.OnParametersSetAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PerformSearch(string query)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(query)) return;
|
||||||
|
if (searching) return;
|
||||||
|
searching = true;
|
||||||
|
Logger.Log($"Received search request for \"{query}\".", LogLevel.Debug);
|
||||||
|
resultsChecked = 0;
|
||||||
|
listings.Clear();
|
||||||
|
Dictionary<ResultsProfile.Category, List<ProductListingInfo>> greatest = new Dictionary<ResultsProfile.Category,
|
||||||
|
List<ProductListingInfo>>();
|
||||||
|
foreach (string shopName in Shops.Keys)
|
||||||
|
{
|
||||||
|
if (activeProfile.shopStates[shopName])
|
||||||
|
{
|
||||||
|
Logger.Log($"Querying \"{shopName}\" for products.");
|
||||||
|
Shops[shopName].SetupSession(query, activeProfile.currency);
|
||||||
|
int shopViableResults = 0;
|
||||||
|
await foreach (ProductListing listing in Shops[shopName])
|
||||||
|
{
|
||||||
|
resultsChecked += 1;
|
||||||
|
if (resultsChecked % 50 == 0)
|
||||||
|
{
|
||||||
|
StateHasChanged();
|
||||||
|
await Task.Yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (listing.Shipping == null && !activeProfile.keepUnknownShipping || (activeProfile.enableMaxShippingFee && listing.Shipping > activeProfile.MaxShippingFee)) continue;
|
||||||
|
float shippingDifference = listing.Shipping != null ? listing.Shipping.Value : 0;
|
||||||
|
if (!(listing.LowerPrice + shippingDifference >= activeProfile.lowerPrice && (!activeProfile.enableUpperPrice || listing.UpperPrice + shippingDifference <= activeProfile.UpperPrice))) continue;
|
||||||
|
if ((listing.Rating == null && !activeProfile.keepUnrated) && activeProfile.minRating > (listing.Rating == null ? 0 : listing.Rating)) continue;
|
||||||
|
if ((listing.PurchaseCount == null && !activeProfile.keepUnknownPurchaseCount) || activeProfile.minPurchases > (listing.PurchaseCount == null ? 0 : listing.PurchaseCount)) continue;
|
||||||
|
if ((listing.ReviewCount == null && !activeProfile.keepUnknownRatingCount) || activeProfile.minReviews > (listing.ReviewCount == null ? 0 : listing.ReviewCount)) continue;
|
||||||
|
|
||||||
|
ProductListingInfo info = new ProductListingInfo(listing, shopName);
|
||||||
|
listings.Add(info);
|
||||||
|
foreach (ResultsProfile.Category c in Enum.GetValues<ResultsProfile.Category>())
|
||||||
|
{
|
||||||
|
if (!greatest.ContainsKey(c)) greatest[c] = new List<ProductListingInfo>();
|
||||||
|
if (greatest[c].Count > 0)
|
||||||
|
{
|
||||||
|
int? compResult = c.CompareListings(info, greatest[c][0]);
|
||||||
|
if (compResult.HasValue)
|
||||||
|
{
|
||||||
|
if (compResult > 0) greatest[c].Clear();
|
||||||
|
if (compResult >= 0) greatest[c].Add(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (c.CompareListings(info, info).HasValue)
|
||||||
|
{
|
||||||
|
greatest[c].Add(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shopViableResults += 1;
|
||||||
|
if (shopViableResults >= activeProfile.maxResults) break;
|
||||||
|
}
|
||||||
|
Logger.Log($"\"{shopName}\" has completed. There are {listings.Count} results in total.", LogLevel.Debug);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.Log($"Skipping {shopName} since it's disabled.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searching = false;
|
||||||
|
searched = true;
|
||||||
|
|
||||||
|
foreach (ResultsProfile.Category c in greatest.Keys)
|
||||||
|
{
|
||||||
|
foreach (ProductListingInfo info in greatest[c])
|
||||||
|
{
|
||||||
|
info.Tops.Add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Organize(activeResultsProfile.Order);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Organize(List<ResultsProfile.Category> order)
|
||||||
|
{
|
||||||
|
if (searching) return;
|
||||||
|
organizing = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
List<ProductListingInfo> sortedResults = await Task.Run<List<ProductListingInfo>>(() =>
|
||||||
|
{
|
||||||
|
List<ProductListingInfo> sorted = new List<ProductListingInfo>(listings);
|
||||||
|
sorted.Sort((a, b) =>
|
||||||
|
{
|
||||||
|
foreach (ResultsProfile.Category category in activeResultsProfile.Order)
|
||||||
|
{
|
||||||
|
int? compareResult = category.CompareListings(a, b);
|
||||||
|
if (compareResult.HasValue && compareResult.Value != 0)
|
||||||
|
{
|
||||||
|
return -compareResult.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
});
|
||||||
|
listings.Clear();
|
||||||
|
listings.AddRange(sortedResults);
|
||||||
|
organizing = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private string GetOrNA(object data, string prepend = null, string append = null)
|
||||||
|
{
|
||||||
|
return data != null ? (prepend + data.ToString() + append) : "N/A";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CategoryTags(ResultsProfile.Category c)
|
||||||
|
{
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case ResultsProfile.Category.RatingPriceRatio:
|
||||||
|
return "Best rating to price ratio";
|
||||||
|
case ResultsProfile.Category.Price:
|
||||||
|
return "Lowest price";
|
||||||
|
case ResultsProfile.Category.Purchases:
|
||||||
|
return "Most purchases";
|
||||||
|
case ResultsProfile.Category.Reviews:
|
||||||
|
return "Most reviews";
|
||||||
|
}
|
||||||
|
throw new ArgumentException($"{c} does not have an associated string.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
src/MultiShop/Client/Program.cs
Normal file
34
src/MultiShop/Client/Program.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SimpleLogger;
|
||||||
|
|
||||||
|
namespace MultiShop.Client
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
Logger.AddLogListener(new ConsoleLogReceiver() {Level = LogLevel.Debug});
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
builder.RootComponents.Add<App>("#app");
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient("MultiShop.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)).AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient("Public-MultiShop.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
|
||||||
|
|
||||||
|
// Supply HttpClient instances that include access tokens when making requests to the server project
|
||||||
|
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("MultiShop.ServerAPI"));
|
||||||
|
|
||||||
|
builder.Services.AddApiAuthorization();
|
||||||
|
|
||||||
|
await builder.Build().RunAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,8 @@
|
|||||||
"windowsAuthentication": false,
|
"windowsAuthentication": false,
|
||||||
"anonymousAuthentication": true,
|
"anonymousAuthentication": true,
|
||||||
"iisExpress": {
|
"iisExpress": {
|
||||||
"applicationUrl": "http://localhost:46072",
|
"applicationUrl": "http://localhost:5738",
|
||||||
"sslPort": 44317
|
"sslPort": 44353
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
24
src/MultiShop/Client/Shared/LoginDisplay.razor
Normal file
24
src/MultiShop/Client/Shared/LoginDisplay.razor
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject SignOutSessionStateManager SignOutManager
|
||||||
|
|
||||||
|
<AuthorizeView>
|
||||||
|
<Authorized>
|
||||||
|
<a href="authentication/profile">Hello, @context.User.Identity.Name!</a>
|
||||||
|
<button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<a href="authentication/register">Register</a>
|
||||||
|
<a href="authentication/login">Log in</a>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
|
||||||
|
@code{
|
||||||
|
private async Task BeginSignOut(MouseEventArgs args)
|
||||||
|
{
|
||||||
|
await SignOutManager.SetSignOutState();
|
||||||
|
Navigation.NavigateTo("authentication/logout");
|
||||||
|
}
|
||||||
|
}
|
10
src/MultiShop/Client/Shared/MainLayout.razor
Normal file
10
src/MultiShop/Client/Shared/MainLayout.razor
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<NavMenu />
|
||||||
|
<div class="main">
|
||||||
|
<div class="content px-4">
|
||||||
|
@Body
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,6 +1,6 @@
|
|||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
<a class="navbar-brand" href="">
|
<a class="navbar-brand" href="">
|
||||||
<img src="100.png" width="30" height="30" class="d-inline-block align-top">
|
<img src="100x100.png" width="30" height="30" class="d-inline-block align-top">
|
||||||
MultiShop
|
MultiShop
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" @onclick="ToggleNavMenu">
|
<button class="navbar-toggler" type="button" @onclick="ToggleNavMenu">
|
8
src/MultiShop/Client/Shared/RedirectToLogin.razor
Normal file
8
src/MultiShop/Client/Shared/RedirectToLogin.razor
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
@inject NavigationManager Navigation
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
@code {
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
@using System.Net.Http
|
@using System.Net.Http
|
||||||
@using System.Net.Http.Json
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using MultiShop
|
@using MultiShop.Client
|
||||||
@using MultiShop.Shared
|
@using MultiShop.Client.Shared
|
||||||
|
@using Shop.Framework
|
||||||
|
@using SimpleLogger
|
Before Width: | Height: | Size: 255 B After Width: | Height: | Size: 255 B |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
@ -8,7 +8,7 @@
|
|||||||
<base href="/" />
|
<base href="/" />
|
||||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||||
<link href="css/app.css" rel="stylesheet" />
|
<link href="css/app.css" rel="stylesheet" />
|
||||||
<link href="MultiShop.styles.css" rel="stylesheet" />
|
<link href="MultiShop.Client.styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@ -19,7 +19,9 @@
|
|||||||
<a href="" class="reload">Reload</a>
|
<a href="" class="reload">Reload</a>
|
||||||
<a class="dismiss">🗙</a>
|
<a class="dismiss">🗙</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/ComponentsSupport.js"></script>
|
<script src="js/ComponentsSupport.js"></script>
|
||||||
|
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
|
||||||
<script src="_framework/blazor.webassembly.js"></script>
|
<script src="_framework/blazor.webassembly.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
@ -1,22 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using SimpleLogger;
|
|
||||||
|
|
||||||
namespace MultiShop
|
|
||||||
{
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
public static async Task Main(string[] args)
|
|
||||||
{
|
|
||||||
|
|
||||||
Logger.AddLogListener(new ConsoleLogReceiver() {Level = LogLevel.Debug});
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
|
||||||
builder.RootComponents.Add<App>("#app");
|
|
||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
|
||||||
await builder.Build().RunAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,35 @@
|
|||||||
|
@using Microsoft.AspNetCore.Identity
|
||||||
|
@using MultiShop.Server.Models
|
||||||
|
@inject SignInManager<ApplicationUser> SignInManager
|
||||||
|
@inject UserManager<ApplicationUser> UserManager
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
|
||||||
|
@{
|
||||||
|
var returnUrl = "/";
|
||||||
|
if (Context.Request.Query.TryGetValue("returnUrl", out var existingUrl)) {
|
||||||
|
returnUrl = existingUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
@if (SignInManager.IsSignedIn(User))
|
||||||
|
{
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/" method="post">
|
||||||
|
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register" asp-route-returnUrl="@returnUrl">Register</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login" asp-route-returnUrl="@returnUrl">Login</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MultiShop.Server.Controllers
|
||||||
|
{
|
||||||
|
public class OidcConfigurationController : Controller
|
||||||
|
{
|
||||||
|
private readonly ILogger<OidcConfigurationController> _logger;
|
||||||
|
|
||||||
|
public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider, ILogger<OidcConfigurationController> logger)
|
||||||
|
{
|
||||||
|
ClientRequestParametersProvider = clientRequestParametersProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IClientRequestParametersProvider ClientRequestParametersProvider { get; }
|
||||||
|
|
||||||
|
[HttpGet("_configuration/{clientId}")]
|
||||||
|
public IActionResult GetClientRequestParameters([FromRoute]string clientId)
|
||||||
|
{
|
||||||
|
var parameters = ClientRequestParametersProvider.GetClientParameters(HttpContext, clientId);
|
||||||
|
return Ok(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
src/MultiShop/Server/Controllers/ShopModulesController.cs
Normal file
42
src/MultiShop/Server/Controllers/ShopModulesController.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace MultiShop.Server.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("[controller]")]
|
||||||
|
public class ShopModulesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IConfiguration configuration;
|
||||||
|
private IDictionary<string, byte[]> shopAssemblyData;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public ShopModulesController(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.shopAssemblyData = new Dictionary<string, byte[]>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> GetShopModuleNames() {
|
||||||
|
foreach (string file in Directory.EnumerateFiles(configuration["ModulesDir"]))
|
||||||
|
{
|
||||||
|
if (Path.GetExtension(file).ToLower().Equals(".dll")) {
|
||||||
|
yield return Path.GetFileNameWithoutExtension(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{shopModuleName}")]
|
||||||
|
public ActionResult GetModule(string shopModuleName) {
|
||||||
|
string shopPath = Path.Join(configuration["ModulesDir"], shopModuleName);
|
||||||
|
if (!System.IO.File.Exists(shopPath)) return NotFound();
|
||||||
|
return File(new FileStream(shopPath, FileMode.Open), "application/shop-dll");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MultiShop.Shared;
|
||||||
|
|
||||||
|
namespace MultiShop.Server.Controllers
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
[Route("[controller]")]
|
||||||
|
public class WeatherForecastController : ControllerBase
|
||||||
|
{
|
||||||
|
private static readonly string[] Summaries = new[]
|
||||||
|
{
|
||||||
|
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly ILogger<WeatherForecastController> _logger;
|
||||||
|
|
||||||
|
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IEnumerable<WeatherForecast> Get()
|
||||||
|
{
|
||||||
|
var rng = new Random();
|
||||||
|
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||||
|
{
|
||||||
|
Date = DateTime.Now.AddDays(index),
|
||||||
|
TemperatureC = rng.Next(-20, 55),
|
||||||
|
Summary = Summaries[rng.Next(Summaries.Length)]
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
src/MultiShop/Server/Data/ApplicationDbContext.cs
Normal file
21
src/MultiShop/Server/Data/ApplicationDbContext.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using MultiShop.Server.Models;
|
||||||
|
using IdentityServer4.EntityFramework.Options;
|
||||||
|
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MultiShop.Server.Data
|
||||||
|
{
|
||||||
|
public class ApplicationDbContext : ApiAuthorizationDbContext<ApplicationUser>
|
||||||
|
{
|
||||||
|
public ApplicationDbContext(
|
||||||
|
DbContextOptions options,
|
||||||
|
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
373
src/MultiShop/Server/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs
generated
Normal file
373
src/MultiShop/Server/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs
generated
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using MultiShop.Server.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace MultiShop.Server.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("00000000000000_CreateIdentitySchema")]
|
||||||
|
partial class CreateIdentitySchema
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.0-rc.1.20417.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserCode")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeviceCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expiration")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserCode");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Expiration");
|
||||||
|
|
||||||
|
b.ToTable("DeviceCodes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ConsumedTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expiration")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("Expiration");
|
||||||
|
|
||||||
|
b.HasIndex("SubjectId", "ClientId", "Type");
|
||||||
|
|
||||||
|
b.HasIndex("SubjectId", "SessionId", "Type");
|
||||||
|
|
||||||
|
b.ToTable("PersistedGrants");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,288 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace MultiShop.Server.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class CreateIdentitySchema : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetRoles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||||
|
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||||
|
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUsers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||||
|
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||||
|
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||||
|
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||||
|
EmailConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
PhoneNumberConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
TwoFactorEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
LockoutEnd = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
|
||||||
|
LockoutEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DeviceCodes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserCode = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
DeviceCode = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
SubjectId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Expiration = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Data = table.Column<string>(type: "TEXT", maxLength: 50000, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DeviceCodes", x => x.UserCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PersistedGrants",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Key = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
Type = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
||||||
|
SubjectId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
ConsumedTime = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
Data = table.Column<string>(type: "TEXT", maxLength: 50000, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PersistedGrants", x => x.Key);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetRoleClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
RoleId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
ClaimValue = table.Column<string>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||||
|
column: x => x.RoleId,
|
||||||
|
principalTable: "AspNetRoles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
ClaimValue = table.Column<string>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserLogins",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
LoginProvider = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||||
|
ProviderKey = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||||
|
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
UserId = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserRoles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
RoleId = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||||
|
column: x => x.RoleId,
|
||||||
|
principalTable: "AspNetRoles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserTokens",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
LoginProvider = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||||
|
Value = table.Column<string>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetRoleClaims_RoleId",
|
||||||
|
table: "AspNetRoleClaims",
|
||||||
|
column: "RoleId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "RoleNameIndex",
|
||||||
|
table: "AspNetRoles",
|
||||||
|
column: "NormalizedName",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetUserClaims_UserId",
|
||||||
|
table: "AspNetUserClaims",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetUserLogins_UserId",
|
||||||
|
table: "AspNetUserLogins",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetUserRoles_RoleId",
|
||||||
|
table: "AspNetUserRoles",
|
||||||
|
column: "RoleId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "EmailIndex",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
column: "NormalizedEmail");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "UserNameIndex",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
column: "NormalizedUserName",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DeviceCodes_DeviceCode",
|
||||||
|
table: "DeviceCodes",
|
||||||
|
column: "DeviceCode",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DeviceCodes_Expiration",
|
||||||
|
table: "DeviceCodes",
|
||||||
|
column: "Expiration");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PersistedGrants_Expiration",
|
||||||
|
table: "PersistedGrants",
|
||||||
|
column: "Expiration");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PersistedGrants_SubjectId_ClientId_Type",
|
||||||
|
table: "PersistedGrants",
|
||||||
|
columns: new[] { "SubjectId", "ClientId", "Type" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PersistedGrants_SubjectId_SessionId_Type",
|
||||||
|
table: "PersistedGrants",
|
||||||
|
columns: new[] { "SubjectId", "SessionId", "Type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetRoleClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserLogins");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserRoles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserTokens");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DeviceCodes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PersistedGrants");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetRoles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,371 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using MultiShop.Server.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace MultiShop.Server.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.0-rc.1.20417.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserCode")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeviceCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expiration")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserCode");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Expiration");
|
||||||
|
|
||||||
|
b.ToTable("DeviceCodes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ConsumedTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expiration")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.HasIndex("Expiration");
|
||||||
|
|
||||||
|
b.HasIndex("SubjectId", "ClientId", "Type");
|
||||||
|
|
||||||
|
b.HasIndex("SubjectId", "SessionId", "Type");
|
||||||
|
|
||||||
|
b.ToTable("PersistedGrants");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/MultiShop/Server/Models/ApplicationUser.cs
Normal file
12
src/MultiShop/Server/Models/ApplicationUser.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MultiShop.Server.Models
|
||||||
|
{
|
||||||
|
public class ApplicationUser : IdentityUser
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
30
src/MultiShop/Server/MultiShop.Server.csproj
Normal file
30
src/MultiShop/Server/MultiShop.Server.csproj
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
|
<UserSecretsId>MultiShop.Server-24B2AA0A-FA57-4FB2-B70E-6546BC734726</UserSecretsId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="5.0.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Client\MultiShop.Client.csproj" />
|
||||||
|
<ProjectReference Include="..\Shared\MultiShop.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="5.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" Version="5.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
42
src/MultiShop/Server/Pages/Error.cshtml
Normal file
42
src/MultiShop/Server/Pages/Error.cshtml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
@page
|
||||||
|
@model MultiShop.Server.Pages.ErrorModel
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<title>Error</title>
|
||||||
|
<link href="~/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link href="~/css/app.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="main">
|
||||||
|
<div class="content px-4">
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
|
||||||
|
@if (Model.ShowRequestId)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Development Mode</h3>
|
||||||
|
<p>
|
||||||
|
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||||
|
It can result in displaying sensitive information from exceptions to end users.
|
||||||
|
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||||
|
and restarting the app.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
32
src/MultiShop/Server/Pages/Error.cshtml.cs
Normal file
32
src/MultiShop/Server/Pages/Error.cshtml.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MultiShop.Server.Pages
|
||||||
|
{
|
||||||
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||||
|
[IgnoreAntiforgeryToken]
|
||||||
|
public class ErrorModel : PageModel
|
||||||
|
{
|
||||||
|
public string RequestId { get; set; }
|
||||||
|
|
||||||
|
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
|
||||||
|
private readonly ILogger<ErrorModel> _logger;
|
||||||
|
|
||||||
|
public ErrorModel(ILogger<ErrorModel> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/MultiShop/Server/Program.cs
Normal file
26
src/MultiShop/Server/Program.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MultiShop.Server
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
CreateHostBuilder(args).Build().Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||||
|
Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
|
{
|
||||||
|
webBuilder.UseStartup<Startup>();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
30
src/MultiShop/Server/Properties/launchSettings.json
Normal file
30
src/MultiShop/Server/Properties/launchSettings.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:5738",
|
||||||
|
"sslPort": 44353
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MultiShop.Server": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": "true",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
src/MultiShop/Server/Startup.cs
Normal file
82
src/MultiShop/Server/Startup.cs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.HttpsPolicy;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using System.Linq;
|
||||||
|
using MultiShop.Server.Data;
|
||||||
|
using MultiShop.Server.Models;
|
||||||
|
|
||||||
|
namespace MultiShop.Server
|
||||||
|
{
|
||||||
|
public class Startup
|
||||||
|
{
|
||||||
|
public Startup(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
Configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfiguration Configuration { get; }
|
||||||
|
|
||||||
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
|
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
|
options.UseSqlite(
|
||||||
|
Configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
|
services.AddDatabaseDeveloperPageExceptionFilter();
|
||||||
|
|
||||||
|
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
|
||||||
|
.AddEntityFrameworkStores<ApplicationDbContext>();
|
||||||
|
|
||||||
|
services.AddIdentityServer()
|
||||||
|
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
|
||||||
|
|
||||||
|
services.AddAuthentication()
|
||||||
|
.AddIdentityServerJwt();
|
||||||
|
|
||||||
|
services.AddControllersWithViews();
|
||||||
|
services.AddRazorPages();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.UseMigrationsEndPoint();
|
||||||
|
app.UseWebAssemblyDebugging();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Error");
|
||||||
|
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||||
|
app.UseHsts();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
app.UseBlazorFrameworkFiles();
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseIdentityServer();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapRazorPages();
|
||||||
|
endpoints.MapControllers();
|
||||||
|
endpoints.MapFallbackToFile("index.html");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
src/MultiShop/Server/app.db
Normal file
BIN
src/MultiShop/Server/app.db
Normal file
Binary file not shown.
14
src/MultiShop/Server/appsettings.Development.json
Normal file
14
src/MultiShop/Server/appsettings.Development.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IdentityServer": {
|
||||||
|
"Key": {
|
||||||
|
"Type": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
src/MultiShop/Server/appsettings.json
Normal file
21
src/MultiShop/Server/appsettings.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"ModulesDir": "modules",
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "DataSource=app.db"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IdentityServer": {
|
||||||
|
"Clients": {
|
||||||
|
"MultiShop.Client": {
|
||||||
|
"Profile": "IdentityServerSPA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
BIN
src/MultiShop/Server/modules/MultiShop.Shop.AliExpressModule.dll
Normal file
BIN
src/MultiShop/Server/modules/MultiShop.Shop.AliExpressModule.dll
Normal file
Binary file not shown.
BIN
src/MultiShop/Server/modules/MultiShop.Shop.BanggoodModule.dll
Normal file
BIN
src/MultiShop/Server/modules/MultiShop.Shop.BanggoodModule.dll
Normal file
Binary file not shown.
@ -1,98 +0,0 @@
|
|||||||
@using DataStructures
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-top-borderless">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Name</th>
|
|
||||||
<th scope="col">Price</th>
|
|
||||||
<th scope="col">Shipping</th>
|
|
||||||
<th scope="col">Purchases</th>
|
|
||||||
<th scope="col">Rating</th>
|
|
||||||
<th scope="col">Reviews</th>
|
|
||||||
<th scope="col"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<Virtualize Items="@Products" Context="product">
|
|
||||||
<tr>
|
|
||||||
<th scope="row" @key="product.Listing">
|
|
||||||
<div class="text-truncate">@product.Listing.Name</div>
|
|
||||||
<small>From @product.ShopName</small>
|
|
||||||
@if (product.Listing.ConvertedPrices)
|
|
||||||
{
|
|
||||||
<span class="ml-3 mr-1 badge badge-warning">Converted price</span>
|
|
||||||
}
|
|
||||||
@foreach (ResultsProfile.Category c in product.Tops)
|
|
||||||
{
|
|
||||||
<span class="mx-1 badge badge-primary">@CategoryTags(c)</span>
|
|
||||||
}
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
@if (product.Listing.UpperPrice != product.Listing.LowerPrice)
|
|
||||||
{
|
|
||||||
<div class="text-truncate">
|
|
||||||
@product.Listing.LowerPrice to @product.Listing.UpperPrice
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="text-truncate">
|
|
||||||
@GetOrNA(product.Listing.LowerPrice)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="text-truncate">
|
|
||||||
@GetOrNA(product.Listing.Shipping)
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="text-truncate">
|
|
||||||
@GetOrNA(product.Listing.PurchaseCount)
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="text-truncate">
|
|
||||||
@(product.Listing.Rating != null ? string.Format("{0:P2}", product.Listing.Rating) : "N/A")
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>@GetOrNA(product.Listing.ReviewCount)</td>
|
|
||||||
<td>
|
|
||||||
<a href="@product.Listing.URL" class="btn btn-primary" target="_blank">View</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</Virtualize>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public List<ProductListingInfo> Products { get; set; }
|
|
||||||
|
|
||||||
private string PriceCellHeight { get => "height: " + "4rem"; }
|
|
||||||
|
|
||||||
private string GetOrNA(object data, string prepend = null, string append = null)
|
|
||||||
{
|
|
||||||
return data != null ? (prepend + data.ToString() + append) : "N/A";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string CategoryTags(ResultsProfile.Category c)
|
|
||||||
{
|
|
||||||
switch (c)
|
|
||||||
{
|
|
||||||
case ResultsProfile.Category.RatingPriceRatio:
|
|
||||||
return "Best rating to price ratio";
|
|
||||||
case ResultsProfile.Category.Price:
|
|
||||||
return "Lowest price";
|
|
||||||
case ResultsProfile.Category.Purchases:
|
|
||||||
return "Most purchases";
|
|
||||||
case ResultsProfile.Category.Reviews:
|
|
||||||
return "Most reviews";
|
|
||||||
}
|
|
||||||
throw new ArgumentException($"{c} does not have an associated string.");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
@using ShopFramework
|
|
||||||
@using SimpleLogger
|
|
||||||
@using System.Reflection
|
|
||||||
@using Microsoft.Extensions.Configuration
|
|
||||||
@inject HttpClient Http
|
|
||||||
@inject IConfiguration Configuration
|
|
||||||
@inherits LayoutComponentBase
|
|
||||||
@implements IDisposable
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
<NavMenu />
|
|
||||||
<div class="main">
|
|
||||||
<div class="content px-4">
|
|
||||||
@if (modulesLoaded)
|
|
||||||
{
|
|
||||||
<CascadingValue Value="shops" Name="Shops">
|
|
||||||
@Body
|
|
||||||
</CascadingValue>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private bool modulesLoaded = false;
|
|
||||||
|
|
||||||
private Dictionary<string, IShop> shops = new Dictionary<string, IShop>();
|
|
||||||
private Dictionary<string, Assembly> unusedDependencies = new Dictionary<string, Assembly>();
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
await DownloadShopModules();
|
|
||||||
await base.OnInitializedAsync();
|
|
||||||
}
|
|
||||||
private async Task DownloadShopModules() {
|
|
||||||
Logger.Log($"Fetching shop modules.", LogLevel.Debug);
|
|
||||||
string[] shopNames = await Http.GetFromJsonAsync<string[]>(Configuration["ModulesList"]);
|
|
||||||
Task<byte[]>[] assemblyDownloadTasks = new Task<byte[]>[shopNames.Length];
|
|
||||||
|
|
||||||
for (int i = 0; i < shopNames.Length; i++)
|
|
||||||
{
|
|
||||||
string shopPath = Configuration["ModulesDir"] + shopNames[i] + ".dll";
|
|
||||||
assemblyDownloadTasks[i] = Http.GetByteArrayAsync(shopPath);
|
|
||||||
Logger.Log($"Downloading \"{shopPath}\".", LogLevel.Debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyDependencyRequest;
|
|
||||||
|
|
||||||
foreach (Task<byte[]> task in assemblyDownloadTasks)
|
|
||||||
{
|
|
||||||
Assembly assembly = AppDomain.CurrentDomain.Load(await task);
|
|
||||||
bool assigned = false;
|
|
||||||
foreach (Type type in assembly.GetTypes())
|
|
||||||
{
|
|
||||||
if (typeof(IShop).IsAssignableFrom(type)) {
|
|
||||||
IShop shop = Activator.CreateInstance(type) as IShop;
|
|
||||||
if (shop != null) {
|
|
||||||
shop.Initialize();
|
|
||||||
shops.Add(shop.ShopName, shop);
|
|
||||||
Logger.Log($"Registered and started lifetime of module for \"{shop.ShopName}\".", LogLevel.Debug);
|
|
||||||
}
|
|
||||||
assigned = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!assigned) {
|
|
||||||
unusedDependencies.Add(assembly.FullName, assembly);
|
|
||||||
Logger.Log($"Assembly \"{assembly.FullName}\" did not contain a shop module. Storing it as potential extension.", LogLevel.Debug);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (string assembly in unusedDependencies.Keys)
|
|
||||||
{
|
|
||||||
Logger.Log($"{assembly} was unused.", LogLevel.Warning);
|
|
||||||
}
|
|
||||||
unusedDependencies.Clear();
|
|
||||||
modulesLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Assembly OnAssemblyDependencyRequest(object sender, ResolveEventArgs args) {
|
|
||||||
Logger.Log($"Assembly {args.RequestingAssembly} is requesting dependency assembly {args.Name}.", LogLevel.Debug);
|
|
||||||
if (unusedDependencies.ContainsKey(args.Name))
|
|
||||||
{
|
|
||||||
Logger.Log("Dependency found.", LogLevel.Debug);
|
|
||||||
Assembly dependency = unusedDependencies[args.Name];
|
|
||||||
unusedDependencies.Remove(args.Name);
|
|
||||||
return dependency;
|
|
||||||
}
|
|
||||||
Logger.Log($"No dependency under name {args.Name}", LogLevel.Debug);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
foreach (string name in shops.Keys)
|
|
||||||
{
|
|
||||||
shops[name].Dispose();
|
|
||||||
Logger.Log($"Ending lifetime of shop module for \"{name}\".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
14
src/MultiShop/Shared/MultiShop.Shared.csproj
Normal file
14
src/MultiShop/Shared/MultiShop.Shared.csproj
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<SupportedPlatform Include="browser" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\MultiShop.Shop\Framework\MultiShop.Shop.Framework.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
@ -1,7 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using MultiShop.ShopFramework;
|
using MultiShop.Shop.Framework;
|
||||||
|
|
||||||
namespace MultiShop.DataStructures
|
namespace MultiShop.Shared
|
||||||
{
|
{
|
||||||
public class ProductListingInfo
|
public class ProductListingInfo
|
||||||
{
|
{
|
@ -1,8 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
|
||||||
using MultiShop.ShopFramework;
|
|
||||||
|
|
||||||
namespace MultiShop.DataStructures
|
namespace MultiShop.Shared
|
||||||
{
|
{
|
||||||
public static class ResultCategoryExtensions
|
public static class ResultCategoryExtensions
|
||||||
{
|
{
|
@ -2,7 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace MultiShop.DataStructures
|
namespace MultiShop.Shared
|
||||||
{
|
{
|
||||||
public class ResultsProfile
|
public class ResultsProfile
|
||||||
{
|
{
|
@ -1,7 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using MultiShop.ShopFramework;
|
using MultiShop.Shop.Framework;
|
||||||
|
|
||||||
namespace MultiShop.DataStructures
|
namespace MultiShop.Shared
|
||||||
{
|
{
|
||||||
public class SearchProfile
|
public class SearchProfile
|
||||||
{
|
{
|
15
src/MultiShop/Shared/WeatherForecast.cs
Normal file
15
src/MultiShop/Shared/WeatherForecast.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MultiShop.Shared
|
||||||
|
{
|
||||||
|
public class WeatherForecast
|
||||||
|
{
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
|
||||||
|
public int TemperatureC { get; set; }
|
||||||
|
|
||||||
|
public string Summary { get; set; }
|
||||||
|
|
||||||
|
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"LogLevel" : "Debug",
|
|
||||||
"ModulesDir" : "modules/",
|
|
||||||
"ModulesList" : "modules/modules_content.json"
|
|
||||||
}
|
|
Binary file not shown.
Binary file not shown.
@ -1,14 +0,0 @@
|
|||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
modulepaths = []
|
|
||||||
|
|
||||||
for content in os.listdir(os.getcwd()):
|
|
||||||
components = os.path.splitext(content)
|
|
||||||
if (os.path.isfile(content) and components[1] == ".dll"):
|
|
||||||
print("Adding \"{0}\" to list of modules.".format(components[0]))
|
|
||||||
modulepaths.append(components[0])
|
|
||||||
|
|
||||||
file = open("modules_content.json", "w")
|
|
||||||
json.dump(modulepaths, file, sort_keys=True, indent=4)
|
|
||||||
file.close()
|
|
@ -1,5 +0,0 @@
|
|||||||
[
|
|
||||||
"HtmlAgilityPack",
|
|
||||||
"AliExpressShop",
|
|
||||||
"BanggoodShop"
|
|
||||||
]
|
|
@ -20,8 +20,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\AliExpressShop\AliExpressShop.csproj" />
|
<ProjectReference Include="..\..\..\SimpleLogger\SimpleLogger.csproj" />
|
||||||
<ProjectReference Include="..\..\SimpleLogger\SimpleLogger.csproj" />
|
<ProjectReference Include="..\..\..\src\MultiShop.Shop\AliExpressModule\MultiShop.Shop.AliExpressModule.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
@ -1,9 +1,9 @@
|
|||||||
using MultiShop.ShopFramework;
|
using MultiShop.Shop.Framework;
|
||||||
using SimpleLogger;
|
using SimpleLogger;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace AliExpressShop.Tests
|
namespace MultiShop.Shop.AliExpressModule.Tests
|
||||||
{
|
{
|
||||||
public class ShopTest
|
public class ShopTest
|
||||||
{
|
{
|
@ -2,7 +2,7 @@ using System;
|
|||||||
using SimpleLogger;
|
using SimpleLogger;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace AliExpressShop
|
namespace MultiShop.Shop.AliExpressModule
|
||||||
{
|
{
|
||||||
public class XUnitLogger : ILogReceiver
|
public class XUnitLogger : ILogReceiver
|
||||||
{
|
{
|
@ -20,8 +20,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\BanggoodShop\BanggoodShop.csproj" />
|
<ProjectReference Include="..\..\..\src\MultiShop.Shop\BanggoodModule\MultiShop.Shop.BanggoodModule.csproj" />
|
||||||
<ProjectReference Include="..\..\SimpleLogger\SimpleLogger.csproj" />
|
<ProjectReference Include="..\..\..\SimpleLogger\SimpleLogger.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
@ -1,9 +1,9 @@
|
|||||||
using MultiShop.ShopFramework;
|
using MultiShop.Shop.Framework;
|
||||||
using SimpleLogger;
|
using SimpleLogger;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace BanggoodShop.Tests
|
namespace MultiShop.Shop.BanggoodModule.Tests
|
||||||
{
|
{
|
||||||
public class ShopTest
|
public class ShopTest
|
||||||
{
|
{
|
||||||
@ -12,7 +12,6 @@ namespace BanggoodShop.Tests
|
|||||||
Logger.AddLogListener(new XUnitLogger(output));
|
Logger.AddLogListener(new XUnitLogger(output));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void Search_CAD_ResultsFound()
|
public async void Search_CAD_ResultsFound()
|
||||||
{
|
{
|
@ -2,7 +2,7 @@ using System;
|
|||||||
using SimpleLogger;
|
using SimpleLogger;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace BanggoodShop
|
namespace MultiShop.Shop.BanggoodModule
|
||||||
{
|
{
|
||||||
public class XUnitLogger : ILogReceiver
|
public class XUnitLogger : ILogReceiver
|
||||||
{
|
{
|
Loading…
Reference in New Issue
Block a user