Skip to content

Commit

Permalink
Initial state
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveSandersonMS committed May 19, 2020
0 parents commit 5897e8b
Show file tree
Hide file tree
Showing 93 changed files with 7,288 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
bin/
obj/
.vs/
*.csproj.user
Server/app.db
64 changes: 64 additions & 0 deletions CarChecker.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.0.0
MinimumVisualStudioVersion = 16.0.0.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarChecker.Server", "Server\CarChecker.Server.csproj", "{28148247-170D-49AE-A367-7984410446CF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarChecker.Client", "Client\CarChecker.Client.csproj", "{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarChecker.Shared", "Shared\CarChecker.Shared.csproj", "{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{28148247-170D-49AE-A367-7984410446CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28148247-170D-49AE-A367-7984410446CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28148247-170D-49AE-A367-7984410446CF}.Debug|x64.ActiveCfg = Debug|Any CPU
{28148247-170D-49AE-A367-7984410446CF}.Debug|x64.Build.0 = Debug|Any CPU
{28148247-170D-49AE-A367-7984410446CF}.Debug|x86.ActiveCfg = Debug|Any CPU
{28148247-170D-49AE-A367-7984410446CF}.Debug|x86.Build.0 = Debug|Any CPU
{28148247-170D-49AE-A367-7984410446CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28148247-170D-49AE-A367-7984410446CF}.Release|Any CPU.Build.0 = Release|Any CPU
{28148247-170D-49AE-A367-7984410446CF}.Release|x64.ActiveCfg = Release|Any CPU
{28148247-170D-49AE-A367-7984410446CF}.Release|x64.Build.0 = Release|Any CPU
{28148247-170D-49AE-A367-7984410446CF}.Release|x86.ActiveCfg = Release|Any CPU
{28148247-170D-49AE-A367-7984410446CF}.Release|x86.Build.0 = Release|Any CPU
{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Debug|x64.ActiveCfg = Debug|Any CPU
{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Debug|x64.Build.0 = Debug|Any CPU
{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Debug|x86.ActiveCfg = Debug|Any CPU
{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Debug|x86.Build.0 = Debug|Any CPU
{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Release|Any CPU.Build.0 = Release|Any CPU
{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Release|x64.ActiveCfg = Release|Any CPU
{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Release|x64.Build.0 = Release|Any CPU
{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Release|x86.ActiveCfg = Release|Any CPU
{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Release|x86.Build.0 = Release|Any CPU
{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Debug|x64.ActiveCfg = Debug|Any CPU
{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Debug|x64.Build.0 = Debug|Any CPU
{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Debug|x86.ActiveCfg = Debug|Any CPU
{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Debug|x86.Build.0 = Debug|Any CPU
{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Release|Any CPU.Build.0 = Release|Any CPU
{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Release|x64.ActiveCfg = Release|Any CPU
{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Release|x64.Build.0 = Release|Any CPU
{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Release|x86.ActiveCfg = Release|Any CPU
{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C511E458-9B4D-4D16-AB90-69DC35B0F57B}
EndGlobalSection
EndGlobal
30 changes: 30 additions & 0 deletions Client/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData">
<Authorizing>
<LayoutView Layout="@typeof(HeaderLayout)">
<div class="loader"></div>
</LayoutView>
</Authorizing>
<NotAuthorized>
@if (!context.User.Identity.IsAuthenticated)
{
<NotLoggedIn />
}
else
{
<LayoutView Layout="@typeof(HeaderLayout)">
<p>You are not authorized to access this resource.</p>
</LayoutView>
}
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(HeaderLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
38 changes: 38 additions & 0 deletions Client/CarChecker.Client.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BlazorInputFile" Version="0.2.0" />
<PackageReference Include="SceneViewer" Version="0.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.DataAnnotations.Validation" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="3.2.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.3" />
<PackageReference Include="System.Net.Http.Json" Version="3.2.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Shared\CarChecker.Shared.csproj" />
</ItemGroup>

<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>

<ItemGroup>
<Compile Update="Resources\App.Designer.cs" DesignTime="True" AutoGen="True" DependentUpon="App.resx" />
<EmbeddedResource Update="Resources\App.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>App.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>

</Project>
26 changes: 26 additions & 0 deletions Client/Data/DataUrl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.IO;

namespace CarChecker.Client.Data
{
public static class DataUrl
{
public static string ToDataUrl(this MemoryStream data, string format)
{
var span = new Span<byte>(data.GetBuffer()).Slice(0, (int)data.Length);
return $"data:{format};base64,{Convert.ToBase64String(span)}";
}

public static byte[] ToBytes(string url)
{
var commaPos = url.IndexOf(',');
if (commaPos >= 0)
{
var base64 = url.Substring(commaPos + 1);
return Convert.FromBase64String(base64);
}

return null;
}
}
}
102 changes: 102 additions & 0 deletions Client/Data/LocalVehiclesStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Threading.Tasks;
using CarChecker.Shared;
using Microsoft.JSInterop;

namespace CarChecker.Client.Data
{
// To support offline use, we use this simple local data repository
// instead of performing data access directly against the server.
// This would not be needed if we assumed that network access was always
// available.

public class LocalVehiclesStore
{
private readonly HttpClient httpClient;
private readonly IJSRuntime js;

public LocalVehiclesStore(HttpClient httpClient, IJSRuntime js)
{
this.httpClient = httpClient;
this.js = js;
}

public ValueTask<Vehicle[]> GetOutstandingLocalEditsAsync()
{
return js.InvokeAsync<Vehicle[]>(
"localVehicleStore.getAll", "localedits");
}

public async Task SynchronizeAsync()
{
// If there are local edits, always send them first
foreach (var editedVehicle in await GetOutstandingLocalEditsAsync())
{
(await httpClient.PutAsJsonAsync("api/vehicle/details", editedVehicle)).EnsureSuccessStatusCode();
await DeleteAsync("localedits", editedVehicle.LicenseNumber);
}

await FetchChangesAsync();
}

public ValueTask SaveUserAccountAsync(ClaimsPrincipal user)
{
return user != null
? PutAsync("metadata", "userAccount", user.Claims.Select(c => new ClaimData { Type = c.Type, Value = c.Value }))
: DeleteAsync("metadata", "userAccount");
}

public async Task<ClaimsPrincipal> LoadUserAccountAsync()
{
var storedClaims = await GetAsync<ClaimData[]>("metadata", "userAccount");
return storedClaims != null
? new ClaimsPrincipal(new ClaimsIdentity(storedClaims.Select(c => new Claim(c.Type, c.Value)), "appAuth"))
: new ClaimsPrincipal(new ClaimsIdentity());
}

public ValueTask<string[]> Autocomplete(string prefix)
=> js.InvokeAsync<string[]>("localVehicleStore.autocompleteKeys", "serverdata", prefix, 5);

// If there's an outstanding local edit, use that. If not, use the server data.
public async Task<Vehicle> GetVehicle(string licenseNumber)
=> await GetAsync<Vehicle>("localedits", licenseNumber)
?? await GetAsync<Vehicle>("serverdata", licenseNumber);

public async ValueTask<DateTime?> GetLastUpdateDateAsync()
{
var value = await GetAsync<string>("metadata", "lastUpdateDate");
return value == null ? (DateTime?)null : DateTime.Parse(value);
}

public ValueTask SaveVehicleAsync(Vehicle vehicle)
=> PutAsync("localedits", null, vehicle);

async Task FetchChangesAsync()
{
var mostRecentlyUpdated = await js.InvokeAsync<Vehicle>("localVehicleStore.getFirstFromIndex", "serverdata", "lastUpdated", "prev");
var since = mostRecentlyUpdated?.LastUpdated ?? DateTime.MinValue;
var json = await httpClient.GetStringAsync($"api/vehicle/changedvehicles?since={since:o}");
await js.InvokeVoidAsync("localVehicleStore.putAllFromJson", "serverdata", json);
await PutAsync("metadata", "lastUpdateDate", DateTime.Now.ToString("o"));
}

ValueTask<T> GetAsync<T>(string storeName, object key)
=> js.InvokeAsync<T>("localVehicleStore.get", storeName, key);

ValueTask PutAsync<T>(string storeName, object key, T value)
=> js.InvokeVoidAsync("localVehicleStore.put", storeName, key, value);

ValueTask DeleteAsync(string storeName, object key)
=> js.InvokeVoidAsync("localVehicleStore.delete", storeName, key);

class ClaimData
{
public string Type { get; set; }
public string Value { get; set; }
}
}
}
36 changes: 36 additions & 0 deletions Client/Data/OfflineAccountClaimsPrincipalFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Security.Claims;
using System.Threading.Tasks;

namespace CarChecker.Client.Data
{
public class OfflineAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
private readonly IServiceProvider services;

public OfflineAccountClaimsPrincipalFactory(IServiceProvider services, IAccessTokenProviderAccessor accessor) : base(accessor)
{
this.services = services;
}

public override async ValueTask<ClaimsPrincipal> CreateUserAsync(RemoteUserAccount account, RemoteAuthenticationUserOptions options)
{
var localVehiclesStore = services.GetRequiredService<LocalVehiclesStore>();

var result = await base.CreateUserAsync(account, options);
if (result.Identity.IsAuthenticated)
{
await localVehiclesStore.SaveUserAccountAsync(result);
}
else
{
result = await localVehiclesStore.LoadUserAccountAsync();
}

return result;
}
}
}
16 changes: 16 additions & 0 deletions Client/Data/UserExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Security.Claims;

namespace CarChecker.Client.Data
{
public static class UserExtensions
{
public static string FirstName(this ClaimsPrincipal user)
=> user.FindFirst("firstname").Value;

public static string LastName(this ClaimsPrincipal user)
=> user.FindFirst("lastname").Value;

public static string Email(this ClaimsPrincipal user)
=> user.Identity.Name;
}
}
16 changes: 16 additions & 0 deletions Client/Pages/Authentication.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@page "/authentication/{action}"
@layout HeaderLayout
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action">
<LoggingIn>
<div class="loader"></div>
</LoggingIn>
<LogOutSucceeded>
<Redirect Url="" />
</LogOutSucceeded>
</RemoteAuthenticatorView>

@code{
[Parameter] public string Action { get; set; }
}
53 changes: 53 additions & 0 deletions Client/Pages/Index.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
@page "/"
@attribute [Authorize]
@inject NavigationManager Navigation
@inject LocalVehiclesStore LocalVehiclesStore
@inject IStringLocalizer<App> Localize

<AuthorizeView>
<header>
<div class="toolbar">
<button class="toolbar-item" @onclick="() => loginStatusOverlay.Show()">☰</button>
</div>
<div class="title">@Localize["Welcome, {0}!", context.User.FirstName()]</div>
</header>

<Overlay @ref="loginStatusOverlay" OverlayStyle="Overlay.Style.Top" CssClass="home-overlay">
<LoginStatus />
</Overlay>
</AuthorizeView>

<main class="container">
<EditForm class="home-options" Model="this" OnValidSubmit="FindVehicle">
<DataAnnotationsValidator />

<p>@Localize["Enter license number:"]</p>
<Autocomplete @bind-Value="@LicenseNumber" Choices="@GetLicenseAutocompleteItems"
OnItemChosen="FindVehicle" class="find-by-license-plate" placeholder="@Localize["License"]" />
<ValidationMessage For="() => LicenseNumber" />
</EditForm>
</main>

<footer>
<SyncStatus />
</footer>

@code {
Overlay loginStatusOverlay;

[RegularExpression("[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*\\-{0,1}", ErrorMessageResourceType = typeof(Resources.App), ErrorMessageResourceName = nameof(Resources.App.LicenseNumberIncorrectFormat))]
public string LicenseNumber { get; set; }

async Task<IEnumerable<string>> GetLicenseAutocompleteItems(string prefix)
{
return await LocalVehiclesStore.Autocomplete(prefix);
}

void FindVehicle()
{
if (!string.IsNullOrEmpty(LicenseNumber))
{
Navigation.NavigateTo($"vehicle/{Uri.EscapeDataString(LicenseNumber)}");
}
}
}
Loading

0 comments on commit 5897e8b

Please sign in to comment.