├── .devcontainer └── devcontainer.json ├── .github └── workflows │ └── build-CI.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CarChecker.sln ├── Client ├── App.razor ├── CarChecker.Client.csproj ├── Data │ ├── DataUrl.cs │ ├── LocalVehiclesStore.cs │ ├── OfflineAccountClaimsPrincipalFactory.cs │ └── UserExtensions.cs ├── Pages │ ├── Authentication.razor │ ├── Index.razor │ └── VehicleEditor.razor ├── Program.cs ├── Properties │ └── launchSettings.json ├── Resources │ ├── App.Designer.cs │ ├── App.es.resx │ └── App.resx ├── Shared │ ├── Autocomplete.razor │ ├── DamageDetection.razor │ ├── HeaderLayout.razor │ ├── LoginStatus.razor │ ├── NotLoggedIn.razor │ ├── Overlay.razor │ ├── Redirect.razor │ ├── SyncStatus.razor │ ├── VehicleNoteEditor.razor │ ├── VehicleNotes.razor │ └── VehicleSummary.razor ├── _Imports.razor └── wwwroot │ ├── 3d │ ├── car.json │ ├── sky-nx.png │ ├── sky-ny.png │ ├── sky-nz.png │ ├── sky-px.png │ ├── sky-py.png │ ├── sky-pz.png │ └── sky.png │ ├── css │ ├── app.css │ ├── blazor.css │ └── spinner.css │ ├── favicon.ico │ ├── icon-512.png │ ├── idb.js │ ├── index.html │ ├── localVehicleStore.js │ ├── manifest.json │ ├── service-worker.js │ ├── service-worker.published.js │ └── user.svg ├── README.md ├── Server ├── ApplicationUserClaimsPrincipalFactory.cs ├── Areas │ └── Identity │ │ ├── IdentityHostingStartup.cs │ │ └── Pages │ │ ├── Account │ │ ├── Manage │ │ │ ├── Index.cshtml │ │ │ └── Index.cshtml.cs │ │ ├── Register.cshtml │ │ ├── Register.cshtml.cs │ │ └── _ViewImports.cshtml │ │ ├── Shared │ │ └── _LoginPartial.cshtml │ │ ├── _ValidationScriptsPartial.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml ├── CarChecker.Server.csproj ├── Controllers │ ├── DetectDamageController.cs │ ├── OidcConfigurationController.cs │ └── VehicleController.cs ├── Data │ ├── ApplicationDbContext.cs │ └── Migrations │ │ ├── 00000000000000_CreateIdentitySchema.Designer.cs │ │ ├── 00000000000000_CreateIdentitySchema.cs │ │ ├── 20200426112711_UserFirstLastNameFields.Designer.cs │ │ ├── 20200426112711_UserFirstLastNameFields.cs │ │ ├── 20200427140021_Vehicles.Designer.cs │ │ ├── 20200427140021_Vehicles.cs │ │ └── ApplicationDbContextModelSnapshot.cs ├── Models │ └── ApplicationUser.cs ├── Pages │ ├── Error.cshtml │ ├── Error.cshtml.cs │ ├── Shared │ │ ├── _Layout.cshtml │ │ └── _LoginPartial.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── Program.cs ├── Properties │ └── launchSettings.json ├── SeedData.cs ├── Startup.cs ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ ├── css │ └── site.css │ └── favicon.ico ├── Shared ├── CarChecker.Shared.csproj ├── DamageDetectionResult.cs ├── FuelLevel.cs ├── InspectionNote.cs ├── Vehicle.cs └── VehiclePart.cs ├── deploy.json └── deploy.parameters.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": [ 3 | "ms-vscode.csharp", 4 | "formulahendry.dotnet-test-explorer" 5 | ] 6 | } -------------------------------------------------------------------------------- /.github/workflows/build-CI.yml: -------------------------------------------------------------------------------- 1 | name: Build Blazor App 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | if: github.event_name == 'push' && contains(toJson(github.event.commits), '***NO_CI***') == false && contains(toJson(github.event.commits), '[ci skip]') == false && contains(toJson(github.event.commits), '[skip ci]') == false 8 | name: Build 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup .NET Core 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: 3.1.300 16 | 17 | - name: Restore 18 | run: dotnet restore 19 | 20 | - name: Build with dotnet 21 | run: dotnet build --configuration Release --no-restore 22 | 23 | - name: Publish with dotnet 24 | run: dotnet publish --configuration Release -o published_app --no-build 25 | 26 | - name: Publish artifacts 27 | uses: actions/upload-artifact@v2 28 | with: 29 | name: webapp 30 | path: published_app 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bin/ 3 | obj/ 4 | .vs/ 5 | *.csproj.user 6 | Server/app.db 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (Blazor Standalone)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "program": "dotnet", 12 | "args": ["run"], 13 | "cwd": "${workspaceFolder}/Server", 14 | "env": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | { 19 | "name": ".NET Core Debug Blazor Web Assembly in Chrome", 20 | "type": "pwa-chrome", 21 | "request": "launch", 22 | "timeout": 30000, 23 | // If you have changed the default port / launch URL make sure to update the expectation below 24 | "url": "https://localhost:5001", 25 | "webRoot": "${workspaceFolder}/Server", 26 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/Server/CarChecker.Server.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/Server/CarChecker.Server.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/Server/CarChecker.Server.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /CarChecker.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 16 3 | VisualStudioVersion = 16.0.0.0 4 | MinimumVisualStudioVersion = 16.0.0.0 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarChecker.Server", "Server\CarChecker.Server.csproj", "{28148247-170D-49AE-A367-7984410446CF}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarChecker.Client", "Client\CarChecker.Client.csproj", "{985FD988-9FA6-4305-A7BB-70E31DF1D0B8}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarChecker.Shared", "Shared\CarChecker.Shared.csproj", "{C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}" 10 | EndProject 11 | Global 12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 13 | Debug|Any CPU = Debug|Any CPU 14 | Debug|x64 = Debug|x64 15 | Debug|x86 = Debug|x86 16 | Release|Any CPU = Release|Any CPU 17 | Release|x64 = Release|x64 18 | Release|x86 = Release|x86 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {28148247-170D-49AE-A367-7984410446CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {28148247-170D-49AE-A367-7984410446CF}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {28148247-170D-49AE-A367-7984410446CF}.Debug|x64.ActiveCfg = Debug|Any CPU 24 | {28148247-170D-49AE-A367-7984410446CF}.Debug|x64.Build.0 = Debug|Any CPU 25 | {28148247-170D-49AE-A367-7984410446CF}.Debug|x86.ActiveCfg = Debug|Any CPU 26 | {28148247-170D-49AE-A367-7984410446CF}.Debug|x86.Build.0 = Debug|Any CPU 27 | {28148247-170D-49AE-A367-7984410446CF}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {28148247-170D-49AE-A367-7984410446CF}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {28148247-170D-49AE-A367-7984410446CF}.Release|x64.ActiveCfg = Release|Any CPU 30 | {28148247-170D-49AE-A367-7984410446CF}.Release|x64.Build.0 = Release|Any CPU 31 | {28148247-170D-49AE-A367-7984410446CF}.Release|x86.ActiveCfg = Release|Any CPU 32 | {28148247-170D-49AE-A367-7984410446CF}.Release|x86.Build.0 = Release|Any CPU 33 | {985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Debug|x64.ActiveCfg = Debug|Any CPU 36 | {985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Debug|x64.Build.0 = Debug|Any CPU 37 | {985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Debug|x86.ActiveCfg = Debug|Any CPU 38 | {985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Debug|x86.Build.0 = Debug|Any CPU 39 | {985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Release|x64.ActiveCfg = Release|Any CPU 42 | {985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Release|x64.Build.0 = Release|Any CPU 43 | {985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Release|x86.ActiveCfg = Release|Any CPU 44 | {985FD988-9FA6-4305-A7BB-70E31DF1D0B8}.Release|x86.Build.0 = Release|Any CPU 45 | {C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Debug|x64.ActiveCfg = Debug|Any CPU 48 | {C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Debug|x64.Build.0 = Debug|Any CPU 49 | {C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Debug|x86.ActiveCfg = Debug|Any CPU 50 | {C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Debug|x86.Build.0 = Debug|Any CPU 51 | {C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Release|x64.ActiveCfg = Release|Any CPU 54 | {C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Release|x64.Build.0 = Release|Any CPU 55 | {C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Release|x86.ActiveCfg = Release|Any CPU 56 | {C5DD5CF5-8462-4433-B463-E3F3EA6F0FA1}.Release|x86.Build.0 = Release|Any CPU 57 | EndGlobalSection 58 | GlobalSection(SolutionProperties) = preSolution 59 | HideSolutionNode = FALSE 60 | EndGlobalSection 61 | GlobalSection(ExtensibilityGlobals) = postSolution 62 | SolutionGuid = {C511E458-9B4D-4D16-AB90-69DC35B0F57B} 63 | EndGlobalSection 64 | EndGlobal 65 | -------------------------------------------------------------------------------- /Client/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 |
10 | 11 | @if (!context.User.Identity.IsAuthenticated) 12 | { 13 | 14 | } 15 | else 16 | { 17 | 18 |

You are not authorized to access this resource.

19 |
20 | } 21 |
22 |
23 |
24 | 25 | 26 |

Sorry, there's nothing at this address.

27 |
28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /Client/CarChecker.Client.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 3.0 6 | service-worker-assets.js 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | PublicResXFileCodeGenerator 34 | App.Designer.cs 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Client/Data/DataUrl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace CarChecker.Client.Data 5 | { 6 | public static class DataUrl 7 | { 8 | public static string ToDataUrl(this MemoryStream data, string format) 9 | { 10 | var span = new Span(data.GetBuffer()).Slice(0, (int)data.Length); 11 | return $"data:{format};base64,{Convert.ToBase64String(span)}"; 12 | } 13 | 14 | public static byte[] ToBytes(string url) 15 | { 16 | var commaPos = url.IndexOf(','); 17 | if (commaPos >= 0) 18 | { 19 | var base64 = url.Substring(commaPos + 1); 20 | return Convert.FromBase64String(base64); 21 | } 22 | 23 | return null; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Client/Data/LocalVehiclesStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Net.Http.Json; 5 | using System.Security.Claims; 6 | using System.Threading.Tasks; 7 | using CarChecker.Shared; 8 | using Microsoft.JSInterop; 9 | 10 | namespace CarChecker.Client.Data 11 | { 12 | // To support offline use, we use this simple local data repository 13 | // instead of performing data access directly against the server. 14 | // This would not be needed if we assumed that network access was always 15 | // available. 16 | 17 | public class LocalVehiclesStore 18 | { 19 | private readonly HttpClient httpClient; 20 | private readonly IJSRuntime js; 21 | 22 | public LocalVehiclesStore(HttpClient httpClient, IJSRuntime js) 23 | { 24 | this.httpClient = httpClient; 25 | this.js = js; 26 | } 27 | 28 | public ValueTask GetOutstandingLocalEditsAsync() 29 | { 30 | return js.InvokeAsync( 31 | "localVehicleStore.getAll", "localedits"); 32 | } 33 | 34 | public async Task SynchronizeAsync() 35 | { 36 | // If there are local edits, always send them first 37 | foreach (var editedVehicle in await GetOutstandingLocalEditsAsync()) 38 | { 39 | (await httpClient.PutAsJsonAsync("api/vehicle/details", editedVehicle)).EnsureSuccessStatusCode(); 40 | await DeleteAsync("localedits", editedVehicle.LicenseNumber); 41 | } 42 | 43 | await FetchChangesAsync(); 44 | } 45 | 46 | public ValueTask SaveUserAccountAsync(ClaimsPrincipal user) 47 | { 48 | return user != null 49 | ? PutAsync("metadata", "userAccount", user.Claims.Select(c => new ClaimData { Type = c.Type, Value = c.Value })) 50 | : DeleteAsync("metadata", "userAccount"); 51 | } 52 | 53 | public async Task LoadUserAccountAsync() 54 | { 55 | var storedClaims = await GetAsync("metadata", "userAccount"); 56 | return storedClaims != null 57 | ? new ClaimsPrincipal(new ClaimsIdentity(storedClaims.Select(c => new Claim(c.Type, c.Value)), "appAuth")) 58 | : new ClaimsPrincipal(new ClaimsIdentity()); 59 | } 60 | 61 | public ValueTask Autocomplete(string prefix) 62 | => js.InvokeAsync("localVehicleStore.autocompleteKeys", "serverdata", prefix, 5); 63 | 64 | // If there's an outstanding local edit, use that. If not, use the server data. 65 | public async Task GetVehicle(string licenseNumber) 66 | => await GetAsync("localedits", licenseNumber) 67 | ?? await GetAsync("serverdata", licenseNumber); 68 | 69 | public async ValueTask GetLastUpdateDateAsync() 70 | { 71 | var value = await GetAsync("metadata", "lastUpdateDate"); 72 | return value == null ? (DateTime?)null : DateTime.Parse(value); 73 | } 74 | 75 | public ValueTask SaveVehicleAsync(Vehicle vehicle) 76 | => PutAsync("localedits", null, vehicle); 77 | 78 | async Task FetchChangesAsync() 79 | { 80 | var mostRecentlyUpdated = await js.InvokeAsync("localVehicleStore.getFirstFromIndex", "serverdata", "lastUpdated", "prev"); 81 | var since = mostRecentlyUpdated?.LastUpdated ?? DateTime.MinValue; 82 | var json = await httpClient.GetStringAsync($"api/vehicle/changedvehicles?since={since:o}"); 83 | await js.InvokeVoidAsync("localVehicleStore.putAllFromJson", "serverdata", json); 84 | await PutAsync("metadata", "lastUpdateDate", DateTime.Now.ToString("o")); 85 | } 86 | 87 | ValueTask GetAsync(string storeName, object key) 88 | => js.InvokeAsync("localVehicleStore.get", storeName, key); 89 | 90 | ValueTask PutAsync(string storeName, object key, T value) 91 | => js.InvokeVoidAsync("localVehicleStore.put", storeName, key, value); 92 | 93 | ValueTask DeleteAsync(string storeName, object key) 94 | => js.InvokeVoidAsync("localVehicleStore.delete", storeName, key); 95 | 96 | class ClaimData 97 | { 98 | public string Type { get; set; } 99 | public string Value { get; set; } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Client/Data/OfflineAccountClaimsPrincipalFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication; 2 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System; 5 | using System.Security.Claims; 6 | using System.Threading.Tasks; 7 | 8 | namespace CarChecker.Client.Data 9 | { 10 | public class OfflineAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory 11 | { 12 | private readonly IServiceProvider services; 13 | 14 | public OfflineAccountClaimsPrincipalFactory(IServiceProvider services, IAccessTokenProviderAccessor accessor) : base(accessor) 15 | { 16 | this.services = services; 17 | } 18 | 19 | public override async ValueTask CreateUserAsync(RemoteUserAccount account, RemoteAuthenticationUserOptions options) 20 | { 21 | var localVehiclesStore = services.GetRequiredService(); 22 | 23 | var result = await base.CreateUserAsync(account, options); 24 | if (result.Identity.IsAuthenticated) 25 | { 26 | await localVehiclesStore.SaveUserAccountAsync(result); 27 | } 28 | else 29 | { 30 | result = await localVehiclesStore.LoadUserAccountAsync(); 31 | } 32 | 33 | return result; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Client/Data/UserExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace CarChecker.Client.Data 4 | { 5 | public static class UserExtensions 6 | { 7 | public static string FirstName(this ClaimsPrincipal user) 8 | => user.FindFirst("firstname").Value; 9 | 10 | public static string LastName(this ClaimsPrincipal user) 11 | => user.FindFirst("lastname").Value; 12 | 13 | public static string Email(this ClaimsPrincipal user) 14 | => user.Identity.Name; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Client/Pages/Authentication.razor: -------------------------------------------------------------------------------- 1 | @page "/authentication/{action}" 2 | @layout HeaderLayout 3 | @using Microsoft.AspNetCore.Components.WebAssembly.Authentication 4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 |
13 | 14 | @code{ 15 | [Parameter] public string Action { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /Client/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @attribute [Authorize] 3 | @inject NavigationManager Navigation 4 | @inject LocalVehiclesStore LocalVehiclesStore 5 | @inject IStringLocalizer Localize 6 | 7 | 8 |
9 |
10 | 11 |
12 |
@Localize["Welcome, {0}!", context.User.FirstName()]
13 |
14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | 24 |

@Localize["Enter license number:"]

25 | 27 | 28 |
29 |
30 | 31 |
32 | 33 |
34 | 35 | @code { 36 | Overlay loginStatusOverlay; 37 | 38 | [RegularExpression("[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*\\-{0,1}", ErrorMessageResourceType = typeof(Resources.App), ErrorMessageResourceName = nameof(Resources.App.LicenseNumberIncorrectFormat))] 39 | public string LicenseNumber { get; set; } 40 | 41 | async Task> GetLicenseAutocompleteItems(string prefix) 42 | { 43 | return await LocalVehiclesStore.Autocomplete(prefix); 44 | } 45 | 46 | void FindVehicle() 47 | { 48 | if (!string.IsNullOrEmpty(LicenseNumber)) 49 | { 50 | Navigation.NavigateTo($"vehicle/{Uri.EscapeDataString(LicenseNumber)}"); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Client/Pages/VehicleEditor.razor: -------------------------------------------------------------------------------- 1 | @page "/vehicle/{LicenseNumber}" 2 | @attribute [Authorize] 3 | @inject IJSRuntime JS 4 | @inject LocalVehiclesStore LocalVehiclesStore 5 | @inject NavigationManager Navigation 6 | @inject IStringLocalizer Localize 7 | 8 | 9 | 10 | 11 |
12 | 15 |
16 | @LicenseNumber 17 | @if (editContext.IsModified()) 18 | { 19 | 20 | } 21 |
22 |
23 | 24 | @if (Vehicle != null) 25 | { 26 |
27 |
28 | 33 | 34 | 35 |
36 |
37 | 40 |
41 |
42 | 43 | 48 | } 49 | else if (notFound) 50 | { 51 |
52 | @Localize["Sorry, there is no vehicle with license number '{0}'.", LicenseNumber] 53 |
54 | } 55 | else 56 | { 57 |
58 | } 59 |
60 | 61 | 62 | 63 | @code { 64 | EditContext editContext; 65 | [ValidateComplexType] public Vehicle Vehicle { get; set; } 66 | bool notFound; 67 | VehicleNoteEditor noteEditor; 68 | VehiclePart? selectedPart; 69 | IEnumerable damagedParts => Vehicle.Notes.Select(n => n.Location).Distinct(); 70 | 71 | [Parameter] public string LicenseNumber { get; set; } 72 | 73 | protected override async Task OnInitializedAsync() 74 | { 75 | editContext = new EditContext(this); 76 | editContext.OnFieldChanged += (sender, args) => StateHasChanged(); 77 | 78 | Vehicle = await LocalVehiclesStore.GetVehicle(LicenseNumber); 79 | notFound = (Vehicle == null); 80 | } 81 | 82 | async Task GoBack() 83 | { 84 | if (!editContext.IsModified() || await JS.InvokeAsync("confirm", Localize["Discard changes?"].Value)) 85 | { 86 | await JS.InvokeVoidAsync("history.back"); 87 | } 88 | } 89 | 90 | async Task Save() 91 | { 92 | await LocalVehiclesStore.SaveVehicleAsync(Vehicle); 93 | editContext.MarkAsUnmodified(); 94 | Navigation.NavigateTo(""); // Return to home screen 95 | } 96 | 97 | async Task AddNote() 98 | { 99 | if (selectedPart.HasValue) 100 | { 101 | noteEditor.Show(Vehicle, new InspectionNote { Location = selectedPart.Value }); 102 | } 103 | else 104 | { 105 | await JS.InvokeVoidAsync("alert", Localize["First, please tap on the part of the vehicle to which the note applies."].Value); 106 | } 107 | } 108 | 109 | void OnNoteEdited() 110 | { 111 | // Switch the edit context into a "modified" state if it isn't already 112 | editContext.NotifyFieldChanged(editContext.Field(nameof(Vehicle.Notes))); 113 | } 114 | 115 | void SetSelectedPart(string objectName) 116 | { 117 | selectedPart = Enum.TryParse(objectName, out var parsedPart) 118 | ? parsedPart 119 | : (VehiclePart?)null; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Client/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using System.Text; 6 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication; 7 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | using CarChecker.Client.Data; 12 | 13 | namespace CarChecker.Client 14 | { 15 | public class Program 16 | { 17 | public static async Task Main(string[] args) 18 | { 19 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 20 | builder.RootComponents.Add("app"); 21 | builder.Logging.SetMinimumLevel(LogLevel.Warning); 22 | 23 | // Configure HttpClient for use when talking to server backend 24 | builder.Services.AddHttpClient("CarChecker.ServerAPI", 25 | client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) 26 | .AddHttpMessageHandler(); 27 | 28 | // Other DI services 29 | builder.Services.AddScoped(); 30 | builder.Services.AddTransient(sp => sp.GetRequiredService().CreateClient("CarChecker.ServerAPI")); 31 | builder.Services.AddApiAuthorization(); 32 | builder.Services.AddScoped, OfflineAccountClaimsPrincipalFactory>(); 33 | builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); 34 | 35 | await builder.Build().RunAsync(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:42252", 7 | "sslPort": 44300 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "CarChecker": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 23 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Client/Resources/App.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace CarChecker.Client.Resources { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | public class App { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal App() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | public static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CarChecker.Client.Resources.App", typeof(App).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | public static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to The license number is not in the correct format.. 65 | /// 66 | public static string LicenseNumberIncorrectFormat { 67 | get { 68 | return ResourceManager.GetString("LicenseNumberIncorrectFormat", resourceCulture); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Client/Resources/App.es.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Añadir observación 122 | 123 | 124 | Atrás 125 | As in "go back" 126 | 127 | 128 | Carrocería frontal 129 | Name of part of car 130 | 131 | 132 | Carrocería frontal izquierda 133 | Name of part of car 134 | 135 | 136 | Carrocería frontal derecha 137 | Name of part of car 138 | 139 | 140 | Carrocería trasera 141 | Name of part of car 142 | 143 | 144 | Carrocería trasera izquierda 145 | Name of part of car 146 | 147 | 148 | Carrocería trasera derecha 149 | Name of part of car 150 | 151 | 152 | Capó 153 | Name of part of car 154 | 155 | 156 | Inspector de vehículos 157 | 158 | 159 | Confianza: 160 | 161 | 162 | Error. Cambios pendientes: 163 | 164 | 165 | Dañado! 166 | As in, car is damaged 167 | 168 | 169 | Eliminar 170 | 171 | 172 | Detalles: 173 | 174 | 175 | Detectar daños 176 | 177 | 178 | Detectando... 179 | As in, detecting damage 180 | 181 | 182 | Descartar cambios? 183 | 184 | 185 | Puerta Delantera Izquierda 186 | Name of part of car 187 | 188 | 189 | Puerta Delantera Derecha 190 | Name of part of car 191 | 192 | 193 | Puerta Trasera Izquierda 194 | Name of part of car 195 | 196 | 197 | Puerta Trasera Derecha 198 | Name of part of car 199 | 200 | 201 | Vacío 202 | 203 | 204 | Introduzca la matrícula: 205 | License meaning "car registration number" to identify the vehicle 206 | 207 | 208 | Primero, pulse en la parte del vehículo aplicable. 209 | Note means "inspection note", i.e., something recorded about the condition of the vehicle 210 | 211 | 212 | Lleno 213 | 214 | 215 | Parrilla 216 | Name of part of car 217 | 218 | 219 | 50% 220 | 221 | 222 | Faro izquierdo 223 | Name of part of car 224 | 225 | 226 | Faro derecho 227 | Name of part of car 228 | 229 | 230 | Última actualizacion: 231 | 232 | 233 | Matrícula 234 | 235 | 236 | El formato de la matrícula es incorrecto. 237 | 238 | 239 | Ubicación: 240 | Meaning "on which part of the car is the damage located" 241 | 242 | 243 | Iniciar sesión 244 | 245 | 246 | Cerrar sesión 247 | 248 | 249 | Administrar cuenta 250 | 251 | 252 | Kilometraje 253 | Mileage recorded on car odometer 254 | 255 | 256 | Retrovisor izquierdo 257 | Name of part of car 258 | 259 | 260 | Retrovisor derecho 261 | Name of part of car 262 | 263 | 264 | Nunca 265 | As in "never updated" 266 | 267 | 268 | No se añadieron observaciónes. 269 | Note means "inspection note", i.e., something recorded about the condition of the vehicle 270 | 271 | 272 | OK - sin daños 273 | 274 | 275 | Foto: 276 | 277 | 278 | Reintentar 279 | 280 | 281 | Techo 282 | Name of part of car 283 | 284 | 285 | Guardar 286 | 287 | 288 | Mostrar todas 289 | 290 | 291 | Lo sentimos, no hay ningún vehículo con la matrícula '{0}'. 292 | 293 | 294 | Luz trasera izquierda 295 | Name of part of car 296 | 297 | 298 | Luz trasera derecha 299 | Name of part of car 300 | 301 | 302 | Tomar nueva 303 | As in "take new photo" 304 | 305 | 306 | Depósito: 307 | As in "fuel level in the tank". Example: "Tank: Full" 308 | 309 | 310 | Chasis 311 | Name of part of car 312 | 313 | 314 | Actualizar 315 | 316 | 317 | Actualizando... 318 | 319 | 320 | Bienvenido, {0}! 321 | 322 | 323 | Paso de la rueda frontal izquierda 324 | Name of part of car 325 | 326 | 327 | Paso de la rueda frontal derecha 328 | Name of part of car 329 | 330 | 331 | Paso de la rueda trasera izquierda 332 | Name of part of car 333 | 334 | 335 | Paso de la rueda trasera derecha 336 | Name of part of car 337 | 338 | 339 | Rueda delantera izquierda 340 | Name of part of car 341 | 342 | 343 | Rueda delantera derecha 344 | Name of part of car 345 | 346 | 347 | Rueda trasera izquierda 348 | Name of part of car 349 | 350 | 351 | Rueda trasera derecha 352 | Name of part of car 353 | 354 | 355 | Parabrisas trasero 356 | Name of part of car 357 | 358 | 359 | Ventana delantera izquierda 360 | Name of part of car 361 | 362 | 363 | Ventana delantera derecha 364 | Name of part of car 365 | 366 | 367 | Ventana trasera izquierda 368 | Name of part of car 369 | 370 | 371 | Ventana trasera derecha 372 | Name of part of car 373 | 374 | 375 | Parabrisas 376 | Name of part of car 377 | 378 | 379 | {0} no mostradas. 380 | example: 3 not shown. 381 | 382 | -------------------------------------------------------------------------------- /Client/Resources/App.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | The license number is not in the correct format. 122 | 123 | -------------------------------------------------------------------------------- /Client/Shared/Autocomplete.razor: -------------------------------------------------------------------------------- 1 | @inherits InputText 2 | 3 | 5 | 6 |
7 | @foreach (var choice in currentChoices.Take(3)) 8 | { 9 |
@choice
10 | } 11 |
12 | 13 | @code { 14 | bool visible; 15 | string[] currentChoices = Array.Empty(); 16 | 17 | [Parameter] public Func>> Choices { get; set; } 18 | [Parameter] public EventCallback OnItemChosen { get; set; } 19 | 20 | string BoundValue 21 | { 22 | get => CurrentValueAsString; 23 | set 24 | { 25 | CurrentValueAsString = value; 26 | _ = UpdateAutocompleteOptionsAsync(); 27 | } 28 | } 29 | 30 | async Task UpdateAutocompleteOptionsAsync() 31 | { 32 | if (EditContext.GetValidationMessages(FieldIdentifier).Any()) 33 | { 34 | // If the input is invalid, don't show any autocomplete options 35 | currentChoices = Array.Empty(); 36 | } 37 | else 38 | { 39 | var valueSnapshot = CurrentValueAsString; 40 | var suppliedChoices = (await Choices(valueSnapshot)).ToArray(); 41 | 42 | // By the time we get here, the user might have typed other characters 43 | // Only use the result if this still represents the latest entry 44 | if (CurrentValueAsString == valueSnapshot) 45 | { 46 | currentChoices = suppliedChoices; 47 | visible = currentChoices.Any(); 48 | StateHasChanged(); 49 | } 50 | } 51 | } 52 | 53 | void Hide() 54 | { 55 | visible = false; 56 | } 57 | 58 | Task ChooseAsync(string choice) 59 | { 60 | CurrentValueAsString = choice; 61 | Hide(); 62 | return OnItemChosen.InvokeAsync(null); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Client/Shared/DamageDetection.razor: -------------------------------------------------------------------------------- 1 | @inject HttpClient Http 2 | @inject IStringLocalizer Localize 3 | 4 | @* 5 | IMPORTANT NOTE 6 | ============== 7 | In this repo, we're not including the pretrained ML.NET model that was used in the demo, 8 | so the server's damage detection responses are simply random. See DamageDetectionController.cs 9 | in the Server project. 10 | 11 | We leave the rest of the damage detection implementation in place here as an example of 12 | how user-supplied files can be uploaded to a backend server. 13 | *@ 14 | 15 | @if (!string.IsNullOrEmpty(Image)) 16 | { 17 | 18 | 19 | if (isDetectingDamage) 20 | { 21 |
@Localize["Detecting..."]
22 | } 23 | else if (damageDetectionResult != null) 24 | { 25 | if (damageDetectionResult.IsDamaged) 26 | { 27 |
28 | @Localize["Damaged!"] 29 | (@Localize["Confidence:"] @((100 * damageDetectionResult.Score).ToString("0"))%) 30 |
31 | } 32 | else 33 | { 34 |
35 | @Localize["OK - not damaged"] 36 | (@Localize["Confidence:"] @((100 * damageDetectionResult.Score).ToString("0"))%) 37 |
38 | } 39 | } 40 | } 41 | 42 | @code { 43 | bool isDetectingDamage; 44 | DamageDetectionResult damageDetectionResult; 45 | 46 | [Parameter] public string Image { get; set; } 47 | 48 | protected override void OnParametersSet() 49 | { 50 | isDetectingDamage = false; 51 | damageDetectionResult = null; 52 | } 53 | 54 | async Task PerformDamageDetection() 55 | { 56 | isDetectingDamage = true; 57 | 58 | var imageBytes = DataUrl.ToBytes(Image); 59 | var response = await Http.PostAsync("api/detectdamage", new ByteArrayContent(imageBytes)); 60 | damageDetectionResult = await response.Content.ReadFromJsonAsync(); 61 | 62 | isDetectingDamage = false; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Client/Shared/HeaderLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 |
5 | 6 |
7 | @Body 8 |
9 | -------------------------------------------------------------------------------- /Client/Shared/LoginStatus.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.WebAssembly.Authentication 2 | @inject SignOutSessionStateManager SignOutManager 3 | @inject NavigationManager Navigation 4 | @inject LocalVehiclesStore LocalVehiclesStore 5 | @inject IStringLocalizer Localize 6 | 7 | 8 |
9 | 10 |
11 |
@context.User.FirstName() @context.User.LastName()
12 |
@context.User.Email()
13 |
14 |
15 | 23 |
24 | 25 | @code { 26 | private async Task BeginLogOut() 27 | { 28 | await SignOutManager.SetSignOutState(); 29 | await LocalVehiclesStore.SaveUserAccountAsync(null); 30 | Navigation.NavigateTo("authentication/logout"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Client/Shared/NotLoggedIn.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager Navigation 2 | @inject IStringLocalizer Localize 3 | 4 |
5 | @Localize["Car Checker"] 6 |
7 | 8 |
9 | 12 |
13 | -------------------------------------------------------------------------------- /Client/Shared/Overlay.razor: -------------------------------------------------------------------------------- 1 |
2 |
3 | @ChildContent 4 |
5 |
6 | 7 | @code { 8 | bool visible; 9 | 10 | [Parameter] public Style OverlayStyle { get; set; } = Style.FullScreen; 11 | [Parameter] public string CssClass { get; set; } 12 | [Parameter] public RenderFragment ChildContent { get; set; } 13 | [Parameter] public EventCallback OnCloseRequested { get; set; } 14 | 15 | public void Show() 16 | { 17 | visible = true; 18 | StateHasChanged(); 19 | } 20 | 21 | public void Hide() 22 | { 23 | visible = false; 24 | StateHasChanged(); // Only relevant when invoked externally 25 | } 26 | 27 | async Task RequestClose() 28 | { 29 | // If a callback was provided, call it. Otherwise just close immediately. 30 | if (OnCloseRequested.HasDelegate) 31 | { 32 | await OnCloseRequested.InvokeAsync(null); 33 | } 34 | else 35 | { 36 | Hide(); 37 | } 38 | } 39 | 40 | string StyleCssClass => OverlayStyle switch 41 | { 42 | Style.Top => "overlay-contents-top", 43 | Style.FullScreen => "overlay-contents-full", 44 | _ => string.Empty 45 | }; 46 | 47 | public enum Style 48 | { 49 | Top, 50 | FullScreen 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Client/Shared/Redirect.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager Navigation 2 | @code{ 3 | [Parameter] public string Url { get; set; } 4 | 5 | protected override void OnInitialized() 6 | { 7 | Navigation.NavigateTo(Url); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Client/Shared/SyncStatus.razor: -------------------------------------------------------------------------------- 1 | @inject LocalVehiclesStore LocalVehiclesStore 2 | @inject IStringLocalizer Localize 3 | 4 | @if (isSynchronizing) 5 | { 6 | @Localize["Updating..."] 7 | } 8 | else if (lastSyncFailed) 9 | { 10 | @Localize["Could not sync. Unsent edits:"] @outstandingLocalEdits 11 | @Localize["Retry"] 12 | } 13 | else 14 | { 15 | @Localize["Last updated:"] @GetLastUpdatedText() 16 | @Localize["Update"] 17 | } 18 | 19 | @code { 20 | bool isSynchronizing; 21 | DateTime? lastUpdated; 22 | int outstandingLocalEdits; 23 | bool lastSyncFailed; 24 | 25 | protected override async Task OnInitializedAsync() 26 | { 27 | await Synchronize(); 28 | } 29 | 30 | async Task Synchronize() 31 | { 32 | isSynchronizing = true; 33 | lastSyncFailed = false; 34 | 35 | try 36 | { 37 | await LocalVehiclesStore.SynchronizeAsync(); 38 | } 39 | catch (Exception ex) 40 | { 41 | lastSyncFailed = true; 42 | Console.WriteLine(ex); 43 | } 44 | finally 45 | { 46 | // Even if we weren't able to reach the server, we can update status based on local data 47 | lastUpdated = await LocalVehiclesStore.GetLastUpdateDateAsync(); 48 | outstandingLocalEdits = (await LocalVehiclesStore.GetOutstandingLocalEditsAsync()).Length; 49 | } 50 | 51 | isSynchronizing = false; 52 | } 53 | 54 | string GetLastUpdatedText() 55 | { 56 | if (lastUpdated.HasValue) 57 | { 58 | return lastUpdated.Value.Date == DateTime.Now.Date 59 | ? lastUpdated.Value.ToLocalTime().ToShortTimeString() 60 | : lastUpdated.Value.ToLocalTime().ToShortDateString(); 61 | } 62 | else 63 | { 64 | return Localize["Never"]; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Client/Shared/VehicleNoteEditor.razor: -------------------------------------------------------------------------------- 1 | @inject IJSRuntime JS 2 | @inject IStringLocalizer Localize 3 | @using BlazorInputFile 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | @foreach (VehiclePart part in Enum.GetValues(typeof(VehiclePart))) 15 | { 16 | 17 | } 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 |
40 | 41 |
42 | @if (isExistingItem) 43 | { 44 | 45 | } 46 | 47 | 48 |
49 |
50 |
51 | 52 | @code { 53 | [Parameter] public EventCallback OnCommittedEdit { get; set; } 54 | 55 | EditContext editContext; 56 | Overlay overlay; 57 | InspectionNote formData; 58 | 59 | Vehicle vehicle; 60 | InspectionNote vehicleNote; 61 | bool isExistingItem; 62 | 63 | public VehicleNoteEditor() 64 | { 65 | formData = new InspectionNote(); 66 | editContext = new EditContext(formData); 67 | } 68 | 69 | public void Show(Vehicle vehicle, InspectionNote noteToEdit) 70 | { 71 | this.vehicle = vehicle; 72 | this.vehicleNote = noteToEdit; 73 | isExistingItem = vehicle.Notes.Contains(noteToEdit); 74 | formData.CopyFrom(noteToEdit); 75 | editContext.MarkAsUnmodified(); 76 | overlay.Show(); 77 | } 78 | 79 | Task CommitEdit() 80 | { 81 | overlay.Hide(); 82 | return OnCommittedEdit.InvokeAsync(null); 83 | } 84 | 85 | Task DeleteAsync() 86 | { 87 | vehicle.Notes.Remove(vehicleNote); 88 | return CommitEdit(); 89 | } 90 | 91 | Task SaveAsync() 92 | { 93 | if (!isExistingItem) 94 | { 95 | vehicle.Notes.Add(vehicleNote); 96 | } 97 | 98 | vehicleNote.CopyFrom(formData); 99 | return CommitEdit(); 100 | } 101 | 102 | async Task Dismiss() 103 | { 104 | await Task.Yield(); // In case this event triggers other things too (e.g., form edits), let that happen first 105 | 106 | if (!editContext.IsModified() || await JS.InvokeAsync("confirm", Localize["Discard changes?"].Value)) 107 | { 108 | overlay.Hide(); 109 | } 110 | } 111 | 112 | async Task HandlePhotoSelected(IFileListEntry[] files) 113 | { 114 | var sourceFile = files.FirstOrDefault(); 115 | if (sourceFile != null) 116 | { 117 | // Convert to reasonably-sized JPEG 118 | var imageFile = await sourceFile.ToImageFileAsync("image/jpeg", 800, 600); 119 | 120 | // Represent it as a data URL we can display 121 | var bytes = await imageFile.ReadAllAsync(); 122 | formData.PhotoUrl = bytes.ToDataUrl("image/jpeg"); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Client/Shared/VehicleNotes.razor: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer Localize 2 | 3 |
4 |
    5 | @foreach (var note in FilteredNotes) 6 | { 7 |
  • 8 |
    9 |

    @Localize[note.Location.DisplayName()]

    10 | @note.Text 11 |
    12 |
  • 13 | } 14 |
15 | 16 | @if (!FilteredNotes.Any() && FilterByLocation.HasValue) 17 | { 18 |
@Localize[FilterByLocation.Value.DisplayName()]
19 |
@Localize["No notes added."]
20 | } 21 | @{ var excludedNotes = CountExcludedNotes(); } 22 | @if (excludedNotes > 0) 23 | { 24 |
25 | + @Localize["{0} not shown.", excludedNotes] 26 | 27 |
28 | } 29 |
30 | 31 | @code { 32 | [Parameter] public Vehicle Vehicle { get; set; } 33 | [Parameter] public VehiclePart? FilterByLocation { get; set; } 34 | [Parameter] public EventCallback OnNoteClicked { get; set; } 35 | [Parameter] public EventCallback OnClearFilterRequested { get; set; } 36 | 37 | IEnumerable FilteredNotes => FilterByLocation.HasValue 38 | ? Vehicle.Notes.Where(n => n.Location == FilterByLocation.Value) 39 | : Vehicle.Notes; 40 | 41 | int CountExcludedNotes() => FilterByLocation.HasValue 42 | ? Vehicle.Notes.Count(n => n.Location != FilterByLocation.Value) 43 | : 0; 44 | } 45 | -------------------------------------------------------------------------------- /Client/Shared/VehicleSummary.razor: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer Localize 2 | 3 |
4 |

5 | @Vehicle.Make 6 | @Vehicle.Model 7 | @Vehicle.RegistrationDate.Year 8 |

9 |
10 | @Localize["Mileage:"] 11 | 12 | 13 | @Localize["Tank:"] 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |
24 | 25 | @code { 26 | [Parameter] public Vehicle Vehicle { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.ComponentModel.DataAnnotations 2 | @using System.Net 3 | @using System.Net.Http 4 | @using System.Net.Http.Json 5 | @using Microsoft.AspNetCore.Authorization 6 | @using Microsoft.AspNetCore.Components.Authorization 7 | @using Microsoft.AspNetCore.Components.Forms 8 | @using Microsoft.AspNetCore.Components.Routing 9 | @using Microsoft.AspNetCore.Components.Web 10 | @using Microsoft.AspNetCore.Components.WebAssembly.Authentication 11 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 12 | @using Microsoft.Extensions.Localization 13 | @using Microsoft.JSInterop 14 | @using CarChecker.Client 15 | @using CarChecker.Client.Data 16 | @using CarChecker.Client.Pages 17 | @using CarChecker.Client.Shared 18 | @using CarChecker.Shared 19 | @using SceneViewer 20 | -------------------------------------------------------------------------------- /Client/wwwroot/3d/sky-nx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspuij/CarChecker/e9c7e7114e7936e38389074f1b5005e4b27a0e8e/Client/wwwroot/3d/sky-nx.png -------------------------------------------------------------------------------- /Client/wwwroot/3d/sky-ny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspuij/CarChecker/e9c7e7114e7936e38389074f1b5005e4b27a0e8e/Client/wwwroot/3d/sky-ny.png -------------------------------------------------------------------------------- /Client/wwwroot/3d/sky-nz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspuij/CarChecker/e9c7e7114e7936e38389074f1b5005e4b27a0e8e/Client/wwwroot/3d/sky-nz.png -------------------------------------------------------------------------------- /Client/wwwroot/3d/sky-px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspuij/CarChecker/e9c7e7114e7936e38389074f1b5005e4b27a0e8e/Client/wwwroot/3d/sky-px.png -------------------------------------------------------------------------------- /Client/wwwroot/3d/sky-py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspuij/CarChecker/e9c7e7114e7936e38389074f1b5005e4b27a0e8e/Client/wwwroot/3d/sky-py.png -------------------------------------------------------------------------------- /Client/wwwroot/3d/sky-pz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspuij/CarChecker/e9c7e7114e7936e38389074f1b5005e4b27a0e8e/Client/wwwroot/3d/sky-pz.png -------------------------------------------------------------------------------- /Client/wwwroot/3d/sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspuij/CarChecker/e9c7e7114e7936e38389074f1b5005e4b27a0e8e/Client/wwwroot/3d/sky.png -------------------------------------------------------------------------------- /Client/wwwroot/css/blazor.css: -------------------------------------------------------------------------------- 1 | .valid.modified:not([type=checkbox]) { 2 | outline: 1px solid #26b050; 3 | } 4 | 5 | .invalid { 6 | outline: 1px solid red; 7 | } 8 | 9 | .validation-message { 10 | color: red; 11 | } 12 | 13 | #blazor-error-ui { 14 | background: lightyellow; 15 | bottom: 0; 16 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 17 | display: none; 18 | left: 0; 19 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 20 | position: fixed; 21 | width: 100%; 22 | z-index: 1000; 23 | } 24 | 25 | #blazor-error-ui .dismiss { 26 | cursor: pointer; 27 | position: absolute; 28 | right: 0.75rem; 29 | top: 0.5rem; 30 | } 31 | -------------------------------------------------------------------------------- /Client/wwwroot/css/spinner.css: -------------------------------------------------------------------------------- 1 | .loader, 2 | .loader:before, 3 | .loader:after { 4 | border-radius: 50%; 5 | width: 2.5em; 6 | height: 2.5em; 7 | -webkit-animation-fill-mode: both; 8 | animation-fill-mode: both; 9 | -webkit-animation: load7 1.8s infinite ease-in-out; 10 | animation: load7 1.8s infinite ease-in-out; 11 | } 12 | 13 | .loader { 14 | color: #ffffff; 15 | font-size: 10px; 16 | margin: 0px auto; 17 | margin-top: calc(var(--big-vertical-gutter-height) - 2em); 18 | position: relative; 19 | text-indent: -9999em; 20 | -webkit-transform: translateZ(0); 21 | -ms-transform: translateZ(0); 22 | transform: translateZ(0); 23 | -webkit-animation-delay: -0.16s; 24 | animation-delay: -0.16s; 25 | } 26 | 27 | .loader:before, 28 | .loader:after { 29 | content: ''; 30 | position: absolute; 31 | top: 0; 32 | } 33 | 34 | .loader:before { 35 | left: -3.5em; 36 | -webkit-animation-delay: -0.32s; 37 | animation-delay: -0.32s; 38 | } 39 | 40 | .loader:after { 41 | left: 3.5em; 42 | } 43 | 44 | @-webkit-keyframes load7 { 45 | 0%, 80%, 100% { 46 | box-shadow: 0 2.5em 0 -1.3em; 47 | } 48 | 49 | 40% { 50 | box-shadow: 0 2.5em 0 0; 51 | } 52 | } 53 | 54 | @keyframes load7 { 55 | 0%, 80%, 100% { 56 | box-shadow: 0 2.5em 0 -1.3em; 57 | } 58 | 59 | 40% { 60 | box-shadow: 0 2.5em 0 0; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Client/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspuij/CarChecker/e9c7e7114e7936e38389074f1b5005e4b27a0e8e/Client/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Client/wwwroot/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspuij/CarChecker/e9c7e7114e7936e38389074f1b5005e4b27a0e8e/Client/wwwroot/icon-512.png -------------------------------------------------------------------------------- /Client/wwwroot/idb.js: -------------------------------------------------------------------------------- 1 | // https://github.com/jakearchibald/idb/ 2 | var idb = function (e) { "use strict"; let t, n; const r = new WeakMap, o = new WeakMap, s = new WeakMap, a = new WeakMap, i = new WeakMap; let c = { get(e, t, n) { if (e instanceof IDBTransaction) { if ("done" === t) return o.get(e); if ("objectStoreNames" === t) return e.objectStoreNames || s.get(e); if ("store" === t) return n.objectStoreNames[1] ? void 0 : n.objectStore(n.objectStoreNames[0]) } return p(e[t]) }, set: (e, t, n) => (e[t] = n, !0), has: (e, t) => e instanceof IDBTransaction && ("done" === t || "store" === t) || t in e }; function u(e) { return e !== IDBDatabase.prototype.transaction || "objectStoreNames" in IDBTransaction.prototype ? (n || (n = [IDBCursor.prototype.advance, IDBCursor.prototype.continue, IDBCursor.prototype.continuePrimaryKey])).includes(e) ? function (...t) { return e.apply(f(this), t), p(r.get(this)) } : function (...t) { return p(e.apply(f(this), t)) } : function (t, ...n) { const r = e.call(f(this), t, ...n); return s.set(r, t.sort ? t.sort() : [t]), p(r) } } function d(e) { return "function" == typeof e ? u(e) : (e instanceof IDBTransaction && function (e) { if (o.has(e)) return; const t = new Promise((t, n) => { const r = () => { e.removeEventListener("complete", o), e.removeEventListener("error", s), e.removeEventListener("abort", s) }, o = () => { t(), r() }, s = () => { n(e.error || new DOMException("AbortError", "AbortError")), r() }; e.addEventListener("complete", o), e.addEventListener("error", s), e.addEventListener("abort", s) }); o.set(e, t) }(e), n = e, (t || (t = [IDBDatabase, IDBObjectStore, IDBIndex, IDBCursor, IDBTransaction])).some(e => n instanceof e) ? new Proxy(e, c) : e); var n } function p(e) { if (e instanceof IDBRequest) return function (e) { const t = new Promise((t, n) => { const r = () => { e.removeEventListener("success", o), e.removeEventListener("error", s) }, o = () => { t(p(e.result)), r() }, s = () => { n(e.error), r() }; e.addEventListener("success", o), e.addEventListener("error", s) }); return t.then(t => { t instanceof IDBCursor && r.set(t, e) }).catch(() => { }), i.set(t, e), t }(e); if (a.has(e)) return a.get(e); const t = d(e); return t !== e && (a.set(e, t), i.set(t, e)), t } const f = e => i.get(e); const l = ["get", "getKey", "getAll", "getAllKeys", "count"], D = ["put", "add", "delete", "clear"], v = new Map; function b(e, t) { if (!(e instanceof IDBDatabase) || t in e || "string" != typeof t) return; if (v.get(t)) return v.get(t); const n = t.replace(/FromIndex$/, ""), r = t !== n, o = D.includes(n); if (!(n in (r ? IDBIndex : IDBObjectStore).prototype) || !o && !l.includes(n)) return; const s = async function (e, ...t) { const s = this.transaction(e, o ? "readwrite" : "readonly"); let a = s.store; r && (a = a.index(t.shift())); const i = a[n](...t); return o && await s.done, i }; return v.set(t, s), s } return c = (e => ({ ...e, get: (t, n, r) => b(t, n) || e.get(t, n, r), has: (t, n) => !!b(t, n) || e.has(t, n) }))(c), e.deleteDB = function (e, { blocked: t } = {}) { const n = indexedDB.deleteDatabase(e); return t && n.addEventListener("blocked", () => t()), p(n).then(() => { }) }, e.openDB = function (e, t, { blocked: n, upgrade: r, blocking: o, terminated: s } = {}) { const a = indexedDB.open(e, t), i = p(a); return r && a.addEventListener("upgradeneeded", e => { r(p(a.result), e.oldVersion, e.newVersion, p(a.transaction)) }), n && a.addEventListener("blocked", () => n()), i.then(e => { s && e.addEventListener("close", () => s()), o && e.addEventListener("versionchange", () => o()) }).catch(() => { }), i }, e.unwrap = f, e.wrap = p, e }({}); 3 | -------------------------------------------------------------------------------- /Client/wwwroot/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Car Checker 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 | 23 |
24 | An unhandled error has occurred. 25 | Reload 26 | 🗙 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Client/wwwroot/localVehicleStore.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // This code exists to support functionality in LocalVehicleStore.cs. It provides convenient access to 3 | // the browser's IndexedDB APIs, along with a preconfigured database structure. 4 | 5 | const db = idb.openDB('Vehicles', 1, { 6 | upgrade(db) { 7 | db.createObjectStore('metadata'); 8 | db.createObjectStore('serverdata', { keyPath: 'licenseNumber' }).createIndex('lastUpdated', 'lastUpdated'); 9 | db.createObjectStore('localedits', { keyPath: 'licenseNumber' }); 10 | }, 11 | }); 12 | 13 | window.localVehicleStore = { 14 | get: async (storeName, key) => (await db).transaction(storeName).store.get(key), 15 | getAll: async (storeName) => (await db).transaction(storeName).store.getAll(), 16 | getFirstFromIndex: async (storeName, indexName, direction) => { 17 | const cursor = await (await db).transaction(storeName).store.index(indexName).openCursor(null, direction); 18 | return (cursor && cursor.value) || null; 19 | }, 20 | put: async (storeName, key, value) => (await db).transaction(storeName, 'readwrite').store.put(value, key === null ? undefined : key), 21 | putAllFromJson: async (storeName, json) => { 22 | const store = (await db).transaction(storeName, 'readwrite').store; 23 | JSON.parse(json).forEach(item => store.put(item)); 24 | }, 25 | delete: async (storeName, key) => (await db).transaction(storeName, 'readwrite').store.delete(key), 26 | autocompleteKeys: async (storeName, text, maxResults) => { 27 | const results = []; 28 | let cursor = await (await db).transaction(storeName).store.openCursor(IDBKeyRange.bound(text, text + '\uffff')); 29 | while (cursor && results.length < maxResults) { 30 | results.push(cursor.key); 31 | cursor = await cursor.continue(); 32 | } 33 | return results; 34 | } 35 | }; 36 | })(); 37 | -------------------------------------------------------------------------------- /Client/wwwroot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Car Checker", 3 | "short_name": "Car Checker", 4 | "start_url": "./", 5 | "display": "standalone", 6 | "background_color": "#e7e7e7", 7 | "theme_color": "#E85757", 8 | "icons": [ 9 | { 10 | "src": "icon-512.png", 11 | "type": "image/png", 12 | "sizes": "512x512" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Client/wwwroot/service-worker.js: -------------------------------------------------------------------------------- 1 | // In development, always fetch from the network and do not enable offline support. 2 | // This is because caching would make development more difficult (changes would not 3 | // be reflected on the first load after each change). 4 | self.addEventListener('fetch', () => { }); 5 | -------------------------------------------------------------------------------- /Client/wwwroot/service-worker.published.js: -------------------------------------------------------------------------------- 1 | // Caution! Be sure you understand the caveats before publishing an application with 2 | // offline support. See https://aka.ms/blazor-offline-considerations 3 | 4 | self.importScripts('./service-worker-assets.js'); 5 | self.addEventListener('install', event => event.waitUntil(onInstall(event))); 6 | self.addEventListener('activate', event => event.waitUntil(onActivate(event))); 7 | self.addEventListener('fetch', event => event.respondWith(onFetch(event))); 8 | 9 | const cacheNamePrefix = 'offline-cache-'; 10 | const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; 11 | const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/ ]; 12 | const offlineAssetsExclude = [ /^service-worker\.js$/ ]; 13 | 14 | async function onInstall(event) { 15 | console.info('Service worker: Install'); 16 | 17 | // Fetch and cache all matching items from the assets manifest 18 | const assetsRequests = self.assetsManifest.assets 19 | .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) 20 | .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) 21 | .map(asset => new Request(asset.url, { integrity: asset.hash })); 22 | 23 | // Also cache authentication configuration 24 | assetsRequests.push(new Request('_configuration/CarChecker.Client')); 25 | 26 | await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); 27 | } 28 | 29 | async function onActivate(event) { 30 | console.info('Service worker: Activate'); 31 | 32 | // Delete unused caches 33 | const cacheKeys = await caches.keys(); 34 | await Promise.all(cacheKeys 35 | .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) 36 | .map(key => caches.delete(key))); 37 | } 38 | 39 | async function onFetch(event) { 40 | let cachedResponse = null; 41 | if (event.request.method === 'GET') { 42 | // For all navigation requests, try to serve index.html from cache 43 | // If you need some URLs to be server-rendered, edit the following check to exclude those URLs 44 | const shouldServeIndexHtml = event.request.mode === 'navigate' 45 | && !event.request.url.includes('/connect/') 46 | && !event.request.url.includes('/Identity/'); 47 | 48 | const request = shouldServeIndexHtml ? 'index.html' : event.request; 49 | const cache = await caches.open(cacheName); 50 | cachedResponse = await cache.match(request); 51 | } 52 | 53 | return cachedResponse || fetch(event.request); 54 | } 55 | -------------------------------------------------------------------------------- /Client/wwwroot/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CarChecker - a Blazor sample app 2 | This is a sample application for [Blazor](https://blazor.net) which was presented at Build 2020. You can view the on-demand walk-through for this on Channel 9: [Modern Web UI with Blazor WebAssembly](https://aka.ms/blazor-in-action). 3 | 4 | To use this it's best to have Visual Studio 2019 and the latest .NET SDK for the Blazor release which you can read about here: [Blazor WebAssembly 3.2.0 now available](https://devblogs.microsoft.com/aspnet/blazor-webassembly-3-2-0-now-available/). 5 | 6 | ## What this sample has: 7 | This is a great sample which has a lot of Blazor + ASP.NET integrations such as: 8 | - client-side debugging with Visual Studio 9 | - Authentication / Authorization 10 | - input validation 11 | - data integration/sync 12 | - Blazor components 13 | - code sharing 14 | - JavaScript interop 15 | - Localization / internationalization 16 | - Progressive Web App (PWA) 17 | 18 | ## Deploy to Azure 19 | You can create the resource (Free SKU App Service) in Azure quickly by clicking below and then can use Visual Studio to select the App Service and deploy the code to it later. 20 | 21 | [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Ftimheuer%2FCarChecker%2Fmaster%2Fdeploy.json) 22 | 23 | ### Remarks 24 | This sample makes use of a preview package for Validation. -------------------------------------------------------------------------------- /Server/ApplicationUserClaimsPrincipalFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using CarChecker.Server.Models; 3 | using Microsoft.Extensions.Options; 4 | using System.Threading.Tasks; 5 | using System.Security.Claims; 6 | 7 | namespace CarChecker.Server 8 | { 9 | public class ApplicationUserClaimsPrincipalFactory : UserClaimsPrincipalFactory 10 | { 11 | public ApplicationUserClaimsPrincipalFactory( 12 | UserManager userManager, 13 | IOptions optionsAccessor) 14 | : base(userManager, optionsAccessor) 15 | { 16 | } 17 | 18 | protected override async Task GenerateClaimsAsync(ApplicationUser user) 19 | { 20 | var identity = await base.GenerateClaimsAsync(user); 21 | identity.AddClaim(new Claim("firstname", user.FirstName ?? "")); 22 | identity.AddClaim(new Claim("lastname", user.LastName ?? "")); 23 | return identity; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Server/Areas/Identity/IdentityHostingStartup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CarChecker.Server.Data; 3 | using CarChecker.Server.Models; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Identity.UI; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | [assembly: HostingStartup(typeof(CarChecker.Server.Areas.Identity.IdentityHostingStartup))] 12 | namespace CarChecker.Server.Areas.Identity 13 | { 14 | public class IdentityHostingStartup : IHostingStartup 15 | { 16 | public void Configure(IWebHostBuilder builder) 17 | { 18 | builder.ConfigureServices((context, services) => { 19 | }); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Server/Areas/Identity/Pages/Account/Manage/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @using CarChecker.Server.Areas.Identity.Pages.Account.Manage 3 | @model IndexModel 4 | @{ 5 | ViewData["Title"] = "Profile"; 6 | ViewData["ActivePage"] = "Index"; 7 | } 8 | 9 |

@ViewData["Title"]

10 | 11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 |
29 | 30 |
31 |
32 |
33 | 34 | @section Scripts { 35 | 36 | } -------------------------------------------------------------------------------- /Server/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using CarChecker.Server.Models; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.AspNetCore.Mvc.RazorPages; 10 | 11 | namespace CarChecker.Server.Areas.Identity.Pages.Account.Manage 12 | { 13 | public partial class IndexModel : PageModel 14 | { 15 | private readonly UserManager _userManager; 16 | private readonly SignInManager _signInManager; 17 | 18 | public IndexModel( 19 | UserManager userManager, 20 | SignInManager signInManager) 21 | { 22 | _userManager = userManager; 23 | _signInManager = signInManager; 24 | } 25 | 26 | public string Username { get; set; } 27 | 28 | [TempData] 29 | public string StatusMessage { get; set; } 30 | 31 | [BindProperty] 32 | public InputModel Input { get; set; } 33 | 34 | public class InputModel 35 | { 36 | [Required] 37 | [Display(Name = "First name")] 38 | public string FirstName { get; set; } 39 | 40 | [Required] 41 | [Display(Name = "Last name")] 42 | public string LastName { get; set; } 43 | } 44 | 45 | private async Task LoadAsync(ApplicationUser user) 46 | { 47 | var userName = await _userManager.GetUserNameAsync(user); 48 | 49 | Username = userName; 50 | 51 | Input = new InputModel 52 | { 53 | FirstName = user.FirstName, 54 | LastName = user.LastName, 55 | }; 56 | } 57 | 58 | public async Task OnGetAsync() 59 | { 60 | var user = await _userManager.GetUserAsync(User); 61 | if (user == null) 62 | { 63 | return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 64 | } 65 | 66 | await LoadAsync(user); 67 | return Page(); 68 | } 69 | 70 | public async Task OnPostAsync() 71 | { 72 | var user = await _userManager.GetUserAsync(User); 73 | if (user == null) 74 | { 75 | return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 76 | } 77 | 78 | if (!ModelState.IsValid) 79 | { 80 | await LoadAsync(user); 81 | return Page(); 82 | } 83 | 84 | user.FirstName = Input.FirstName; 85 | user.LastName = Input.LastName; 86 | await _userManager.UpdateAsync(user); 87 | await _signInManager.RefreshSignInAsync(user); 88 | StatusMessage = "Your profile has been updated"; 89 | return RedirectToPage(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Server/Areas/Identity/Pages/Account/Register.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model RegisterModel 3 | @{ 4 | ViewData["Title"] = "Register"; 5 | } 6 | 7 |

@ViewData["Title"]

8 | 9 |
10 |
11 |
12 |

Create a new account.

13 |
14 |
15 | 16 |
17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 |
31 |
32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 | 40 |
41 | 42 |
43 |
44 |
45 |
46 |

Use another service to register.

47 |
48 | @{ 49 | if ((Model.ExternalLogins?.Count ?? 0) == 0) 50 | { 51 |
52 |

53 | There are no external authentication services configured. See this article 54 | for details on setting up this ASP.NET application to support logging in via external services. 55 |

56 |
57 | } 58 | else 59 | { 60 |
61 |
62 |

63 | @foreach (var provider in Model.ExternalLogins) 64 | { 65 | 66 | } 67 |

68 |
69 |
70 | } 71 | } 72 |
73 |
74 |
75 | 76 | @section Scripts { 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Server/Areas/Identity/Pages/Account/Register.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.Encodings.Web; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Authentication; 9 | using Microsoft.AspNetCore.Authorization; 10 | using CarChecker.Server.Models; 11 | using Microsoft.AspNetCore.Identity; 12 | using Microsoft.AspNetCore.Identity.UI.Services; 13 | using Microsoft.AspNetCore.Mvc; 14 | using Microsoft.AspNetCore.Mvc.RazorPages; 15 | using Microsoft.AspNetCore.WebUtilities; 16 | using Microsoft.Extensions.Logging; 17 | 18 | namespace CarChecker.Server.Areas.Identity.Pages.Account 19 | { 20 | [AllowAnonymous] 21 | public class RegisterModel : PageModel 22 | { 23 | private readonly SignInManager _signInManager; 24 | private readonly UserManager _userManager; 25 | private readonly ILogger _logger; 26 | private readonly IEmailSender _emailSender; 27 | 28 | public RegisterModel( 29 | UserManager userManager, 30 | SignInManager signInManager, 31 | ILogger logger, 32 | IEmailSender emailSender) 33 | { 34 | _userManager = userManager; 35 | _signInManager = signInManager; 36 | _logger = logger; 37 | _emailSender = emailSender; 38 | } 39 | 40 | [BindProperty] 41 | public InputModel Input { get; set; } 42 | 43 | public string ReturnUrl { get; set; } 44 | 45 | public IList ExternalLogins { get; set; } 46 | 47 | public class InputModel 48 | { 49 | [Required] 50 | [Display(Name = "First name")] 51 | public string FirstName { get; set; } 52 | 53 | [Required] 54 | [Display(Name = "Last name")] 55 | public string LastName { get; set; } 56 | 57 | [Required] 58 | [EmailAddress] 59 | [Display(Name = "Email")] 60 | public string Email { get; set; } 61 | 62 | [Required] 63 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 64 | [DataType(DataType.Password)] 65 | [Display(Name = "Password")] 66 | public string Password { get; set; } 67 | 68 | [DataType(DataType.Password)] 69 | [Display(Name = "Confirm password")] 70 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 71 | public string ConfirmPassword { get; set; } 72 | } 73 | 74 | public async Task OnGetAsync(string returnUrl = null) 75 | { 76 | ReturnUrl = returnUrl; 77 | ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); 78 | } 79 | 80 | public async Task OnPostAsync(string returnUrl = null) 81 | { 82 | returnUrl = returnUrl ?? Url.Content("~/"); 83 | ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); 84 | if (ModelState.IsValid) 85 | { 86 | var user = new ApplicationUser { FirstName = Input.FirstName, LastName = Input.LastName, UserName = Input.Email, Email = Input.Email }; 87 | var result = await _userManager.CreateAsync(user, Input.Password); 88 | if (result.Succeeded) 89 | { 90 | _logger.LogInformation("User created a new account with password."); 91 | 92 | var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); 93 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 94 | var callbackUrl = Url.Page( 95 | "/Account/ConfirmEmail", 96 | pageHandler: null, 97 | values: new { area = "Identity", userId = user.Id, code = code }, 98 | protocol: Request.Scheme); 99 | 100 | await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", 101 | $"Please confirm your account by clicking here."); 102 | 103 | if (_userManager.Options.SignIn.RequireConfirmedAccount) 104 | { 105 | return RedirectToPage("RegisterConfirmation", new { email = Input.Email }); 106 | } 107 | else 108 | { 109 | await _signInManager.SignInAsync(user, isPersistent: false); 110 | return LocalRedirect(returnUrl); 111 | } 112 | } 113 | foreach (var error in result.Errors) 114 | { 115 | ModelState.AddModelError(string.Empty, error.Description); 116 | } 117 | } 118 | 119 | // If we got this far, something failed, redisplay form 120 | return Page(); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Server/Areas/Identity/Pages/Account/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using CarChecker.Server.Areas.Identity.Pages.Account -------------------------------------------------------------------------------- /Server/Areas/Identity/Pages/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using CarChecker.Server.Models 3 | @inject SignInManager SignInManager 4 | @inject UserManager UserManager 5 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 6 | 7 | @{ 8 | var returnUrl = "/"; 9 | if (Context.Request.Query.TryGetValue("returnUrl", out var existingUrl)) { 10 | returnUrl = existingUrl; 11 | } 12 | } 13 | 14 | 36 | -------------------------------------------------------------------------------- /Server/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /Server/Areas/Identity/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using CarChecker.Server.Areas.Identity 3 | @using CarChecker.Server.Areas.Identity.Pages 4 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 5 | @using CarChecker.Server.Models 6 | -------------------------------------------------------------------------------- /Server/Areas/Identity/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "/Areas/Identity/Pages/_Layout.cshtml"; 3 | } 4 | -------------------------------------------------------------------------------- /Server/CarChecker.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | CarChecker.Server-D3AE7875-C05E-4B16-9C28-3108207DE8BB 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Server/Controllers/DetectDamageController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CarChecker.Shared; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace CarChecker.Server.Controllers 7 | { 8 | [Authorize] 9 | [Route("api/[controller]")] 10 | public class DetectDamageController : ControllerBase 11 | { 12 | [HttpPost] 13 | public DamageDetectionResult PerformDamageDetection() 14 | { 15 | // Here we could read the uploaded image data from Request.Body, 16 | // and then pass that through to a pretrained ML model for 17 | // damage detection. 18 | // 19 | // However in this repo, we're not able to distribute the ML.NET 20 | // model data used in the demo, so the actual response here is 21 | // simply random. 22 | 23 | var rng = new Random(); 24 | return new DamageDetectionResult 25 | { 26 | IsDamaged = rng.Next(2) == 0, 27 | Score = 0.5 + rng.NextDouble() / 2, 28 | }; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Server/Controllers/OidcConfigurationController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.ApiAuthorization.IdentityServer; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace CarChecker.Server.Controllers 6 | { 7 | public class OidcConfigurationController : Controller 8 | { 9 | private readonly ILogger _logger; 10 | 11 | public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider, ILogger logger) 12 | { 13 | ClientRequestParametersProvider = clientRequestParametersProvider; 14 | _logger = logger; 15 | } 16 | 17 | public IClientRequestParametersProvider ClientRequestParametersProvider { get; } 18 | 19 | [HttpGet("_configuration/{clientId}")] 20 | public IActionResult GetClientRequestParameters([FromRoute]string clientId) 21 | { 22 | var parameters = ClientRequestParametersProvider.GetClientParameters(HttpContext, clientId); 23 | return Ok(parameters); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Server/Controllers/VehicleController.cs: -------------------------------------------------------------------------------- 1 | using CarChecker.Shared; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | using CarChecker.Server.Data; 9 | using Microsoft.EntityFrameworkCore; 10 | 11 | namespace CarChecker.Server.Controllers 12 | { 13 | [Authorize] 14 | [ApiController] 15 | [Route("api/[controller]/[action]")] 16 | public class VehicleController : ControllerBase 17 | { 18 | ApplicationDbContext db; 19 | 20 | public VehicleController(ApplicationDbContext db) 21 | { 22 | this.db = db; 23 | } 24 | 25 | public IEnumerable ChangedVehicles([FromQuery] DateTime since) 26 | { 27 | return db.Vehicles.Where(v => v.LastUpdated >= since).Include(v => v.Notes); 28 | } 29 | 30 | [HttpPut] 31 | public async Task Details(Vehicle vehicle) 32 | { 33 | var id = vehicle.LicenseNumber; 34 | var existingNotes = (await db.Vehicles.AsNoTracking().Include(v => v.Notes).SingleAsync(v => v.LicenseNumber == id)).Notes; 35 | var retainedNotes = vehicle.Notes.ToLookup(n => n.InspectionNoteId); 36 | var notesToDelete = existingNotes.Where(n => !retainedNotes.Contains(n.InspectionNoteId)); 37 | db.RemoveRange(notesToDelete); 38 | 39 | vehicle.LastUpdated = DateTime.Now; 40 | db.Vehicles.Update(vehicle); 41 | 42 | await db.SaveChangesAsync(); 43 | return Ok(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Server/Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using CarChecker.Server.Models; 2 | using CarChecker.Shared; 3 | using IdentityServer4.EntityFramework.Options; 4 | using Microsoft.AspNetCore.ApiAuthorization.IdentityServer; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Options; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | 12 | namespace CarChecker.Server.Data 13 | { 14 | public class ApplicationDbContext : ApiAuthorizationDbContext 15 | { 16 | public ApplicationDbContext( 17 | DbContextOptions options, 18 | IOptions operationalStoreOptions) : base(options, operationalStoreOptions) 19 | { 20 | } 21 | 22 | public DbSet Vehicles { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Server/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using CarChecker.Server.Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace CarChecker.Server.Data.Migrations 10 | { 11 | [DbContext(typeof(ApplicationDbContext))] 12 | [Migration("00000000000000_CreateIdentitySchema")] 13 | partial class CreateIdentitySchema 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "3.0.0-rc1.19455.8"); 20 | 21 | modelBuilder.Entity("CarChecker.Server.Models.ApplicationUser", b => 22 | { 23 | b.Property("Id") 24 | .HasColumnType("TEXT"); 25 | 26 | b.Property("AccessFailedCount") 27 | .HasColumnType("INTEGER"); 28 | 29 | b.Property("ConcurrencyStamp") 30 | .IsConcurrencyToken() 31 | .HasColumnType("TEXT"); 32 | 33 | b.Property("Email") 34 | .HasColumnType("TEXT") 35 | .HasMaxLength(256); 36 | 37 | b.Property("EmailConfirmed") 38 | .HasColumnType("INTEGER"); 39 | 40 | b.Property("LockoutEnabled") 41 | .HasColumnType("INTEGER"); 42 | 43 | b.Property("LockoutEnd") 44 | .HasColumnType("TEXT"); 45 | 46 | b.Property("NormalizedEmail") 47 | .HasColumnType("TEXT") 48 | .HasMaxLength(256); 49 | 50 | b.Property("NormalizedUserName") 51 | .HasColumnType("TEXT") 52 | .HasMaxLength(256); 53 | 54 | b.Property("PasswordHash") 55 | .HasColumnType("TEXT"); 56 | 57 | b.Property("PhoneNumber") 58 | .HasColumnType("TEXT"); 59 | 60 | b.Property("PhoneNumberConfirmed") 61 | .HasColumnType("INTEGER"); 62 | 63 | b.Property("SecurityStamp") 64 | .HasColumnType("TEXT"); 65 | 66 | b.Property("TwoFactorEnabled") 67 | .HasColumnType("INTEGER"); 68 | 69 | b.Property("UserName") 70 | .HasColumnType("TEXT") 71 | .HasMaxLength(256); 72 | 73 | b.HasKey("Id"); 74 | 75 | b.HasIndex("NormalizedEmail") 76 | .HasName("EmailIndex"); 77 | 78 | b.HasIndex("NormalizedUserName") 79 | .IsUnique() 80 | .HasName("UserNameIndex"); 81 | 82 | b.ToTable("AspNetUsers"); 83 | }); 84 | 85 | modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => 86 | { 87 | b.Property("UserCode") 88 | .HasColumnType("TEXT") 89 | .HasMaxLength(200); 90 | 91 | b.Property("ClientId") 92 | .IsRequired() 93 | .HasColumnType("TEXT") 94 | .HasMaxLength(200); 95 | 96 | b.Property("CreationTime") 97 | .HasColumnType("TEXT"); 98 | 99 | b.Property("Data") 100 | .IsRequired() 101 | .HasColumnType("TEXT") 102 | .HasMaxLength(50000); 103 | 104 | b.Property("DeviceCode") 105 | .IsRequired() 106 | .HasColumnType("TEXT") 107 | .HasMaxLength(200); 108 | 109 | b.Property("Expiration") 110 | .IsRequired() 111 | .HasColumnType("TEXT"); 112 | 113 | b.Property("SubjectId") 114 | .HasColumnType("TEXT") 115 | .HasMaxLength(200); 116 | 117 | b.HasKey("UserCode"); 118 | 119 | b.HasIndex("DeviceCode") 120 | .IsUnique(); 121 | 122 | b.HasIndex("Expiration"); 123 | 124 | b.ToTable("DeviceCodes"); 125 | }); 126 | 127 | modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => 128 | { 129 | b.Property("Key") 130 | .HasColumnType("TEXT") 131 | .HasMaxLength(200); 132 | 133 | b.Property("ClientId") 134 | .IsRequired() 135 | .HasColumnType("TEXT") 136 | .HasMaxLength(200); 137 | 138 | b.Property("CreationTime") 139 | .HasColumnType("TEXT"); 140 | 141 | b.Property("Data") 142 | .IsRequired() 143 | .HasColumnType("TEXT") 144 | .HasMaxLength(50000); 145 | 146 | b.Property("Expiration") 147 | .HasColumnType("TEXT"); 148 | 149 | b.Property("SubjectId") 150 | .HasColumnType("TEXT") 151 | .HasMaxLength(200); 152 | 153 | b.Property("Type") 154 | .IsRequired() 155 | .HasColumnType("TEXT") 156 | .HasMaxLength(50); 157 | 158 | b.HasKey("Key"); 159 | 160 | b.HasIndex("Expiration"); 161 | 162 | b.HasIndex("SubjectId", "ClientId", "Type"); 163 | 164 | b.ToTable("PersistedGrants"); 165 | }); 166 | 167 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 168 | { 169 | b.Property("Id") 170 | .HasColumnType("TEXT"); 171 | 172 | b.Property("ConcurrencyStamp") 173 | .IsConcurrencyToken() 174 | .HasColumnType("TEXT"); 175 | 176 | b.Property("Name") 177 | .HasColumnType("TEXT") 178 | .HasMaxLength(256); 179 | 180 | b.Property("NormalizedName") 181 | .HasColumnType("TEXT") 182 | .HasMaxLength(256); 183 | 184 | b.HasKey("Id"); 185 | 186 | b.HasIndex("NormalizedName") 187 | .IsUnique() 188 | .HasName("RoleNameIndex"); 189 | 190 | b.ToTable("AspNetRoles"); 191 | }); 192 | 193 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 194 | { 195 | b.Property("Id") 196 | .ValueGeneratedOnAdd() 197 | .HasColumnType("INTEGER"); 198 | 199 | b.Property("ClaimType") 200 | .HasColumnType("TEXT"); 201 | 202 | b.Property("ClaimValue") 203 | .HasColumnType("TEXT"); 204 | 205 | b.Property("RoleId") 206 | .IsRequired() 207 | .HasColumnType("TEXT"); 208 | 209 | b.HasKey("Id"); 210 | 211 | b.HasIndex("RoleId"); 212 | 213 | b.ToTable("AspNetRoleClaims"); 214 | }); 215 | 216 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 217 | { 218 | b.Property("Id") 219 | .ValueGeneratedOnAdd() 220 | .HasColumnType("INTEGER"); 221 | 222 | b.Property("ClaimType") 223 | .HasColumnType("TEXT"); 224 | 225 | b.Property("ClaimValue") 226 | .HasColumnType("TEXT"); 227 | 228 | b.Property("UserId") 229 | .IsRequired() 230 | .HasColumnType("TEXT"); 231 | 232 | b.HasKey("Id"); 233 | 234 | b.HasIndex("UserId"); 235 | 236 | b.ToTable("AspNetUserClaims"); 237 | }); 238 | 239 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 240 | { 241 | b.Property("LoginProvider") 242 | .HasColumnType("TEXT") 243 | .HasMaxLength(128); 244 | 245 | b.Property("ProviderKey") 246 | .HasColumnType("TEXT") 247 | .HasMaxLength(128); 248 | 249 | b.Property("ProviderDisplayName") 250 | .HasColumnType("TEXT"); 251 | 252 | b.Property("UserId") 253 | .IsRequired() 254 | .HasColumnType("TEXT"); 255 | 256 | b.HasKey("LoginProvider", "ProviderKey"); 257 | 258 | b.HasIndex("UserId"); 259 | 260 | b.ToTable("AspNetUserLogins"); 261 | }); 262 | 263 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 264 | { 265 | b.Property("UserId") 266 | .HasColumnType("TEXT"); 267 | 268 | b.Property("RoleId") 269 | .HasColumnType("TEXT"); 270 | 271 | b.HasKey("UserId", "RoleId"); 272 | 273 | b.HasIndex("RoleId"); 274 | 275 | b.ToTable("AspNetUserRoles"); 276 | }); 277 | 278 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 279 | { 280 | b.Property("UserId") 281 | .HasColumnType("TEXT"); 282 | 283 | b.Property("LoginProvider") 284 | .HasColumnType("TEXT") 285 | .HasMaxLength(128); 286 | 287 | b.Property("Name") 288 | .HasColumnType("TEXT") 289 | .HasMaxLength(128); 290 | 291 | b.Property("Value") 292 | .HasColumnType("TEXT"); 293 | 294 | b.HasKey("UserId", "LoginProvider", "Name"); 295 | 296 | b.ToTable("AspNetUserTokens"); 297 | }); 298 | 299 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 300 | { 301 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 302 | .WithMany() 303 | .HasForeignKey("RoleId") 304 | .OnDelete(DeleteBehavior.Cascade) 305 | .IsRequired(); 306 | }); 307 | 308 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 309 | { 310 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 311 | .WithMany() 312 | .HasForeignKey("UserId") 313 | .OnDelete(DeleteBehavior.Cascade) 314 | .IsRequired(); 315 | }); 316 | 317 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 318 | { 319 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 320 | .WithMany() 321 | .HasForeignKey("UserId") 322 | .OnDelete(DeleteBehavior.Cascade) 323 | .IsRequired(); 324 | }); 325 | 326 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 327 | { 328 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 329 | .WithMany() 330 | .HasForeignKey("RoleId") 331 | .OnDelete(DeleteBehavior.Cascade) 332 | .IsRequired(); 333 | 334 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 335 | .WithMany() 336 | .HasForeignKey("UserId") 337 | .OnDelete(DeleteBehavior.Cascade) 338 | .IsRequired(); 339 | }); 340 | 341 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 342 | { 343 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 344 | .WithMany() 345 | .HasForeignKey("UserId") 346 | .OnDelete(DeleteBehavior.Cascade) 347 | .IsRequired(); 348 | }); 349 | #pragma warning restore 612, 618 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /Server/Data/Migrations/00000000000000_CreateIdentitySchema.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace CarChecker.Server.Data.Migrations 5 | { 6 | public partial class CreateIdentitySchema : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "AspNetRoles", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false), 15 | Name = table.Column(maxLength: 256, nullable: true), 16 | NormalizedName = table.Column(maxLength: 256, nullable: true), 17 | ConcurrencyStamp = table.Column(nullable: true) 18 | }, 19 | constraints: table => 20 | { 21 | table.PrimaryKey("PK_AspNetRoles", x => x.Id); 22 | }); 23 | 24 | migrationBuilder.CreateTable( 25 | name: "AspNetUsers", 26 | columns: table => new 27 | { 28 | Id = table.Column(nullable: false), 29 | UserName = table.Column(maxLength: 256, nullable: true), 30 | NormalizedUserName = table.Column(maxLength: 256, nullable: true), 31 | Email = table.Column(maxLength: 256, nullable: true), 32 | NormalizedEmail = table.Column(maxLength: 256, nullable: true), 33 | EmailConfirmed = table.Column(nullable: false), 34 | PasswordHash = table.Column(nullable: true), 35 | SecurityStamp = table.Column(nullable: true), 36 | ConcurrencyStamp = table.Column(nullable: true), 37 | PhoneNumber = table.Column(nullable: true), 38 | PhoneNumberConfirmed = table.Column(nullable: false), 39 | TwoFactorEnabled = table.Column(nullable: false), 40 | LockoutEnd = table.Column(nullable: true), 41 | LockoutEnabled = table.Column(nullable: false), 42 | AccessFailedCount = table.Column(nullable: false) 43 | }, 44 | constraints: table => 45 | { 46 | table.PrimaryKey("PK_AspNetUsers", x => x.Id); 47 | }); 48 | 49 | migrationBuilder.CreateTable( 50 | name: "DeviceCodes", 51 | columns: table => new 52 | { 53 | UserCode = table.Column(maxLength: 200, nullable: false), 54 | DeviceCode = table.Column(maxLength: 200, nullable: false), 55 | SubjectId = table.Column(maxLength: 200, nullable: true), 56 | ClientId = table.Column(maxLength: 200, nullable: false), 57 | CreationTime = table.Column(nullable: false), 58 | Expiration = table.Column(nullable: false), 59 | Data = table.Column(maxLength: 50000, nullable: false) 60 | }, 61 | constraints: table => 62 | { 63 | table.PrimaryKey("PK_DeviceCodes", x => x.UserCode); 64 | }); 65 | 66 | migrationBuilder.CreateTable( 67 | name: "PersistedGrants", 68 | columns: table => new 69 | { 70 | Key = table.Column(maxLength: 200, nullable: false), 71 | Type = table.Column(maxLength: 50, nullable: false), 72 | SubjectId = table.Column(maxLength: 200, nullable: true), 73 | ClientId = table.Column(maxLength: 200, nullable: false), 74 | CreationTime = table.Column(nullable: false), 75 | Expiration = table.Column(nullable: true), 76 | Data = table.Column(maxLength: 50000, nullable: false) 77 | }, 78 | constraints: table => 79 | { 80 | table.PrimaryKey("PK_PersistedGrants", x => x.Key); 81 | }); 82 | 83 | migrationBuilder.CreateTable( 84 | name: "AspNetRoleClaims", 85 | columns: table => new 86 | { 87 | Id = table.Column(nullable: false) 88 | .Annotation("Sqlite:Autoincrement", true), 89 | RoleId = table.Column(nullable: false), 90 | ClaimType = table.Column(nullable: true), 91 | ClaimValue = table.Column(nullable: true) 92 | }, 93 | constraints: table => 94 | { 95 | table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); 96 | table.ForeignKey( 97 | name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", 98 | column: x => x.RoleId, 99 | principalTable: "AspNetRoles", 100 | principalColumn: "Id", 101 | onDelete: ReferentialAction.Cascade); 102 | }); 103 | 104 | migrationBuilder.CreateTable( 105 | name: "AspNetUserClaims", 106 | columns: table => new 107 | { 108 | Id = table.Column(nullable: false) 109 | .Annotation("Sqlite:Autoincrement", true), 110 | UserId = table.Column(nullable: false), 111 | ClaimType = table.Column(nullable: true), 112 | ClaimValue = table.Column(nullable: true) 113 | }, 114 | constraints: table => 115 | { 116 | table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); 117 | table.ForeignKey( 118 | name: "FK_AspNetUserClaims_AspNetUsers_UserId", 119 | column: x => x.UserId, 120 | principalTable: "AspNetUsers", 121 | principalColumn: "Id", 122 | onDelete: ReferentialAction.Cascade); 123 | }); 124 | 125 | migrationBuilder.CreateTable( 126 | name: "AspNetUserLogins", 127 | columns: table => new 128 | { 129 | LoginProvider = table.Column(maxLength: 128, nullable: false), 130 | ProviderKey = table.Column(maxLength: 128, nullable: false), 131 | ProviderDisplayName = table.Column(nullable: true), 132 | UserId = table.Column(nullable: false) 133 | }, 134 | constraints: table => 135 | { 136 | table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); 137 | table.ForeignKey( 138 | name: "FK_AspNetUserLogins_AspNetUsers_UserId", 139 | column: x => x.UserId, 140 | principalTable: "AspNetUsers", 141 | principalColumn: "Id", 142 | onDelete: ReferentialAction.Cascade); 143 | }); 144 | 145 | migrationBuilder.CreateTable( 146 | name: "AspNetUserRoles", 147 | columns: table => new 148 | { 149 | UserId = table.Column(nullable: false), 150 | RoleId = table.Column(nullable: false) 151 | }, 152 | constraints: table => 153 | { 154 | table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); 155 | table.ForeignKey( 156 | name: "FK_AspNetUserRoles_AspNetRoles_RoleId", 157 | column: x => x.RoleId, 158 | principalTable: "AspNetRoles", 159 | principalColumn: "Id", 160 | onDelete: ReferentialAction.Cascade); 161 | table.ForeignKey( 162 | name: "FK_AspNetUserRoles_AspNetUsers_UserId", 163 | column: x => x.UserId, 164 | principalTable: "AspNetUsers", 165 | principalColumn: "Id", 166 | onDelete: ReferentialAction.Cascade); 167 | }); 168 | 169 | migrationBuilder.CreateTable( 170 | name: "AspNetUserTokens", 171 | columns: table => new 172 | { 173 | UserId = table.Column(nullable: false), 174 | LoginProvider = table.Column(maxLength: 128, nullable: false), 175 | Name = table.Column(maxLength: 128, nullable: false), 176 | Value = table.Column(nullable: true) 177 | }, 178 | constraints: table => 179 | { 180 | table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); 181 | table.ForeignKey( 182 | name: "FK_AspNetUserTokens_AspNetUsers_UserId", 183 | column: x => x.UserId, 184 | principalTable: "AspNetUsers", 185 | principalColumn: "Id", 186 | onDelete: ReferentialAction.Cascade); 187 | }); 188 | 189 | migrationBuilder.CreateIndex( 190 | name: "IX_AspNetRoleClaims_RoleId", 191 | table: "AspNetRoleClaims", 192 | column: "RoleId"); 193 | 194 | migrationBuilder.CreateIndex( 195 | name: "RoleNameIndex", 196 | table: "AspNetRoles", 197 | column: "NormalizedName", 198 | unique: true); 199 | 200 | migrationBuilder.CreateIndex( 201 | name: "IX_AspNetUserClaims_UserId", 202 | table: "AspNetUserClaims", 203 | column: "UserId"); 204 | 205 | migrationBuilder.CreateIndex( 206 | name: "IX_AspNetUserLogins_UserId", 207 | table: "AspNetUserLogins", 208 | column: "UserId"); 209 | 210 | migrationBuilder.CreateIndex( 211 | name: "IX_AspNetUserRoles_RoleId", 212 | table: "AspNetUserRoles", 213 | column: "RoleId"); 214 | 215 | migrationBuilder.CreateIndex( 216 | name: "EmailIndex", 217 | table: "AspNetUsers", 218 | column: "NormalizedEmail"); 219 | 220 | migrationBuilder.CreateIndex( 221 | name: "UserNameIndex", 222 | table: "AspNetUsers", 223 | column: "NormalizedUserName", 224 | unique: true); 225 | 226 | migrationBuilder.CreateIndex( 227 | name: "IX_DeviceCodes_DeviceCode", 228 | table: "DeviceCodes", 229 | column: "DeviceCode", 230 | unique: true); 231 | 232 | migrationBuilder.CreateIndex( 233 | name: "IX_DeviceCodes_Expiration", 234 | table: "DeviceCodes", 235 | column: "Expiration"); 236 | 237 | migrationBuilder.CreateIndex( 238 | name: "IX_PersistedGrants_Expiration", 239 | table: "PersistedGrants", 240 | column: "Expiration"); 241 | 242 | migrationBuilder.CreateIndex( 243 | name: "IX_PersistedGrants_SubjectId_ClientId_Type", 244 | table: "PersistedGrants", 245 | columns: new[] { "SubjectId", "ClientId", "Type" }); 246 | } 247 | 248 | protected override void Down(MigrationBuilder migrationBuilder) 249 | { 250 | migrationBuilder.DropTable( 251 | name: "AspNetRoleClaims"); 252 | 253 | migrationBuilder.DropTable( 254 | name: "AspNetUserClaims"); 255 | 256 | migrationBuilder.DropTable( 257 | name: "AspNetUserLogins"); 258 | 259 | migrationBuilder.DropTable( 260 | name: "AspNetUserRoles"); 261 | 262 | migrationBuilder.DropTable( 263 | name: "AspNetUserTokens"); 264 | 265 | migrationBuilder.DropTable( 266 | name: "DeviceCodes"); 267 | 268 | migrationBuilder.DropTable( 269 | name: "PersistedGrants"); 270 | 271 | migrationBuilder.DropTable( 272 | name: "AspNetRoles"); 273 | 274 | migrationBuilder.DropTable( 275 | name: "AspNetUsers"); 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /Server/Data/Migrations/20200426112711_UserFirstLastNameFields.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using CarChecker.Server.Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace CarChecker.Server.Data.Migrations 10 | { 11 | [DbContext(typeof(ApplicationDbContext))] 12 | [Migration("20200426112711_UserFirstLastNameFields")] 13 | partial class UserFirstLastNameFields 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "3.1.3"); 20 | 21 | modelBuilder.Entity("CarChecker.Server.Models.ApplicationUser", b => 22 | { 23 | b.Property("Id") 24 | .HasColumnType("TEXT"); 25 | 26 | b.Property("AccessFailedCount") 27 | .HasColumnType("INTEGER"); 28 | 29 | b.Property("ConcurrencyStamp") 30 | .IsConcurrencyToken() 31 | .HasColumnType("TEXT"); 32 | 33 | b.Property("Email") 34 | .HasColumnType("TEXT") 35 | .HasMaxLength(256); 36 | 37 | b.Property("EmailConfirmed") 38 | .HasColumnType("INTEGER"); 39 | 40 | b.Property("FirstName") 41 | .HasColumnType("TEXT"); 42 | 43 | b.Property("LastName") 44 | .HasColumnType("TEXT"); 45 | 46 | b.Property("LockoutEnabled") 47 | .HasColumnType("INTEGER"); 48 | 49 | b.Property("LockoutEnd") 50 | .HasColumnType("TEXT"); 51 | 52 | b.Property("NormalizedEmail") 53 | .HasColumnType("TEXT") 54 | .HasMaxLength(256); 55 | 56 | b.Property("NormalizedUserName") 57 | .HasColumnType("TEXT") 58 | .HasMaxLength(256); 59 | 60 | b.Property("PasswordHash") 61 | .HasColumnType("TEXT"); 62 | 63 | b.Property("PhoneNumber") 64 | .HasColumnType("TEXT"); 65 | 66 | b.Property("PhoneNumberConfirmed") 67 | .HasColumnType("INTEGER"); 68 | 69 | b.Property("SecurityStamp") 70 | .HasColumnType("TEXT"); 71 | 72 | b.Property("TwoFactorEnabled") 73 | .HasColumnType("INTEGER"); 74 | 75 | b.Property("UserName") 76 | .HasColumnType("TEXT") 77 | .HasMaxLength(256); 78 | 79 | b.HasKey("Id"); 80 | 81 | b.HasIndex("NormalizedEmail") 82 | .HasName("EmailIndex"); 83 | 84 | b.HasIndex("NormalizedUserName") 85 | .IsUnique() 86 | .HasName("UserNameIndex"); 87 | 88 | b.ToTable("AspNetUsers"); 89 | }); 90 | 91 | modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => 92 | { 93 | b.Property("UserCode") 94 | .HasColumnType("TEXT") 95 | .HasMaxLength(200); 96 | 97 | b.Property("ClientId") 98 | .IsRequired() 99 | .HasColumnType("TEXT") 100 | .HasMaxLength(200); 101 | 102 | b.Property("CreationTime") 103 | .HasColumnType("TEXT"); 104 | 105 | b.Property("Data") 106 | .IsRequired() 107 | .HasColumnType("TEXT") 108 | .HasMaxLength(50000); 109 | 110 | b.Property("DeviceCode") 111 | .IsRequired() 112 | .HasColumnType("TEXT") 113 | .HasMaxLength(200); 114 | 115 | b.Property("Expiration") 116 | .IsRequired() 117 | .HasColumnType("TEXT"); 118 | 119 | b.Property("SubjectId") 120 | .HasColumnType("TEXT") 121 | .HasMaxLength(200); 122 | 123 | b.HasKey("UserCode"); 124 | 125 | b.HasIndex("DeviceCode") 126 | .IsUnique(); 127 | 128 | b.HasIndex("Expiration"); 129 | 130 | b.ToTable("DeviceCodes"); 131 | }); 132 | 133 | modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => 134 | { 135 | b.Property("Key") 136 | .HasColumnType("TEXT") 137 | .HasMaxLength(200); 138 | 139 | b.Property("ClientId") 140 | .IsRequired() 141 | .HasColumnType("TEXT") 142 | .HasMaxLength(200); 143 | 144 | b.Property("CreationTime") 145 | .HasColumnType("TEXT"); 146 | 147 | b.Property("Data") 148 | .IsRequired() 149 | .HasColumnType("TEXT") 150 | .HasMaxLength(50000); 151 | 152 | b.Property("Expiration") 153 | .HasColumnType("TEXT"); 154 | 155 | b.Property("SubjectId") 156 | .HasColumnType("TEXT") 157 | .HasMaxLength(200); 158 | 159 | b.Property("Type") 160 | .IsRequired() 161 | .HasColumnType("TEXT") 162 | .HasMaxLength(50); 163 | 164 | b.HasKey("Key"); 165 | 166 | b.HasIndex("Expiration"); 167 | 168 | b.HasIndex("SubjectId", "ClientId", "Type"); 169 | 170 | b.ToTable("PersistedGrants"); 171 | }); 172 | 173 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 174 | { 175 | b.Property("Id") 176 | .HasColumnType("TEXT"); 177 | 178 | b.Property("ConcurrencyStamp") 179 | .IsConcurrencyToken() 180 | .HasColumnType("TEXT"); 181 | 182 | b.Property("Name") 183 | .HasColumnType("TEXT") 184 | .HasMaxLength(256); 185 | 186 | b.Property("NormalizedName") 187 | .HasColumnType("TEXT") 188 | .HasMaxLength(256); 189 | 190 | b.HasKey("Id"); 191 | 192 | b.HasIndex("NormalizedName") 193 | .IsUnique() 194 | .HasName("RoleNameIndex"); 195 | 196 | b.ToTable("AspNetRoles"); 197 | }); 198 | 199 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 200 | { 201 | b.Property("Id") 202 | .ValueGeneratedOnAdd() 203 | .HasColumnType("INTEGER"); 204 | 205 | b.Property("ClaimType") 206 | .HasColumnType("TEXT"); 207 | 208 | b.Property("ClaimValue") 209 | .HasColumnType("TEXT"); 210 | 211 | b.Property("RoleId") 212 | .IsRequired() 213 | .HasColumnType("TEXT"); 214 | 215 | b.HasKey("Id"); 216 | 217 | b.HasIndex("RoleId"); 218 | 219 | b.ToTable("AspNetRoleClaims"); 220 | }); 221 | 222 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 223 | { 224 | b.Property("Id") 225 | .ValueGeneratedOnAdd() 226 | .HasColumnType("INTEGER"); 227 | 228 | b.Property("ClaimType") 229 | .HasColumnType("TEXT"); 230 | 231 | b.Property("ClaimValue") 232 | .HasColumnType("TEXT"); 233 | 234 | b.Property("UserId") 235 | .IsRequired() 236 | .HasColumnType("TEXT"); 237 | 238 | b.HasKey("Id"); 239 | 240 | b.HasIndex("UserId"); 241 | 242 | b.ToTable("AspNetUserClaims"); 243 | }); 244 | 245 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 246 | { 247 | b.Property("LoginProvider") 248 | .HasColumnType("TEXT") 249 | .HasMaxLength(128); 250 | 251 | b.Property("ProviderKey") 252 | .HasColumnType("TEXT") 253 | .HasMaxLength(128); 254 | 255 | b.Property("ProviderDisplayName") 256 | .HasColumnType("TEXT"); 257 | 258 | b.Property("UserId") 259 | .IsRequired() 260 | .HasColumnType("TEXT"); 261 | 262 | b.HasKey("LoginProvider", "ProviderKey"); 263 | 264 | b.HasIndex("UserId"); 265 | 266 | b.ToTable("AspNetUserLogins"); 267 | }); 268 | 269 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 270 | { 271 | b.Property("UserId") 272 | .HasColumnType("TEXT"); 273 | 274 | b.Property("RoleId") 275 | .HasColumnType("TEXT"); 276 | 277 | b.HasKey("UserId", "RoleId"); 278 | 279 | b.HasIndex("RoleId"); 280 | 281 | b.ToTable("AspNetUserRoles"); 282 | }); 283 | 284 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 285 | { 286 | b.Property("UserId") 287 | .HasColumnType("TEXT"); 288 | 289 | b.Property("LoginProvider") 290 | .HasColumnType("TEXT") 291 | .HasMaxLength(128); 292 | 293 | b.Property("Name") 294 | .HasColumnType("TEXT") 295 | .HasMaxLength(128); 296 | 297 | b.Property("Value") 298 | .HasColumnType("TEXT"); 299 | 300 | b.HasKey("UserId", "LoginProvider", "Name"); 301 | 302 | b.ToTable("AspNetUserTokens"); 303 | }); 304 | 305 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 306 | { 307 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 308 | .WithMany() 309 | .HasForeignKey("RoleId") 310 | .OnDelete(DeleteBehavior.Cascade) 311 | .IsRequired(); 312 | }); 313 | 314 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 315 | { 316 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 317 | .WithMany() 318 | .HasForeignKey("UserId") 319 | .OnDelete(DeleteBehavior.Cascade) 320 | .IsRequired(); 321 | }); 322 | 323 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 324 | { 325 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 326 | .WithMany() 327 | .HasForeignKey("UserId") 328 | .OnDelete(DeleteBehavior.Cascade) 329 | .IsRequired(); 330 | }); 331 | 332 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 333 | { 334 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 335 | .WithMany() 336 | .HasForeignKey("RoleId") 337 | .OnDelete(DeleteBehavior.Cascade) 338 | .IsRequired(); 339 | 340 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 341 | .WithMany() 342 | .HasForeignKey("UserId") 343 | .OnDelete(DeleteBehavior.Cascade) 344 | .IsRequired(); 345 | }); 346 | 347 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 348 | { 349 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 350 | .WithMany() 351 | .HasForeignKey("UserId") 352 | .OnDelete(DeleteBehavior.Cascade) 353 | .IsRequired(); 354 | }); 355 | #pragma warning restore 612, 618 356 | } 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /Server/Data/Migrations/20200426112711_UserFirstLastNameFields.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace CarChecker.Server.Data.Migrations 4 | { 5 | public partial class UserFirstLastNameFields : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "FirstName", 11 | table: "AspNetUsers", 12 | nullable: true); 13 | 14 | migrationBuilder.AddColumn( 15 | name: "LastName", 16 | table: "AspNetUsers", 17 | nullable: true); 18 | } 19 | 20 | protected override void Down(MigrationBuilder migrationBuilder) 21 | { 22 | migrationBuilder.DropColumn( 23 | name: "FirstName", 24 | table: "AspNetUsers"); 25 | 26 | migrationBuilder.DropColumn( 27 | name: "LastName", 28 | table: "AspNetUsers"); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Server/Data/Migrations/20200427140021_Vehicles.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using CarChecker.Server.Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace CarChecker.Server.Data.Migrations 10 | { 11 | [DbContext(typeof(ApplicationDbContext))] 12 | [Migration("20200427140021_Vehicles")] 13 | partial class Vehicles 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "3.1.3"); 20 | 21 | modelBuilder.Entity("CarChecker.Server.Models.ApplicationUser", b => 22 | { 23 | b.Property("Id") 24 | .HasColumnType("TEXT"); 25 | 26 | b.Property("AccessFailedCount") 27 | .HasColumnType("INTEGER"); 28 | 29 | b.Property("ConcurrencyStamp") 30 | .IsConcurrencyToken() 31 | .HasColumnType("TEXT"); 32 | 33 | b.Property("Email") 34 | .HasColumnType("TEXT") 35 | .HasMaxLength(256); 36 | 37 | b.Property("EmailConfirmed") 38 | .HasColumnType("INTEGER"); 39 | 40 | b.Property("FirstName") 41 | .HasColumnType("TEXT"); 42 | 43 | b.Property("LastName") 44 | .HasColumnType("TEXT"); 45 | 46 | b.Property("LockoutEnabled") 47 | .HasColumnType("INTEGER"); 48 | 49 | b.Property("LockoutEnd") 50 | .HasColumnType("TEXT"); 51 | 52 | b.Property("NormalizedEmail") 53 | .HasColumnType("TEXT") 54 | .HasMaxLength(256); 55 | 56 | b.Property("NormalizedUserName") 57 | .HasColumnType("TEXT") 58 | .HasMaxLength(256); 59 | 60 | b.Property("PasswordHash") 61 | .HasColumnType("TEXT"); 62 | 63 | b.Property("PhoneNumber") 64 | .HasColumnType("TEXT"); 65 | 66 | b.Property("PhoneNumberConfirmed") 67 | .HasColumnType("INTEGER"); 68 | 69 | b.Property("SecurityStamp") 70 | .HasColumnType("TEXT"); 71 | 72 | b.Property("TwoFactorEnabled") 73 | .HasColumnType("INTEGER"); 74 | 75 | b.Property("UserName") 76 | .HasColumnType("TEXT") 77 | .HasMaxLength(256); 78 | 79 | b.HasKey("Id"); 80 | 81 | b.HasIndex("NormalizedEmail") 82 | .HasName("EmailIndex"); 83 | 84 | b.HasIndex("NormalizedUserName") 85 | .IsUnique() 86 | .HasName("UserNameIndex"); 87 | 88 | b.ToTable("AspNetUsers"); 89 | }); 90 | 91 | modelBuilder.Entity("CarChecker.Shared.InspectionNote", b => 92 | { 93 | b.Property("InspectionNoteId") 94 | .ValueGeneratedOnAdd() 95 | .HasColumnType("INTEGER"); 96 | 97 | b.Property("Location") 98 | .HasColumnType("INTEGER"); 99 | 100 | b.Property("PhotoUrl") 101 | .HasColumnType("TEXT"); 102 | 103 | b.Property("Text") 104 | .IsRequired() 105 | .HasColumnType("TEXT") 106 | .HasMaxLength(100); 107 | 108 | b.Property("VehicleLicenseNumber") 109 | .HasColumnType("TEXT"); 110 | 111 | b.HasKey("InspectionNoteId"); 112 | 113 | b.HasIndex("VehicleLicenseNumber"); 114 | 115 | b.ToTable("InspectionNote"); 116 | }); 117 | 118 | modelBuilder.Entity("CarChecker.Shared.Vehicle", b => 119 | { 120 | b.Property("LicenseNumber") 121 | .HasColumnType("TEXT"); 122 | 123 | b.Property("LastUpdated") 124 | .HasColumnType("TEXT"); 125 | 126 | b.Property("Make") 127 | .IsRequired() 128 | .HasColumnType("TEXT"); 129 | 130 | b.Property("Mileage") 131 | .HasColumnType("INTEGER"); 132 | 133 | b.Property("Model") 134 | .IsRequired() 135 | .HasColumnType("TEXT"); 136 | 137 | b.Property("RegistrationDate") 138 | .HasColumnType("TEXT"); 139 | 140 | b.Property("Tank") 141 | .HasColumnType("INTEGER"); 142 | 143 | b.HasKey("LicenseNumber"); 144 | 145 | b.ToTable("Vehicles"); 146 | }); 147 | 148 | modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => 149 | { 150 | b.Property("UserCode") 151 | .HasColumnType("TEXT") 152 | .HasMaxLength(200); 153 | 154 | b.Property("ClientId") 155 | .IsRequired() 156 | .HasColumnType("TEXT") 157 | .HasMaxLength(200); 158 | 159 | b.Property("CreationTime") 160 | .HasColumnType("TEXT"); 161 | 162 | b.Property("Data") 163 | .IsRequired() 164 | .HasColumnType("TEXT") 165 | .HasMaxLength(50000); 166 | 167 | b.Property("DeviceCode") 168 | .IsRequired() 169 | .HasColumnType("TEXT") 170 | .HasMaxLength(200); 171 | 172 | b.Property("Expiration") 173 | .IsRequired() 174 | .HasColumnType("TEXT"); 175 | 176 | b.Property("SubjectId") 177 | .HasColumnType("TEXT") 178 | .HasMaxLength(200); 179 | 180 | b.HasKey("UserCode"); 181 | 182 | b.HasIndex("DeviceCode") 183 | .IsUnique(); 184 | 185 | b.HasIndex("Expiration"); 186 | 187 | b.ToTable("DeviceCodes"); 188 | }); 189 | 190 | modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => 191 | { 192 | b.Property("Key") 193 | .HasColumnType("TEXT") 194 | .HasMaxLength(200); 195 | 196 | b.Property("ClientId") 197 | .IsRequired() 198 | .HasColumnType("TEXT") 199 | .HasMaxLength(200); 200 | 201 | b.Property("CreationTime") 202 | .HasColumnType("TEXT"); 203 | 204 | b.Property("Data") 205 | .IsRequired() 206 | .HasColumnType("TEXT") 207 | .HasMaxLength(50000); 208 | 209 | b.Property("Expiration") 210 | .HasColumnType("TEXT"); 211 | 212 | b.Property("SubjectId") 213 | .HasColumnType("TEXT") 214 | .HasMaxLength(200); 215 | 216 | b.Property("Type") 217 | .IsRequired() 218 | .HasColumnType("TEXT") 219 | .HasMaxLength(50); 220 | 221 | b.HasKey("Key"); 222 | 223 | b.HasIndex("Expiration"); 224 | 225 | b.HasIndex("SubjectId", "ClientId", "Type"); 226 | 227 | b.ToTable("PersistedGrants"); 228 | }); 229 | 230 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 231 | { 232 | b.Property("Id") 233 | .HasColumnType("TEXT"); 234 | 235 | b.Property("ConcurrencyStamp") 236 | .IsConcurrencyToken() 237 | .HasColumnType("TEXT"); 238 | 239 | b.Property("Name") 240 | .HasColumnType("TEXT") 241 | .HasMaxLength(256); 242 | 243 | b.Property("NormalizedName") 244 | .HasColumnType("TEXT") 245 | .HasMaxLength(256); 246 | 247 | b.HasKey("Id"); 248 | 249 | b.HasIndex("NormalizedName") 250 | .IsUnique() 251 | .HasName("RoleNameIndex"); 252 | 253 | b.ToTable("AspNetRoles"); 254 | }); 255 | 256 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 257 | { 258 | b.Property("Id") 259 | .ValueGeneratedOnAdd() 260 | .HasColumnType("INTEGER"); 261 | 262 | b.Property("ClaimType") 263 | .HasColumnType("TEXT"); 264 | 265 | b.Property("ClaimValue") 266 | .HasColumnType("TEXT"); 267 | 268 | b.Property("RoleId") 269 | .IsRequired() 270 | .HasColumnType("TEXT"); 271 | 272 | b.HasKey("Id"); 273 | 274 | b.HasIndex("RoleId"); 275 | 276 | b.ToTable("AspNetRoleClaims"); 277 | }); 278 | 279 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 280 | { 281 | b.Property("Id") 282 | .ValueGeneratedOnAdd() 283 | .HasColumnType("INTEGER"); 284 | 285 | b.Property("ClaimType") 286 | .HasColumnType("TEXT"); 287 | 288 | b.Property("ClaimValue") 289 | .HasColumnType("TEXT"); 290 | 291 | b.Property("UserId") 292 | .IsRequired() 293 | .HasColumnType("TEXT"); 294 | 295 | b.HasKey("Id"); 296 | 297 | b.HasIndex("UserId"); 298 | 299 | b.ToTable("AspNetUserClaims"); 300 | }); 301 | 302 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 303 | { 304 | b.Property("LoginProvider") 305 | .HasColumnType("TEXT") 306 | .HasMaxLength(128); 307 | 308 | b.Property("ProviderKey") 309 | .HasColumnType("TEXT") 310 | .HasMaxLength(128); 311 | 312 | b.Property("ProviderDisplayName") 313 | .HasColumnType("TEXT"); 314 | 315 | b.Property("UserId") 316 | .IsRequired() 317 | .HasColumnType("TEXT"); 318 | 319 | b.HasKey("LoginProvider", "ProviderKey"); 320 | 321 | b.HasIndex("UserId"); 322 | 323 | b.ToTable("AspNetUserLogins"); 324 | }); 325 | 326 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 327 | { 328 | b.Property("UserId") 329 | .HasColumnType("TEXT"); 330 | 331 | b.Property("RoleId") 332 | .HasColumnType("TEXT"); 333 | 334 | b.HasKey("UserId", "RoleId"); 335 | 336 | b.HasIndex("RoleId"); 337 | 338 | b.ToTable("AspNetUserRoles"); 339 | }); 340 | 341 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 342 | { 343 | b.Property("UserId") 344 | .HasColumnType("TEXT"); 345 | 346 | b.Property("LoginProvider") 347 | .HasColumnType("TEXT") 348 | .HasMaxLength(128); 349 | 350 | b.Property("Name") 351 | .HasColumnType("TEXT") 352 | .HasMaxLength(128); 353 | 354 | b.Property("Value") 355 | .HasColumnType("TEXT"); 356 | 357 | b.HasKey("UserId", "LoginProvider", "Name"); 358 | 359 | b.ToTable("AspNetUserTokens"); 360 | }); 361 | 362 | modelBuilder.Entity("CarChecker.Shared.InspectionNote", b => 363 | { 364 | b.HasOne("CarChecker.Shared.Vehicle", null) 365 | .WithMany("Notes") 366 | .HasForeignKey("VehicleLicenseNumber"); 367 | }); 368 | 369 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 370 | { 371 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 372 | .WithMany() 373 | .HasForeignKey("RoleId") 374 | .OnDelete(DeleteBehavior.Cascade) 375 | .IsRequired(); 376 | }); 377 | 378 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 379 | { 380 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 381 | .WithMany() 382 | .HasForeignKey("UserId") 383 | .OnDelete(DeleteBehavior.Cascade) 384 | .IsRequired(); 385 | }); 386 | 387 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 388 | { 389 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 390 | .WithMany() 391 | .HasForeignKey("UserId") 392 | .OnDelete(DeleteBehavior.Cascade) 393 | .IsRequired(); 394 | }); 395 | 396 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 397 | { 398 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 399 | .WithMany() 400 | .HasForeignKey("RoleId") 401 | .OnDelete(DeleteBehavior.Cascade) 402 | .IsRequired(); 403 | 404 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 405 | .WithMany() 406 | .HasForeignKey("UserId") 407 | .OnDelete(DeleteBehavior.Cascade) 408 | .IsRequired(); 409 | }); 410 | 411 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 412 | { 413 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 414 | .WithMany() 415 | .HasForeignKey("UserId") 416 | .OnDelete(DeleteBehavior.Cascade) 417 | .IsRequired(); 418 | }); 419 | #pragma warning restore 612, 618 420 | } 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /Server/Data/Migrations/20200427140021_Vehicles.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace CarChecker.Server.Data.Migrations 5 | { 6 | public partial class Vehicles : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Vehicles", 12 | columns: table => new 13 | { 14 | LicenseNumber = table.Column(nullable: false), 15 | Make = table.Column(nullable: false), 16 | Model = table.Column(nullable: false), 17 | RegistrationDate = table.Column(nullable: false), 18 | Mileage = table.Column(nullable: false), 19 | Tank = table.Column(nullable: false), 20 | LastUpdated = table.Column(nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Vehicles", x => x.LicenseNumber); 25 | }); 26 | 27 | migrationBuilder.CreateTable( 28 | name: "InspectionNote", 29 | columns: table => new 30 | { 31 | InspectionNoteId = table.Column(nullable: false) 32 | .Annotation("Sqlite:Autoincrement", true), 33 | Location = table.Column(nullable: false), 34 | Text = table.Column(maxLength: 100, nullable: false), 35 | PhotoUrl = table.Column(nullable: true), 36 | VehicleLicenseNumber = table.Column(nullable: true) 37 | }, 38 | constraints: table => 39 | { 40 | table.PrimaryKey("PK_InspectionNote", x => x.InspectionNoteId); 41 | table.ForeignKey( 42 | name: "FK_InspectionNote_Vehicles_VehicleLicenseNumber", 43 | column: x => x.VehicleLicenseNumber, 44 | principalTable: "Vehicles", 45 | principalColumn: "LicenseNumber", 46 | onDelete: ReferentialAction.Restrict); 47 | }); 48 | 49 | migrationBuilder.CreateIndex( 50 | name: "IX_InspectionNote_VehicleLicenseNumber", 51 | table: "InspectionNote", 52 | column: "VehicleLicenseNumber"); 53 | } 54 | 55 | protected override void Down(MigrationBuilder migrationBuilder) 56 | { 57 | migrationBuilder.DropTable( 58 | name: "InspectionNote"); 59 | 60 | migrationBuilder.DropTable( 61 | name: "Vehicles"); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using CarChecker.Server.Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | 8 | namespace CarChecker.Server.Data.Migrations 9 | { 10 | [DbContext(typeof(ApplicationDbContext))] 11 | partial class ApplicationDbContextModelSnapshot : ModelSnapshot 12 | { 13 | protected override void BuildModel(ModelBuilder modelBuilder) 14 | { 15 | #pragma warning disable 612, 618 16 | modelBuilder 17 | .HasAnnotation("ProductVersion", "3.1.3"); 18 | 19 | modelBuilder.Entity("CarChecker.Server.Models.ApplicationUser", b => 20 | { 21 | b.Property("Id") 22 | .HasColumnType("TEXT"); 23 | 24 | b.Property("AccessFailedCount") 25 | .HasColumnType("INTEGER"); 26 | 27 | b.Property("ConcurrencyStamp") 28 | .IsConcurrencyToken() 29 | .HasColumnType("TEXT"); 30 | 31 | b.Property("Email") 32 | .HasColumnType("TEXT") 33 | .HasMaxLength(256); 34 | 35 | b.Property("EmailConfirmed") 36 | .HasColumnType("INTEGER"); 37 | 38 | b.Property("FirstName") 39 | .HasColumnType("TEXT"); 40 | 41 | b.Property("LastName") 42 | .HasColumnType("TEXT"); 43 | 44 | b.Property("LockoutEnabled") 45 | .HasColumnType("INTEGER"); 46 | 47 | b.Property("LockoutEnd") 48 | .HasColumnType("TEXT"); 49 | 50 | b.Property("NormalizedEmail") 51 | .HasColumnType("TEXT") 52 | .HasMaxLength(256); 53 | 54 | b.Property("NormalizedUserName") 55 | .HasColumnType("TEXT") 56 | .HasMaxLength(256); 57 | 58 | b.Property("PasswordHash") 59 | .HasColumnType("TEXT"); 60 | 61 | b.Property("PhoneNumber") 62 | .HasColumnType("TEXT"); 63 | 64 | b.Property("PhoneNumberConfirmed") 65 | .HasColumnType("INTEGER"); 66 | 67 | b.Property("SecurityStamp") 68 | .HasColumnType("TEXT"); 69 | 70 | b.Property("TwoFactorEnabled") 71 | .HasColumnType("INTEGER"); 72 | 73 | b.Property("UserName") 74 | .HasColumnType("TEXT") 75 | .HasMaxLength(256); 76 | 77 | b.HasKey("Id"); 78 | 79 | b.HasIndex("NormalizedEmail") 80 | .HasName("EmailIndex"); 81 | 82 | b.HasIndex("NormalizedUserName") 83 | .IsUnique() 84 | .HasName("UserNameIndex"); 85 | 86 | b.ToTable("AspNetUsers"); 87 | }); 88 | 89 | modelBuilder.Entity("CarChecker.Shared.InspectionNote", b => 90 | { 91 | b.Property("InspectionNoteId") 92 | .ValueGeneratedOnAdd() 93 | .HasColumnType("INTEGER"); 94 | 95 | b.Property("Location") 96 | .HasColumnType("INTEGER"); 97 | 98 | b.Property("PhotoUrl") 99 | .HasColumnType("TEXT"); 100 | 101 | b.Property("Text") 102 | .IsRequired() 103 | .HasColumnType("TEXT") 104 | .HasMaxLength(100); 105 | 106 | b.Property("VehicleLicenseNumber") 107 | .HasColumnType("TEXT"); 108 | 109 | b.HasKey("InspectionNoteId"); 110 | 111 | b.HasIndex("VehicleLicenseNumber"); 112 | 113 | b.ToTable("InspectionNote"); 114 | }); 115 | 116 | modelBuilder.Entity("CarChecker.Shared.Vehicle", b => 117 | { 118 | b.Property("LicenseNumber") 119 | .HasColumnType("TEXT"); 120 | 121 | b.Property("LastUpdated") 122 | .HasColumnType("TEXT"); 123 | 124 | b.Property("Make") 125 | .IsRequired() 126 | .HasColumnType("TEXT"); 127 | 128 | b.Property("Mileage") 129 | .HasColumnType("INTEGER"); 130 | 131 | b.Property("Model") 132 | .IsRequired() 133 | .HasColumnType("TEXT"); 134 | 135 | b.Property("RegistrationDate") 136 | .HasColumnType("TEXT"); 137 | 138 | b.Property("Tank") 139 | .HasColumnType("INTEGER"); 140 | 141 | b.HasKey("LicenseNumber"); 142 | 143 | b.ToTable("Vehicles"); 144 | }); 145 | 146 | modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => 147 | { 148 | b.Property("UserCode") 149 | .HasColumnType("TEXT") 150 | .HasMaxLength(200); 151 | 152 | b.Property("ClientId") 153 | .IsRequired() 154 | .HasColumnType("TEXT") 155 | .HasMaxLength(200); 156 | 157 | b.Property("CreationTime") 158 | .HasColumnType("TEXT"); 159 | 160 | b.Property("Data") 161 | .IsRequired() 162 | .HasColumnType("TEXT") 163 | .HasMaxLength(50000); 164 | 165 | b.Property("DeviceCode") 166 | .IsRequired() 167 | .HasColumnType("TEXT") 168 | .HasMaxLength(200); 169 | 170 | b.Property("Expiration") 171 | .IsRequired() 172 | .HasColumnType("TEXT"); 173 | 174 | b.Property("SubjectId") 175 | .HasColumnType("TEXT") 176 | .HasMaxLength(200); 177 | 178 | b.HasKey("UserCode"); 179 | 180 | b.HasIndex("DeviceCode") 181 | .IsUnique(); 182 | 183 | b.HasIndex("Expiration"); 184 | 185 | b.ToTable("DeviceCodes"); 186 | }); 187 | 188 | modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => 189 | { 190 | b.Property("Key") 191 | .HasColumnType("TEXT") 192 | .HasMaxLength(200); 193 | 194 | b.Property("ClientId") 195 | .IsRequired() 196 | .HasColumnType("TEXT") 197 | .HasMaxLength(200); 198 | 199 | b.Property("CreationTime") 200 | .HasColumnType("TEXT"); 201 | 202 | b.Property("Data") 203 | .IsRequired() 204 | .HasColumnType("TEXT") 205 | .HasMaxLength(50000); 206 | 207 | b.Property("Expiration") 208 | .HasColumnType("TEXT"); 209 | 210 | b.Property("SubjectId") 211 | .HasColumnType("TEXT") 212 | .HasMaxLength(200); 213 | 214 | b.Property("Type") 215 | .IsRequired() 216 | .HasColumnType("TEXT") 217 | .HasMaxLength(50); 218 | 219 | b.HasKey("Key"); 220 | 221 | b.HasIndex("Expiration"); 222 | 223 | b.HasIndex("SubjectId", "ClientId", "Type"); 224 | 225 | b.ToTable("PersistedGrants"); 226 | }); 227 | 228 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 229 | { 230 | b.Property("Id") 231 | .HasColumnType("TEXT"); 232 | 233 | b.Property("ConcurrencyStamp") 234 | .IsConcurrencyToken() 235 | .HasColumnType("TEXT"); 236 | 237 | b.Property("Name") 238 | .HasColumnType("TEXT") 239 | .HasMaxLength(256); 240 | 241 | b.Property("NormalizedName") 242 | .HasColumnType("TEXT") 243 | .HasMaxLength(256); 244 | 245 | b.HasKey("Id"); 246 | 247 | b.HasIndex("NormalizedName") 248 | .IsUnique() 249 | .HasName("RoleNameIndex"); 250 | 251 | b.ToTable("AspNetRoles"); 252 | }); 253 | 254 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 255 | { 256 | b.Property("Id") 257 | .ValueGeneratedOnAdd() 258 | .HasColumnType("INTEGER"); 259 | 260 | b.Property("ClaimType") 261 | .HasColumnType("TEXT"); 262 | 263 | b.Property("ClaimValue") 264 | .HasColumnType("TEXT"); 265 | 266 | b.Property("RoleId") 267 | .IsRequired() 268 | .HasColumnType("TEXT"); 269 | 270 | b.HasKey("Id"); 271 | 272 | b.HasIndex("RoleId"); 273 | 274 | b.ToTable("AspNetRoleClaims"); 275 | }); 276 | 277 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 278 | { 279 | b.Property("Id") 280 | .ValueGeneratedOnAdd() 281 | .HasColumnType("INTEGER"); 282 | 283 | b.Property("ClaimType") 284 | .HasColumnType("TEXT"); 285 | 286 | b.Property("ClaimValue") 287 | .HasColumnType("TEXT"); 288 | 289 | b.Property("UserId") 290 | .IsRequired() 291 | .HasColumnType("TEXT"); 292 | 293 | b.HasKey("Id"); 294 | 295 | b.HasIndex("UserId"); 296 | 297 | b.ToTable("AspNetUserClaims"); 298 | }); 299 | 300 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 301 | { 302 | b.Property("LoginProvider") 303 | .HasColumnType("TEXT") 304 | .HasMaxLength(128); 305 | 306 | b.Property("ProviderKey") 307 | .HasColumnType("TEXT") 308 | .HasMaxLength(128); 309 | 310 | b.Property("ProviderDisplayName") 311 | .HasColumnType("TEXT"); 312 | 313 | b.Property("UserId") 314 | .IsRequired() 315 | .HasColumnType("TEXT"); 316 | 317 | b.HasKey("LoginProvider", "ProviderKey"); 318 | 319 | b.HasIndex("UserId"); 320 | 321 | b.ToTable("AspNetUserLogins"); 322 | }); 323 | 324 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 325 | { 326 | b.Property("UserId") 327 | .HasColumnType("TEXT"); 328 | 329 | b.Property("RoleId") 330 | .HasColumnType("TEXT"); 331 | 332 | b.HasKey("UserId", "RoleId"); 333 | 334 | b.HasIndex("RoleId"); 335 | 336 | b.ToTable("AspNetUserRoles"); 337 | }); 338 | 339 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 340 | { 341 | b.Property("UserId") 342 | .HasColumnType("TEXT"); 343 | 344 | b.Property("LoginProvider") 345 | .HasColumnType("TEXT") 346 | .HasMaxLength(128); 347 | 348 | b.Property("Name") 349 | .HasColumnType("TEXT") 350 | .HasMaxLength(128); 351 | 352 | b.Property("Value") 353 | .HasColumnType("TEXT"); 354 | 355 | b.HasKey("UserId", "LoginProvider", "Name"); 356 | 357 | b.ToTable("AspNetUserTokens"); 358 | }); 359 | 360 | modelBuilder.Entity("CarChecker.Shared.InspectionNote", b => 361 | { 362 | b.HasOne("CarChecker.Shared.Vehicle", null) 363 | .WithMany("Notes") 364 | .HasForeignKey("VehicleLicenseNumber"); 365 | }); 366 | 367 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 368 | { 369 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 370 | .WithMany() 371 | .HasForeignKey("RoleId") 372 | .OnDelete(DeleteBehavior.Cascade) 373 | .IsRequired(); 374 | }); 375 | 376 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 377 | { 378 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 379 | .WithMany() 380 | .HasForeignKey("UserId") 381 | .OnDelete(DeleteBehavior.Cascade) 382 | .IsRequired(); 383 | }); 384 | 385 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 386 | { 387 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 388 | .WithMany() 389 | .HasForeignKey("UserId") 390 | .OnDelete(DeleteBehavior.Cascade) 391 | .IsRequired(); 392 | }); 393 | 394 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 395 | { 396 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 397 | .WithMany() 398 | .HasForeignKey("RoleId") 399 | .OnDelete(DeleteBehavior.Cascade) 400 | .IsRequired(); 401 | 402 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 403 | .WithMany() 404 | .HasForeignKey("UserId") 405 | .OnDelete(DeleteBehavior.Cascade) 406 | .IsRequired(); 407 | }); 408 | 409 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 410 | { 411 | b.HasOne("CarChecker.Server.Models.ApplicationUser", null) 412 | .WithMany() 413 | .HasForeignKey("UserId") 414 | .OnDelete(DeleteBehavior.Cascade) 415 | .IsRequired(); 416 | }); 417 | #pragma warning restore 612, 618 418 | } 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /Server/Models/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace CarChecker.Server.Models 8 | { 9 | public class ApplicationUser : IdentityUser 10 | { 11 | public string FirstName { get; set; } 12 | 13 | public string LastName { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Server/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model CarChecker.Server.Pages.ErrorModel 3 | @{ 4 | Layout = "_Layout"; 5 | ViewData["Title"] = "Error"; 6 | } 7 | 8 |

Error.

9 |

An error occurred while processing your request.

10 | 11 | @if (Model.ShowRequestId) 12 | { 13 |

14 | Request ID: @Model.RequestId 15 |

16 | } 17 | 18 |

Development Mode

19 |

20 | Swapping to the Development environment displays detailed information about the error that occurred. 21 |

22 |

23 | The Development environment shouldn't be enabled for deployed applications. 24 | It can result in displaying sensitive information from exceptions to end users. 25 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 26 | and restarting the app. 27 |

28 | -------------------------------------------------------------------------------- /Server/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace CarChecker.Server.Pages 11 | { 12 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 13 | public class ErrorModel : PageModel 14 | { 15 | public string RequestId { get; set; } 16 | 17 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 18 | 19 | private readonly ILogger _logger; 20 | 21 | public ErrorModel(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | public void OnGet() 27 | { 28 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Server/Pages/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | @ViewBag.Title 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | @RenderBody() 16 |
17 |
18 | 19 | 20 | 21 | @RenderSection("Scripts", required: false) 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Server/Pages/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using CarChecker.Server.Models 3 | 4 | @inject SignInManager SignInManager 5 | @inject UserManager UserManager 6 | 7 | 29 | -------------------------------------------------------------------------------- /Server/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using CarChecker.Server.Areas.Identity 3 | 4 | 5 | CarChecker.Server.Pages 6 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 7 | -------------------------------------------------------------------------------- /Server/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /Server/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CarChecker.Server.Data; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace CarChecker.Server 13 | { 14 | public class Program 15 | { 16 | public static void Main(string[] args) 17 | { 18 | var host = CreateHostBuilder(args).Build(); 19 | 20 | // Initialize the database 21 | var scopeFactory = host.Services.GetRequiredService(); 22 | using (var scope = scopeFactory.CreateScope()) 23 | { 24 | var db = scope.ServiceProvider.GetRequiredService(); 25 | if (db.Database.EnsureCreated()) 26 | { 27 | SeedData.Initialize(db); 28 | } 29 | } 30 | 31 | host.Run(); 32 | } 33 | 34 | public static IHostBuilder CreateHostBuilder(string[] args) => 35 | Host.CreateDefaultBuilder(args) 36 | .ConfigureWebHostDefaults(webBuilder => 37 | { 38 | webBuilder.UseStartup(); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:42252", 7 | "sslPort": 44300 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "CarChecker.Server": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 23 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Server/SeedData.cs: -------------------------------------------------------------------------------- 1 | using CarChecker.Server.Data; 2 | using CarChecker.Shared; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | 8 | namespace CarChecker.Server 9 | { 10 | public class SeedData 11 | { 12 | const int NumVehicles = 1000; 13 | static Random Random = new Random(); 14 | 15 | public static void Initialize(ApplicationDbContext db) 16 | { 17 | db.Vehicles.AddRange(CreateSeedData()); 18 | db.SaveChanges(); 19 | } 20 | 21 | private static IEnumerable CreateSeedData() 22 | { 23 | var makes = new[] { "Toyota", "Honda", "Mercedes", "Tesla", "BMW", "Kia", "Opel", "Mitsubishi", "Subaru", "Mazda", "Skoda", "Volkswagen", "Audi", "Chrysler", "Daewoo", "Peugeot", "Renault", "Seat", "Volvo", "Land Rover", "Porsche" }; 24 | var models = new[] { "Sprint", "Fury", "Explorer", "Discovery", "305", "920", "Brightside", "XS", "Traveller", "Wanderer", "Pace", "Espresso", "Expert", "Jupiter", "Neptune", "Prowler" }; 25 | 26 | for (var i = 0; i < NumVehicles; i++) 27 | { 28 | yield return new Vehicle 29 | { 30 | LicenseNumber = GenerateRandomLicenseNumber(), 31 | Make = PickRandom(makes), 32 | Model = PickRandom(models), 33 | RegistrationDate = new DateTime(PickRandomRange(2016, 2021), PickRandomRange(1, 13), PickRandomRange(1, 29)), 34 | LastUpdated = DateTime.Now, 35 | Mileage = PickRandomRange(500, 50000), 36 | Tank = PickRandomEnum(), 37 | Notes = Enumerable.Range(0, PickRandomRange(0, 5)).Select(_ => new InspectionNote 38 | { 39 | Location = PickRandomEnum(), 40 | Text = GenerateRandomNoteText() 41 | }).ToList() 42 | }; 43 | } 44 | } 45 | 46 | static string[] Adjectives = new[] { "Light", "Heavy", "Deep", "Long", "Short", "Substantial", "Slight", "Severe", "Problematic" }; 47 | static string[] Damages = new[] { "Scratch", "Dent", "Ding", "Break", "Discoloration" }; 48 | static string[] Relations = new[] { "towards", "behind", "near", "beside", "along" }; 49 | static string[] Positions = new[] { "Edge", "Side", "Top", "Back", "Front", "Inside", "Outside" }; 50 | 51 | private static string GenerateRandomNoteText() 52 | { 53 | return PickRandom(new[] 54 | { 55 | $"{PickRandom(Adjectives)} {PickRandom(Damages).ToLower()}", 56 | $"{PickRandom(Adjectives)} {PickRandom(Damages).ToLower()} {PickRandom(Relations)} {PickRandom(Positions).ToLower()}", 57 | $"{PickRandom(Positions)} has {PickRandom(Damages).ToLower()}", 58 | $"{PickRandom(Positions)} has {PickRandom(Adjectives).ToLower()} {PickRandom(Damages).ToLower()}", 59 | }); 60 | } 61 | 62 | private static int PickRandomRange(int minInc, int maxExc) 63 | { 64 | return Random.Next(minInc, maxExc); 65 | } 66 | 67 | private static T PickRandom(T[] values) 68 | { 69 | return values[Random.Next(values.Length)]; 70 | } 71 | 72 | public static T PickRandomEnum() 73 | { 74 | return PickRandom((T[])Enum.GetValues(typeof(T))); 75 | } 76 | 77 | private static string GenerateRandomLicenseNumber() 78 | { 79 | var result = new StringBuilder(); 80 | result.Append(Random.Next(10)); 81 | result.Append(Random.Next(10)); 82 | result.Append(Random.Next(10)); 83 | result.Append("-"); 84 | result.Append((char)Random.Next('A', 'Z' + 1)); 85 | result.Append((char)Random.Next('A', 'Z' + 1)); 86 | result.Append((char)Random.Next('A', 'Z' + 1)); 87 | return result.ToString(); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Server/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Components.Authorization; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.AspNetCore.Identity.UI; 6 | using Microsoft.AspNetCore.HttpsPolicy; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.ResponseCompression; 9 | using Microsoft.EntityFrameworkCore; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Hosting; 13 | using System.Linq; 14 | using CarChecker.Server.Data; 15 | using CarChecker.Server.Models; 16 | 17 | namespace CarChecker.Server 18 | { 19 | public class Startup 20 | { 21 | public Startup(IConfiguration configuration) 22 | { 23 | Configuration = configuration; 24 | } 25 | 26 | public IConfiguration Configuration { get; } 27 | 28 | // This method gets called by the runtime. Use this method to add services to the container. 29 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 30 | public void ConfigureServices(IServiceCollection services) 31 | { 32 | services.AddDbContext(options => 33 | options.UseSqlite( 34 | Configuration.GetConnectionString("DefaultConnection"))); 35 | 36 | services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) 37 | .AddEntityFrameworkStores(); 38 | 39 | services.AddIdentityServer() 40 | .AddApiAuthorization(options => 41 | { 42 | options.IdentityResources["profile"].UserClaims.Add("firstname"); 43 | options.IdentityResources["profile"].UserClaims.Add("lastname"); 44 | }); 45 | 46 | services.AddAuthentication() 47 | .AddIdentityServerJwt(); 48 | 49 | services.AddControllersWithViews(); 50 | services.AddRazorPages(); 51 | 52 | services.AddScoped, ApplicationUserClaimsPrincipalFactory>(); 53 | } 54 | 55 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 56 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 57 | { 58 | if (env.IsDevelopment()) 59 | { 60 | app.UseDeveloperExceptionPage(); 61 | app.UseDatabaseErrorPage(); 62 | app.UseWebAssemblyDebugging(); 63 | } 64 | else 65 | { 66 | app.UseExceptionHandler("/Error"); 67 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 68 | app.UseHsts(); 69 | } 70 | 71 | app.UseHttpsRedirection(); 72 | app.UseBlazorFrameworkFiles(); 73 | app.UseStaticFiles(); 74 | 75 | app.UseRouting(); 76 | 77 | app.UseIdentityServer(); 78 | app.UseAuthentication(); 79 | app.UseAuthorization(); 80 | 81 | app.UseEndpoints(endpoints => 82 | { 83 | endpoints.MapRazorPages(); 84 | endpoints.MapControllers(); 85 | endpoints.MapFallbackToFile("index.html"); 86 | }); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "IdentityServer": { 10 | "Key": { 11 | "Type": "Development" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "DataSource=app.db" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft": "Warning", 9 | "Microsoft.Hosting.Lifetime": "Information" 10 | } 11 | }, 12 | "IdentityServer": { 13 | "Clients": { 14 | "CarChecker.Client": { 15 | "Profile": "IdentityServerSPA" 16 | } 17 | }, 18 | "Key": { 19 | "Type": "Development" // For demo purposes only. Don't use a development key for real production apps. 20 | } 21 | }, 22 | "AllowedHosts": "*" 23 | } 24 | -------------------------------------------------------------------------------- /Server/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification\ 2 | for details on configuring this project to bundle and minify static web assets. */ 3 | 4 | a.navbar-brand { 5 | white-space: normal; 6 | text-align: center; 7 | word-break: break-all; 8 | } 9 | 10 | /* Sticky footer styles 11 | -------------------------------------------------- */ 12 | html { 13 | font-size: 14px; 14 | } 15 | @media (min-width: 768px) { 16 | html { 17 | font-size: 16px; 18 | } 19 | } 20 | 21 | .container { 22 | max-width: 960px; 23 | } 24 | 25 | .pricing-header { 26 | max-width: 700px; 27 | } 28 | 29 | .border-top { 30 | border-top: 1px solid #e5e5e5; 31 | } 32 | .border-bottom { 33 | border-bottom: 1px solid #e5e5e5; 34 | } 35 | 36 | .box-shadow { 37 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 38 | } 39 | 40 | button.accept-policy { 41 | font-size: 1rem; 42 | line-height: inherit; 43 | } 44 | 45 | /* Sticky footer styles 46 | -------------------------------------------------- */ 47 | html { 48 | position: relative; 49 | min-height: 100%; 50 | } 51 | 52 | body { 53 | /* Margin bottom by footer height */ 54 | margin-bottom: 60px; 55 | } 56 | .footer { 57 | position: absolute; 58 | bottom: 0; 59 | width: 100%; 60 | overflow: scroll; 61 | white-space: nowrap; 62 | /* Set the fixed height of the footer here */ 63 | height: 60px; 64 | line-height: 60px; /* Vertically center the text there */ 65 | } 66 | -------------------------------------------------------------------------------- /Shared/CarChecker.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Shared/DamageDetectionResult.cs: -------------------------------------------------------------------------------- 1 | namespace CarChecker.Shared 2 | { 3 | public class DamageDetectionResult 4 | { 5 | public bool IsDamaged { get; set; } 6 | public double Score { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Shared/FuelLevel.cs: -------------------------------------------------------------------------------- 1 | namespace CarChecker.Shared 2 | { 3 | public enum FuelLevel 4 | { 5 | Empty, 6 | Quarter, 7 | Half, 8 | ThreeQuarters, 9 | Full 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Shared/InspectionNote.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace CarChecker.Shared 6 | { 7 | public class InspectionNote 8 | { 9 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 10 | [JsonPropertyName("id")] 11 | public int InspectionNoteId { get; set; } 12 | 13 | [Required] 14 | public VehiclePart Location { get; set; } 15 | 16 | [Required] 17 | [StringLength(100)] 18 | public string Text { get; set; } 19 | 20 | public string PhotoUrl { get; set; } 21 | 22 | public void CopyFrom(InspectionNote other) 23 | { 24 | Location = other.Location; 25 | Text = other.Text; 26 | PhotoUrl = other.PhotoUrl; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Shared/Vehicle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace CarChecker.Shared 6 | { 7 | public class Vehicle 8 | { 9 | [Key] 10 | public string LicenseNumber { get; set; } 11 | 12 | [Required] 13 | public string Make { get; set; } 14 | 15 | [Required] 16 | public string Model { get; set; } 17 | 18 | [Required] 19 | public DateTime RegistrationDate { get; set; } 20 | 21 | [Range(1, 1000000)] 22 | public int Mileage { get; set; } 23 | 24 | [Required] 25 | public FuelLevel Tank { get; set; } 26 | 27 | public List Notes { get; set; } 28 | 29 | [Required] 30 | public DateTime LastUpdated { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Shared/VehiclePart.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace CarChecker.Shared 4 | { 5 | public enum VehiclePart 6 | { 7 | BodyFront, 8 | BodyFrontLeft, 9 | BodyFrontRight, 10 | BodyRear, 11 | BodyRearLeft, 12 | BodyRearRight, 13 | Bonnet, 14 | DoorFrontLeft, 15 | DoorFrontRight, 16 | DoorRearLeft, 17 | DoorRearRight, 18 | Grill, 19 | HeadLightLeft, 20 | HeadLightRight, 21 | MirrorLeft, 22 | MirrorRight, 23 | Roof, 24 | TailLightLeft, 25 | TailLightRight, 26 | Undercarriage, 27 | WheelArchFrontLeft, 28 | WheelArchFrontRight, 29 | WheelArchRearLeft, 30 | WheelArchRearRight, 31 | WheelFrontLeft, 32 | WheelFrontRight, 33 | WheelRearLeft, 34 | WheelRearRight, 35 | WindowBack, 36 | WindowFrontLeft, 37 | WindowFrontRight, 38 | WindowRearLeft, 39 | WindowRearRight, 40 | Windshield, 41 | } 42 | 43 | public static class VehiclePartExtensions 44 | { 45 | private static Regex InnerCapital = new Regex("(.)([A-Z])"); 46 | 47 | public static string DisplayName(this VehiclePart part) 48 | { 49 | return InnerCapital.Replace(part.ToString(), m => $"{m.Groups[1].Value} {m.Groups[2].Value}"); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "webAppName": { 6 | "type": "string", 7 | "defaultValue": "[concat('BlazorApp-', uniqueString(resourceGroup().id))]", 8 | "metadata":{ 9 | "description": "That name is the name of our application. It has to be unique. Type a name followed by your resource group name. (-)" 10 | } 11 | }, 12 | "location": { 13 | "type": "string", 14 | "defaultValue": "[resourceGroup().location]", 15 | "metadata":{ 16 | "description": "Location for all resources." 17 | } 18 | } 19 | }, 20 | "variables": { 21 | "alwaysOn": false, 22 | "sku": "Free", 23 | "skuCode": "F1", 24 | "workerSize": "0", 25 | "workerSizeId": 0, 26 | "numberOfWorkers": "1", 27 | "currentStack": "dotnetcore", 28 | "netFrameworkVersion": "v4.0", 29 | "hostingPlanName": "[concat('hpn-', resourceGroup().name)]" 30 | }, 31 | "resources": [ 32 | { 33 | "apiVersion": "2018-02-01", 34 | "name": "[parameters('webAppName')]", 35 | "type": "Microsoft.Web/sites", 36 | "location": "[parameters('location')]", 37 | "dependsOn": [ 38 | "[resourceId('Microsoft.Web/serverfarms/', variables('hostingPlanName'))]" 39 | ], 40 | "properties": { 41 | "name": "[parameters('webAppName')]", 42 | "siteConfig": { 43 | "metadata": [ 44 | { 45 | "name": "CURRENT_STACK", 46 | "value": "[variables('currentStack')]" 47 | } 48 | ], 49 | "netFrameworkVersion": "[variables('netFrameworkVersion')]", 50 | "alwaysOn": "[variables('alwaysOn')]" 51 | }, 52 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 53 | "clientAffinityEnabled": true 54 | } 55 | }, 56 | { 57 | "apiVersion": "2018-02-01", 58 | "name": "[variables('hostingPlanName')]", 59 | "type": "Microsoft.Web/serverfarms", 60 | "location": "[parameters('location')]", 61 | "properties": { 62 | "name": "[variables('hostingPlanName')]", 63 | "workerSize": "[variables('workerSize')]", 64 | "workerSizeId": "[variables('workerSizeId')]", 65 | "numberOfWorkers": "[variables('numberOfWorkers')]" 66 | }, 67 | "sku": { 68 | "Tier": "[variables('sku')]", 69 | "Name": "[variables('skuCode')]" 70 | } 71 | } 72 | ] 73 | } -------------------------------------------------------------------------------- /deploy.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "webAppName": { 6 | "value": "GEN-UNIQUE" 7 | } 8 | } 9 | } --------------------------------------------------------------------------------