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.Tasks;
|
||||
using GameServiceWarden.Core.Collection;
|
||||
using MultiShop.ShopFramework;
|
||||
using MultiShop.Shop.Framework;
|
||||
using SimpleLogger;
|
||||
|
||||
namespace AliExpressShop
|
||||
namespace MultiShop.Shop.AliExpressModule
|
||||
{
|
||||
public class Shop : IShop
|
||||
{
|
@ -7,10 +7,10 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GameServiceWarden.Core.Collection;
|
||||
using MultiShop.ShopFramework;
|
||||
using MultiShop.Shop.Framework;
|
||||
using SimpleLogger;
|
||||
|
||||
namespace AliExpressShop
|
||||
namespace MultiShop.Shop.AliExpressModule
|
||||
{
|
||||
class ShopEnumerator : IAsyncEnumerator<ProductListing>
|
||||
{
|
@ -1,8 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MultiShop.ShopFramework\MultiShop.ShopFramework.csproj" />
|
||||
<ProjectReference Include="..\..\SimpleLogger\SimpleLogger.csproj" />
|
||||
<ProjectReference Include="..\Framework\MultiShop.Shop.Framework.csproj" />
|
||||
<ProjectReference Include="..\..\..\SimpleLogger\SimpleLogger.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
@ -3,9 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using MultiShop.ShopFramework;
|
||||
using MultiShop.Shop.Framework;
|
||||
|
||||
namespace BanggoodShop
|
||||
namespace MultiShop.Shop.BanggoodModule
|
||||
{
|
||||
public class Shop : IShop
|
||||
{
|
@ -9,10 +9,10 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using HtmlAgilityPack;
|
||||
using MultiShop.ShopFramework;
|
||||
using MultiShop.Shop.Framework;
|
||||
using SimpleLogger;
|
||||
|
||||
namespace BanggoodShop
|
||||
namespace MultiShop.Shop.BanggoodModule
|
||||
{
|
||||
class ShopEnumerator : IAsyncEnumerator<ProductListing>
|
||||
{
|
@ -1,4 +1,4 @@
|
||||
namespace MultiShop.ShopFramework
|
||||
namespace MultiShop.Shop.Framework
|
||||
{
|
||||
public enum Currency
|
||||
{
|
@ -4,7 +4,7 @@ using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MultiShop.ShopFramework
|
||||
namespace MultiShop.Shop.Framework
|
||||
{
|
||||
public interface IShop : IAsyncEnumerable<ProductListing>, IDisposable
|
||||
{
|
@ -1,4 +1,4 @@
|
||||
namespace MultiShop.ShopFramework
|
||||
namespace MultiShop.Shop.Framework
|
||||
{
|
||||
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>
|
||||
<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.Authentication" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||
<PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MultiShop.ShopFramework\MultiShop.ShopFramework.csproj" />
|
||||
<ProjectReference Include="..\..\SimpleLogger\SimpleLogger.csproj" />
|
||||
<ProjectReference Include="..\Shared\MultiShop.Shared.csproj" />
|
||||
<ProjectReference Include="..\..\MultiShop.Shop\Framework\MultiShop.Shop.Framework.csproj" />
|
||||
<ProjectReference Include="..\..\..\SimpleLogger\SimpleLogger.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</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"
|
||||
@using ShopFramework
|
||||
@using Shop.Framework
|
||||
|
||||
<div>
|
||||
|
@ -1,13 +1,10 @@
|
||||
@page "/search/{Query?}"
|
||||
@using Microsoft.Extensions.Configuration
|
||||
@using ShopFramework
|
||||
@using SimpleLogger
|
||||
@using DataStructures
|
||||
@using MultiShop.Shared
|
||||
@inject HttpClient Http
|
||||
@inject IConfiguration Configuration
|
||||
@inject IJSRuntime js
|
||||
|
||||
@* TODO: Split C# code into a partial class. *@
|
||||
<div class="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">
|
||||
@ -242,171 +239,76 @@
|
||||
</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>
|
||||
|
||||
|
||||
@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,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:46072",
|
||||
"sslPort": 44317
|
||||
"applicationUrl": "http://localhost:5738",
|
||||
"sslPort": 44353
|
||||
}
|
||||
},
|
||||
"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">
|
||||
<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
|
||||
</a>
|
||||
<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.Json
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using MultiShop
|
||||
@using MultiShop.Shared
|
||||
@using MultiShop.Client
|
||||
@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="/" />
|
||||
<link href="css/bootstrap/bootstrap.min.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>
|
||||
|
||||
<body>
|
||||
@ -19,8 +19,10 @@
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
<script src="js/ComponentsSupport.js"></script>
|
||||
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
@ -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 MultiShop.ShopFramework;
|
||||
using MultiShop.Shop.Framework;
|
||||
|
||||
namespace MultiShop.DataStructures
|
||||
namespace MultiShop.Shared
|
||||
{
|
||||
public class ProductListingInfo
|
||||
{
|
@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using MultiShop.ShopFramework;
|
||||
|
||||
namespace MultiShop.DataStructures
|
||||
namespace MultiShop.Shared
|
||||
{
|
||||
public static class ResultCategoryExtensions
|
||||
{
|
@ -2,7 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MultiShop.DataStructures
|
||||
namespace MultiShop.Shared
|
||||
{
|
||||
public class ResultsProfile
|
||||
{
|
@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using MultiShop.ShopFramework;
|
||||
using MultiShop.Shop.Framework;
|
||||
|
||||
namespace MultiShop.DataStructures
|
||||
namespace MultiShop.Shared
|
||||
{
|
||||
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>
|
||||
<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>
|
||||
|
||||
</Project>
|
@ -1,9 +1,9 @@
|
||||
using MultiShop.ShopFramework;
|
||||
using MultiShop.Shop.Framework;
|
||||
using SimpleLogger;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace AliExpressShop.Tests
|
||||
namespace MultiShop.Shop.AliExpressModule.Tests
|
||||
{
|
||||
public class ShopTest
|
||||
{
|
@ -2,7 +2,7 @@ using System;
|
||||
using SimpleLogger;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace AliExpressShop
|
||||
namespace MultiShop.Shop.AliExpressModule
|
||||
{
|
||||
public class XUnitLogger : ILogReceiver
|
||||
{
|
@ -20,8 +20,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\BanggoodShop\BanggoodShop.csproj" />
|
||||
<ProjectReference Include="..\..\SimpleLogger\SimpleLogger.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\MultiShop.Shop\BanggoodModule\MultiShop.Shop.BanggoodModule.csproj" />
|
||||
<ProjectReference Include="..\..\..\SimpleLogger\SimpleLogger.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -1,9 +1,9 @@
|
||||
using MultiShop.ShopFramework;
|
||||
using MultiShop.Shop.Framework;
|
||||
using SimpleLogger;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BanggoodShop.Tests
|
||||
namespace MultiShop.Shop.BanggoodModule.Tests
|
||||
{
|
||||
public class ShopTest
|
||||
{
|
||||
@ -12,7 +12,6 @@ namespace BanggoodShop.Tests
|
||||
Logger.AddLogListener(new XUnitLogger(output));
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async void Search_CAD_ResultsFound()
|
||||
{
|
@ -2,7 +2,7 @@ using System;
|
||||
using SimpleLogger;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BanggoodShop
|
||||
namespace MultiShop.Shop.BanggoodModule
|
||||
{
|
||||
public class XUnitLogger : ILogReceiver
|
||||
{
|
Loading…
Reference in New Issue
Block a user