Changed to hosted blazorwasm project.

Restructured project following changes.

Moved shop assembly fetching to public facing Web API.
This commit is contained in:
Harrison Deng 2021-05-21 13:32:25 -05:00
parent e07b234eb2
commit 6c684372df
91 changed files with 2153 additions and 478 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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
{

View File

@ -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>
{

View File

@ -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>

View File

@ -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
{

View File

@ -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>
{

View File

@ -1,4 +1,4 @@
namespace MultiShop.ShopFramework
namespace MultiShop.Shop.Framework
{
public enum Currency
{

View File

@ -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
{

View File

@ -1,4 +1,4 @@
namespace MultiShop.ShopFramework
namespace MultiShop.Shop.Framework
{
public struct ProductListing
{

View File

@ -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>

View 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>

View 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}\".");
}
}
}
}

View File

@ -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>

View File

@ -0,0 +1,7 @@
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />
@code{
[Parameter] public string Action { get; set; }
}

View 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++;
}
}

View 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();
}
}
}

View File

@ -1,5 +1,5 @@
@page "/info"
@using ShopFramework
@using Shop.Framework
<div>

View File

@ -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();
}
}

View 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.");
}
}
}

View 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();
}
}
}

View File

@ -3,8 +3,8 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:46072",
"sslPort": 44317
"applicationUrl": "http://localhost:5738",
"sslPort": 44353
}
},
"profiles": {

View 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");
}
}

View File

@ -0,0 +1,10 @@
@inherits LayoutComponentBase
<div class="page">
<NavMenu />
<div class="main">
<div class="content px-4">
@Body
</div>
</div>
</div>

View File

@ -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">

View 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)}");
}
}

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 255 B

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -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>

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View 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");
}
}
}

View File

@ -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();
}
}
}

View 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)
{
}
}
}

View 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
}
}
}

View File

@ -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");
}
}
}

View File

@ -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
}
}
}

View 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
{
}
}

View 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>

View 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>

View 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;
}
}
}

View 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>();
});
}
}

View 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"
}
}
}
}

View 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

Binary file not shown.

View File

@ -0,0 +1,14 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"IdentityServer": {
"Key": {
"Type": "Development"
}
}
}

View 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": "*"
}

View File

@ -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.");
}
}

View File

@ -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}\".");
}
}
}

View 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>

View File

@ -1,7 +1,7 @@
using System.Collections.Generic;
using MultiShop.ShopFramework;
using MultiShop.Shop.Framework;
namespace MultiShop.DataStructures
namespace MultiShop.Shared
{
public class ProductListingInfo
{

View File

@ -1,8 +1,6 @@
using System;
using System.ComponentModel;
using MultiShop.ShopFramework;
namespace MultiShop.DataStructures
namespace MultiShop.Shared
{
public static class ResultCategoryExtensions
{

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace MultiShop.DataStructures
namespace MultiShop.Shared
{
public class ResultsProfile
{

View File

@ -1,7 +1,7 @@
using System.Collections.Generic;
using MultiShop.ShopFramework;
using MultiShop.Shop.Framework;
namespace MultiShop.DataStructures
namespace MultiShop.Shared
{
public class SearchProfile
{

View 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);
}
}

View File

@ -1,5 +0,0 @@
{
"LogLevel" : "Debug",
"ModulesDir" : "modules/",
"ModulesList" : "modules/modules_content.json"
}

View File

@ -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()

View File

@ -1,5 +0,0 @@
[
"HtmlAgilityPack",
"AliExpressShop",
"BanggoodShop"
]

View File

@ -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>

View File

@ -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
{

View File

@ -2,7 +2,7 @@ using System;
using SimpleLogger;
using Xunit.Abstractions;
namespace AliExpressShop
namespace MultiShop.Shop.AliExpressModule
{
public class XUnitLogger : ILogReceiver
{

View File

@ -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>

View File

@ -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()
{

View File

@ -2,7 +2,7 @@ using System;
using SimpleLogger;
using Xunit.Abstractions;
namespace BanggoodShop
namespace MultiShop.Shop.BanggoodModule
{
public class XUnitLogger : ILogReceiver
{