parent
8f76544e4c
commit
3aef36ff14
@ -0,0 +1,16 @@ |
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> |
||||
|
||||
<PropertyGroup> |
||||
<TargetFramework>net8.0</TargetFramework> |
||||
<ImplicitUsings>enable</ImplicitUsings> |
||||
<Nullable>enable</Nullable> |
||||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile> |
||||
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode> |
||||
</PropertyGroup> |
||||
|
||||
<ItemGroup> |
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.7" /> |
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.7" /> |
||||
</ItemGroup> |
||||
|
||||
</Project> |
@ -0,0 +1,41 @@ |
||||
using Microsoft.AspNetCore.Components; |
||||
using Microsoft.AspNetCore.Components.Authorization; |
||||
using System.Security.Claims; |
||||
|
||||
namespace AppIdentity.Client |
||||
{ |
||||
// This is a client-side AuthenticationStateProvider that determines the user's authentication state by |
||||
// looking for data persisted in the page when it was rendered on the server. This authentication state will |
||||
// be fixed for the lifetime of the WebAssembly application. So, if the user needs to log in or out, a full |
||||
// page reload is required. |
||||
// |
||||
// This only provides a user name and email for display purposes. It does not actually include any tokens |
||||
// that authenticate to the server when making subsequent requests. That works separately using a |
||||
// cookie that will be included on HttpClient requests to the server. |
||||
internal class PersistentAuthenticationStateProvider : AuthenticationStateProvider |
||||
{ |
||||
private static readonly Task<AuthenticationState> defaultUnauthenticatedTask = |
||||
Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); |
||||
|
||||
private readonly Task<AuthenticationState> authenticationStateTask = defaultUnauthenticatedTask; |
||||
|
||||
public PersistentAuthenticationStateProvider(PersistentComponentState state) |
||||
{ |
||||
if (!state.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
Claim[] claims = [ |
||||
new Claim(ClaimTypes.NameIdentifier, userInfo.UserId), |
||||
new Claim(ClaimTypes.Name, userInfo.Email), |
||||
new Claim(ClaimTypes.Email, userInfo.Email) ]; |
||||
|
||||
authenticationStateTask = Task.FromResult( |
||||
new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, |
||||
authenticationType: nameof(PersistentAuthenticationStateProvider))))); |
||||
} |
||||
|
||||
public override Task<AuthenticationState> GetAuthenticationStateAsync() => authenticationStateTask; |
||||
} |
||||
} |
@ -0,0 +1,20 @@ |
||||
using AppIdentity.Client; |
||||
using Microsoft.AspNetCore.Components.Authorization; |
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; |
||||
|
||||
namespace AppIdentity.Client |
||||
{ |
||||
internal class Program |
||||
{ |
||||
static async Task Main(string[] args) |
||||
{ |
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args); |
||||
|
||||
builder.Services.AddAuthorizationCore(); |
||||
builder.Services.AddCascadingAuthenticationState(); |
||||
builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>(); |
||||
|
||||
await builder.Build().RunAsync(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
namespace AppIdentity.Client |
||||
{ |
||||
// Add properties to this class and update the server and client AuthenticationStateProviders |
||||
// to expose more information about the authenticated user to the client. |
||||
public class UserInfo |
||||
{ |
||||
public required string UserId { get; set; } |
||||
public required string Email { get; set; } |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
{ |
||||
"Logging": { |
||||
"LogLevel": { |
||||
"Default": "Information", |
||||
"Microsoft.AspNetCore": "Warning" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
{ |
||||
"Logging": { |
||||
"LogLevel": { |
||||
"Default": "Information", |
||||
"Microsoft.AspNetCore": "Warning" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
<Project Sdk="Microsoft.NET.Sdk.Web"> |
||||
|
||||
<PropertyGroup> |
||||
<TargetFramework>net8.0</TargetFramework> |
||||
<Nullable>enable</Nullable> |
||||
<ImplicitUsings>enable</ImplicitUsings> |
||||
<UserSecretsId>aspnet-AppIdentity-8ce3ac08-489c-4e92-9a9f-090045dd22bd</UserSecretsId> |
||||
</PropertyGroup> |
||||
|
||||
<ItemGroup> |
||||
<ProjectReference Include="..\AppIdentity.Client\AppIdentity.Client.csproj" /> |
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.7" /> |
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.7" /> |
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" /> |
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.7" /> |
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7" /> |
||||
</ItemGroup> |
||||
|
||||
</Project> |
@ -0,0 +1,113 @@ |
||||
using AppIdentity.Components.Account.Pages; |
||||
using AppIdentity.Components.Account.Pages.Manage; |
||||
using AppIdentity.Data; |
||||
using Microsoft.AspNetCore.Authentication; |
||||
using Microsoft.AspNetCore.Components.Authorization; |
||||
using Microsoft.AspNetCore.Http.Extensions; |
||||
using Microsoft.AspNetCore.Identity; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
using Microsoft.Extensions.Primitives; |
||||
using System.Security.Claims; |
||||
using System.Text.Json; |
||||
|
||||
namespace Microsoft.AspNetCore.Routing |
||||
{ |
||||
internal static class IdentityComponentsEndpointRouteBuilderExtensions |
||||
{ |
||||
// These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project. |
||||
public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints) |
||||
{ |
||||
ArgumentNullException.ThrowIfNull(endpoints); |
||||
|
||||
var accountGroup = endpoints.MapGroup("/Account"); |
||||
|
||||
accountGroup.MapPost("/PerformExternalLogin", ( |
||||
HttpContext context, |
||||
[FromServices] SignInManager<ApplicationUser> signInManager, |
||||
[FromForm] string provider, |
||||
[FromForm] string returnUrl) => |
||||
{ |
||||
IEnumerable<KeyValuePair<string, StringValues>> query = [ |
||||
new("ReturnUrl", returnUrl), |
||||
new("Action", ExternalLogin.LoginCallbackAction)]; |
||||
|
||||
var redirectUrl = UriHelper.BuildRelative( |
||||
context.Request.PathBase, |
||||
"/Account/ExternalLogin", |
||||
QueryString.Create(query)); |
||||
|
||||
var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); |
||||
return TypedResults.Challenge(properties, [provider]); |
||||
}); |
||||
|
||||
accountGroup.MapPost("/Logout", async ( |
||||
ClaimsPrincipal user, |
||||
SignInManager<ApplicationUser> signInManager, |
||||
[FromForm] string returnUrl) => |
||||
{ |
||||
await signInManager.SignOutAsync(); |
||||
return TypedResults.LocalRedirect($"~/{returnUrl}"); |
||||
}); |
||||
|
||||
var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization(); |
||||
|
||||
manageGroup.MapPost("/LinkExternalLogin", async ( |
||||
HttpContext context, |
||||
[FromServices] SignInManager<ApplicationUser> signInManager, |
||||
[FromForm] string provider) => |
||||
{ |
||||
// Clear the existing external cookie to ensure a clean login process |
||||
await context.SignOutAsync(IdentityConstants.ExternalScheme); |
||||
|
||||
var redirectUrl = UriHelper.BuildRelative( |
||||
context.Request.PathBase, |
||||
"/Account/Manage/ExternalLogins", |
||||
QueryString.Create("Action", ExternalLogins.LinkLoginCallbackAction)); |
||||
|
||||
var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, signInManager.UserManager.GetUserId(context.User)); |
||||
return TypedResults.Challenge(properties, [provider]); |
||||
}); |
||||
|
||||
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>(); |
||||
var downloadLogger = loggerFactory.CreateLogger("DownloadPersonalData"); |
||||
|
||||
manageGroup.MapPost("/DownloadPersonalData", async ( |
||||
HttpContext context, |
||||
[FromServices] UserManager<ApplicationUser> userManager, |
||||
[FromServices] AuthenticationStateProvider authenticationStateProvider) => |
||||
{ |
||||
var user = await userManager.GetUserAsync(context.User); |
||||
if (user is null) |
||||
{ |
||||
return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'."); |
||||
} |
||||
|
||||
var userId = await userManager.GetUserIdAsync(user); |
||||
downloadLogger.LogInformation("User with ID '{UserId}' asked for their personal data.", userId); |
||||
|
||||
// Only include personal data for download |
||||
var personalData = new Dictionary<string, string>(); |
||||
var personalDataProps = typeof(ApplicationUser).GetProperties().Where( |
||||
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); |
||||
foreach (var p in personalDataProps) |
||||
{ |
||||
personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); |
||||
} |
||||
|
||||
var logins = await userManager.GetLoginsAsync(user); |
||||
foreach (var l in logins) |
||||
{ |
||||
personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey); |
||||
} |
||||
|
||||
personalData.Add("Authenticator Key", (await userManager.GetAuthenticatorKeyAsync(user))!); |
||||
var fileBytes = JsonSerializer.SerializeToUtf8Bytes(personalData); |
||||
|
||||
context.Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json"); |
||||
return TypedResults.File(fileBytes, contentType: "application/json", fileDownloadName: "PersonalData.json"); |
||||
}); |
||||
|
||||
return accountGroup; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
using AppIdentity.Data; |
||||
using Microsoft.AspNetCore.Identity; |
||||
using Microsoft.AspNetCore.Identity.UI.Services; |
||||
|
||||
namespace AppIdentity.Components.Account |
||||
{ |
||||
// Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation. |
||||
internal sealed class IdentityNoOpEmailSender : IEmailSender<ApplicationUser> |
||||
{ |
||||
private readonly IEmailSender emailSender = new NoOpEmailSender(); |
||||
|
||||
public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) => |
||||
emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>."); |
||||
|
||||
public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) => |
||||
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by <a href='{resetLink}'>clicking here</a>."); |
||||
|
||||
public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) => |
||||
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}"); |
||||
} |
||||
} |
@ -0,0 +1,59 @@ |
||||
using Microsoft.AspNetCore.Components; |
||||
using System.Diagnostics.CodeAnalysis; |
||||
|
||||
namespace AppIdentity.Components.Account |
||||
{ |
||||
internal sealed class IdentityRedirectManager(NavigationManager navigationManager) |
||||
{ |
||||
public const string StatusCookieName = "Identity.StatusMessage"; |
||||
|
||||
private static readonly CookieBuilder StatusCookieBuilder = new() |
||||
{ |
||||
SameSite = SameSiteMode.Strict, |
||||
HttpOnly = true, |
||||
IsEssential = true, |
||||
MaxAge = TimeSpan.FromSeconds(5), |
||||
}; |
||||
|
||||
[DoesNotReturn] |
||||
public void RedirectTo(string? uri) |
||||
{ |
||||
uri ??= ""; |
||||
|
||||
// Prevent open redirects. |
||||
if (!Uri.IsWellFormedUriString(uri, UriKind.Relative)) |
||||
{ |
||||
uri = navigationManager.ToBaseRelativePath(uri); |
||||
} |
||||
|
||||
// During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect. |
||||
// So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown. |
||||
navigationManager.NavigateTo(uri); |
||||
throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering."); |
||||
} |
||||
|
||||
[DoesNotReturn] |
||||
public void RedirectTo(string uri, Dictionary<string, object?> queryParameters) |
||||
{ |
||||
var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path); |
||||
var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters); |
||||
RedirectTo(newUri); |
||||
} |
||||
|
||||
[DoesNotReturn] |
||||
public void RedirectToWithStatus(string uri, string message, HttpContext context) |
||||
{ |
||||
context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context)); |
||||
RedirectTo(uri); |
||||
} |
||||
|
||||
private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path); |
||||
|
||||
[DoesNotReturn] |
||||
public void RedirectToCurrentPage() => RedirectTo(CurrentPath); |
||||
|
||||
[DoesNotReturn] |
||||
public void RedirectToCurrentPageWithStatus(string message, HttpContext context) |
||||
=> RedirectToWithStatus(CurrentPath, message, context); |
||||
} |
||||
} |
@ -0,0 +1,20 @@ |
||||
using AppIdentity.Data; |
||||
using Microsoft.AspNetCore.Identity; |
||||
|
||||
namespace AppIdentity.Components.Account |
||||
{ |
||||
internal sealed class IdentityUserAccessor(UserManager<ApplicationUser> userManager, IdentityRedirectManager redirectManager) |
||||
{ |
||||
public async Task<ApplicationUser> GetRequiredUserAsync(HttpContext context) |
||||
{ |
||||
var user = await userManager.GetUserAsync(context.User); |
||||
|
||||
if (user is null) |
||||
{ |
||||
redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context); |
||||
} |
||||
|
||||
return user; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,110 @@ |
||||
using AppIdentity.Client; |
||||
using AppIdentity.Data; |
||||
using Microsoft.AspNetCore.Components; |
||||
using Microsoft.AspNetCore.Components.Authorization; |
||||
using Microsoft.AspNetCore.Components.Server; |
||||
using Microsoft.AspNetCore.Components.Web; |
||||
using Microsoft.AspNetCore.Identity; |
||||
using Microsoft.Extensions.Options; |
||||
using System.Diagnostics; |
||||
using System.Security.Claims; |
||||
|
||||
namespace AppIdentity.Components.Account |
||||
{ |
||||
// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user |
||||
// every 30 minutes an interactive circuit is connected. It also uses PersistentComponentState to flow the |
||||
// authentication state to the client which is then fixed for the lifetime of the WebAssembly application. |
||||
internal sealed class PersistingRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider |
||||
{ |
||||
private readonly IServiceScopeFactory scopeFactory; |
||||
private readonly PersistentComponentState state; |
||||
private readonly IdentityOptions options; |
||||
|
||||
private readonly PersistingComponentStateSubscription subscription; |
||||
|
||||
private Task<AuthenticationState>? authenticationStateTask; |
||||
|
||||
public PersistingRevalidatingAuthenticationStateProvider( |
||||
ILoggerFactory loggerFactory, |
||||
IServiceScopeFactory serviceScopeFactory, |
||||
PersistentComponentState persistentComponentState, |
||||
IOptions<IdentityOptions> optionsAccessor) |
||||
: base(loggerFactory) |
||||
{ |
||||
scopeFactory = serviceScopeFactory; |
||||
state = persistentComponentState; |
||||
options = optionsAccessor.Value; |
||||
|
||||
AuthenticationStateChanged += OnAuthenticationStateChanged; |
||||
subscription = state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly); |
||||
} |
||||
|
||||
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); |
||||
|
||||
protected override async Task<bool> ValidateAuthenticationStateAsync( |
||||
AuthenticationState authenticationState, CancellationToken cancellationToken) |
||||
{ |
||||
// Get the user manager from a new scope to ensure it fetches fresh data |
||||
await using var scope = scopeFactory.CreateAsyncScope(); |
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); |
||||
return await ValidateSecurityStampAsync(userManager, authenticationState.User); |
||||
} |
||||
|
||||
private async Task<bool> ValidateSecurityStampAsync(UserManager<ApplicationUser> userManager, ClaimsPrincipal principal) |
||||
{ |
||||
var user = await userManager.GetUserAsync(principal); |
||||
if (user is null) |
||||
{ |
||||
return false; |
||||
} |
||||
else if (!userManager.SupportsUserSecurityStamp) |
||||
{ |
||||
return true; |
||||
} |
||||
else |
||||
{ |
||||
var principalStamp = principal.FindFirstValue(options.ClaimsIdentity.SecurityStampClaimType); |
||||
var userStamp = await userManager.GetSecurityStampAsync(user); |
||||
return principalStamp == userStamp; |
||||
} |
||||
} |
||||
|
||||
private void OnAuthenticationStateChanged(Task<AuthenticationState> task) |
||||
{ |
||||
authenticationStateTask = task; |
||||
} |
||||
|
||||
private async Task OnPersistingAsync() |
||||
{ |
||||
if (authenticationStateTask is null) |
||||
{ |
||||
throw new UnreachableException($"Authentication state not set in {nameof(OnPersistingAsync)}()."); |
||||
} |
||||
|
||||
var authenticationState = await authenticationStateTask; |
||||
var principal = authenticationState.User; |
||||
|
||||
if (principal.Identity?.IsAuthenticated == true) |
||||
{ |
||||
var userId = principal.FindFirst(options.ClaimsIdentity.UserIdClaimType)?.Value; |
||||
var email = principal.FindFirst(options.ClaimsIdentity.EmailClaimType)?.Value; |
||||
|
||||
if (userId != null && email != null) |
||||
{ |
||||
state.PersistAsJson(nameof(UserInfo), new UserInfo |
||||
{ |
||||
UserId = userId, |
||||
Email = email, |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
|
||||
protected override void Dispose(bool disposing) |
||||
{ |
||||
subscription.Dispose(); |
||||
AuthenticationStateChanged -= OnAuthenticationStateChanged; |
||||
base.Dispose(disposing); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,96 @@ |
||||
.page { |
||||
position: relative; |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
|
||||
main { |
||||
flex: 1; |
||||
} |
||||
|
||||
.sidebar { |
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); |
||||
} |
||||
|
||||
.top-row { |
||||
background-color: #f7f7f7; |
||||
border-bottom: 1px solid #d6d5d5; |
||||
justify-content: flex-end; |
||||
height: 3.5rem; |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link { |
||||
white-space: nowrap; |
||||
margin-left: 1.5rem; |
||||
text-decoration: none; |
||||
} |
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover { |
||||
text-decoration: underline; |
||||
} |
||||
|
||||
.top-row ::deep a:first-child { |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
|
||||
@media (max-width: 640.98px) { |
||||
.top-row { |
||||
justify-content: space-between; |
||||
} |
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link { |
||||
margin-left: 0; |
||||
} |
||||
} |
||||
|
||||
@media (min-width: 641px) { |
||||
.page { |
||||
flex-direction: row; |
||||
} |
||||
|
||||
.sidebar { |
||||
width: 250px; |
||||
height: 100vh; |
||||
position: sticky; |
||||
top: 0; |
||||
} |
||||
|
||||
.top-row { |
||||
position: sticky; |
||||
top: 0; |
||||
z-index: 1; |
||||
} |
||||
|
||||
.top-row.auth ::deep a:first-child { |
||||
flex: 1; |
||||
text-align: right; |
||||
width: 0; |
||||
} |
||||
|
||||
.top-row, article { |
||||
padding-left: 2rem !important; |
||||
padding-right: 1.5rem !important; |
||||
} |
||||
} |
||||
|
||||
#blazor-error-ui { |
||||
background: lightyellow; |
||||
bottom: 0; |
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); |
||||
display: none; |
||||
left: 0; |
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem; |
||||
position: fixed; |
||||
width: 100%; |
||||
z-index: 1000; |
||||
} |
||||
|
||||
#blazor-error-ui .dismiss { |
||||
cursor: pointer; |
||||
position: absolute; |
||||
right: 0.75rem; |
||||
top: 0.5rem; |
||||
} |
@ -0,0 +1,125 @@ |
||||
.navbar-toggler { |
||||
appearance: none; |
||||
cursor: pointer; |
||||
width: 3.5rem; |
||||
height: 2.5rem; |
||||
color: white; |
||||
position: absolute; |
||||
top: 0.5rem; |
||||
right: 1rem; |
||||
border: 1px solid rgba(255, 255, 255, 0.1); |
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); |
||||
} |
||||
|
||||
.navbar-toggler:checked { |
||||
background-color: rgba(255, 255, 255, 0.5); |
||||
} |
||||
|
||||
.top-row { |
||||
height: 3.5rem; |
||||
background-color: rgba(0,0,0,0.4); |
||||
} |
||||
|
||||
.navbar-brand { |
||||
font-size: 1.1rem; |
||||
} |
||||
|
||||
.bi { |
||||
display: inline-block; |
||||
position: relative; |
||||
width: 1.25rem; |
||||
height: 1.25rem; |
||||
margin-right: 0.75rem; |
||||
top: -1px; |
||||
background-size: cover; |
||||
} |
||||
|
||||
.bi-house-door-fill-nav-menu { |
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); |
||||
} |
||||
|
||||
.bi-plus-square-fill-nav-menu { |
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); |
||||
} |
||||
|
||||
.bi-list-nested-nav-menu { |
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); |
||||
} |
||||
|
||||
.bi-lock-nav-menu { |
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3C/svg%3E"); |
||||
} |
||||
|
||||
.bi-person-nav-menu { |
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person' viewBox='0 0 16 16'%3E%3Cpath d='M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z'/%3E%3C/svg%3E"); |
||||
} |
||||
|
||||
.bi-person-badge-nav-menu { |
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-badge' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'/%3E%3Cpath d='M4.5 0A2.5 2.5 0 0 0 2 2.5V14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2.5A2.5 2.5 0 0 0 11.5 0h-7zM3 2.5A1.5 1.5 0 0 1 4.5 1h7A1.5 1.5 0 0 1 13 2.5v10.795a4.2 4.2 0 0 0-.776-.492C11.392 12.387 10.063 12 8 12s-3.392.387-4.224.803a4.2 4.2 0 0 0-.776.492V2.5z'/%3E%3C/svg%3E"); |
||||
} |
||||
|
||||
.bi-person-fill-nav-menu { |
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-fill' viewBox='0 0 16 16'%3E%3Cpath d='M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3C/svg%3E"); |
||||
} |
||||
|
||||
.bi-arrow-bar-left-nav-menu { |
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-arrow-bar-left' viewBox='0 0 16 16'%3E%3Cpath d='M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5ZM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5Z'/%3E%3C/svg%3E"); |
||||
} |
||||
|
||||
.nav-item { |
||||
font-size: 0.9rem; |
||||
padding-bottom: 0.5rem; |
||||
} |
||||
|
||||
.nav-item:first-of-type { |
||||
padding-top: 1rem; |
||||
} |
||||
|
||||
.nav-item:last-of-type { |
||||
padding-bottom: 1rem; |
||||
} |
||||
|
||||
.nav-item ::deep .nav-link { |
||||
color: #d7d7d7; |
||||
background: none; |
||||
border: none; |
||||
border-radius: 4px; |
||||
height: 3rem; |
||||
display: flex; |
||||
align-items: center; |
||||
line-height: 3rem; |
||||
width: 100%; |
||||
} |
||||
|
||||
.nav-item ::deep a.active { |
||||
background-color: rgba(255,255,255,0.37); |
||||
color: white; |
||||
} |
||||
|
||||
.nav-item ::deep .nav-link:hover { |
||||
background-color: rgba(255,255,255,0.1); |
||||
color: white; |
||||
} |
||||
|
||||
.nav-scrollable { |
||||
display: none; |
||||
} |
||||
|
||||
.navbar-toggler:checked ~ .nav-scrollable { |
||||
display: block; |
||||
} |
||||
|
||||
@media (min-width: 641px) { |
||||
.navbar-toggler { |
||||
display: none; |
||||
} |
||||
|
||||
.nav-scrollable { |
||||
/* Never collapse the sidebar for wide screens */ |
||||
display: block; |
||||
|
||||
/* Allow sidebar to scroll for tall menus */ |
||||
height: calc(100vh - 3.5rem); |
||||
overflow-y: auto; |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace AppIdentity.Data |
||||
{ |
||||
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ApplicationUser>(options) |
||||
{ |
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
using Microsoft.AspNetCore.Identity; |
||||
|
||||
namespace AppIdentity.Data |
||||
{ |
||||
// Add profile data for application users by adding properties to the ApplicationUser class |
||||
public class ApplicationUser : IdentityUser |
||||
{ |
||||
} |
||||
|
||||
} |
@ -0,0 +1,279 @@ |
||||
// <auto-generated /> |
||||
using AppIdentity.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Infrastructure; |
||||
using Microsoft.EntityFrameworkCore.Metadata; |
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |
||||
using System; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace AppIdentity.Migrations |
||||
{ |
||||
[DbContext(typeof(ApplicationDbContext))] |
||||
[Migration("00000000000000_CreateIdentitySchema")] |
||||
partial class CreateIdentitySchema |
||||
{ |
||||
/// <inheritdoc /> |
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder) |
||||
{ |
||||
#pragma warning disable 612, 618 |
||||
modelBuilder |
||||
.HasAnnotation("ProductVersion", "8.0.0") |
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128); |
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); |
||||
|
||||
modelBuilder.Entity("AppIdentity.Data.ApplicationUser", b => |
||||
{ |
||||
b.Property<string>("Id") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<int>("AccessFailedCount") |
||||
.HasColumnType("int"); |
||||
|
||||
b.Property<string>("ConcurrencyStamp") |
||||
.IsConcurrencyToken() |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("Email") |
||||
.HasMaxLength(256) |
||||
.HasColumnType("nvarchar(256)"); |
||||
|
||||
b.Property<bool>("EmailConfirmed") |
||||
.HasColumnType("bit"); |
||||
|
||||
b.Property<bool>("LockoutEnabled") |
||||
.HasColumnType("bit"); |
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd") |
||||
.HasColumnType("datetimeoffset"); |
||||
|
||||
b.Property<string>("NormalizedEmail") |
||||
.HasMaxLength(256) |
||||
.HasColumnType("nvarchar(256)"); |
||||
|
||||
b.Property<string>("NormalizedUserName") |
||||
.HasMaxLength(256) |
||||
.HasColumnType("nvarchar(256)"); |
||||
|
||||
b.Property<string>("PasswordHash") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("PhoneNumber") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed") |
||||
.HasColumnType("bit"); |
||||
|
||||
b.Property<string>("SecurityStamp") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<bool>("TwoFactorEnabled") |
||||
.HasColumnType("bit"); |
||||
|
||||
b.Property<string>("UserName") |
||||
.HasMaxLength(256) |
||||
.HasColumnType("nvarchar(256)"); |
||||
|
||||
b.HasKey("Id"); |
||||
|
||||
b.HasIndex("NormalizedEmail") |
||||
.HasDatabaseName("EmailIndex"); |
||||
|
||||
b.HasIndex("NormalizedUserName") |
||||
.IsUnique() |
||||
.HasDatabaseName("UserNameIndex") |
||||
.HasFilter("[NormalizedUserName] IS NOT NULL"); |
||||
|
||||
b.ToTable("AspNetUsers", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => |
||||
{ |
||||
b.Property<string>("Id") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("ConcurrencyStamp") |
||||
.IsConcurrencyToken() |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("Name") |
||||
.HasMaxLength(256) |
||||
.HasColumnType("nvarchar(256)"); |
||||
|
||||
b.Property<string>("NormalizedName") |
||||
.HasMaxLength(256) |
||||
.HasColumnType("nvarchar(256)"); |
||||
|
||||
b.HasKey("Id"); |
||||
|
||||
b.HasIndex("NormalizedName") |
||||
.IsUnique() |
||||
.HasDatabaseName("RoleNameIndex") |
||||
.HasFilter("[NormalizedName] IS NOT NULL"); |
||||
|
||||
b.ToTable("AspNetRoles", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => |
||||
{ |
||||
b.Property<int>("Id") |
||||
.ValueGeneratedOnAdd() |
||||
.HasColumnType("int"); |
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); |
||||
|
||||
b.Property<string>("ClaimType") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("ClaimValue") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("RoleId") |
||||
.IsRequired() |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.HasKey("Id"); |
||||
|
||||
b.HasIndex("RoleId"); |
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => |
||||
{ |
||||
b.Property<int>("Id") |
||||
.ValueGeneratedOnAdd() |
||||
.HasColumnType("int"); |
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); |
||||
|
||||
b.Property<string>("ClaimType") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("ClaimValue") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("UserId") |
||||
.IsRequired() |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.HasKey("Id"); |
||||
|
||||
b.HasIndex("UserId"); |
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => |
||||
{ |
||||
b.Property<string>("LoginProvider") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("ProviderKey") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("ProviderDisplayName") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("UserId") |
||||
.IsRequired() |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey"); |
||||
|
||||
b.HasIndex("UserId"); |
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b => |
||||
{ |
||||
b.Property<string>("UserId") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("RoleId") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.HasKey("UserId", "RoleId"); |
||||
|
||||
b.HasIndex("RoleId"); |
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b => |
||||
{ |
||||
b.Property<string>("UserId") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("LoginProvider") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("Name") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("Value") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name"); |
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => |
||||
{ |
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) |
||||
.WithMany() |
||||
.HasForeignKey("RoleId") |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => |
||||
{ |
||||
b.HasOne("AppIdentity.Data.ApplicationUser", null) |
||||
.WithMany() |
||||
.HasForeignKey("UserId") |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => |
||||
{ |
||||
b.HasOne("AppIdentity.Data.ApplicationUser", null) |
||||
.WithMany() |
||||
.HasForeignKey("UserId") |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b => |
||||
{ |
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) |
||||
.WithMany() |
||||
.HasForeignKey("RoleId") |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(); |
||||
|
||||
b.HasOne("AppIdentity.Data.ApplicationUser", null) |
||||
.WithMany() |
||||
.HasForeignKey("UserId") |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b => |
||||
{ |
||||
b.HasOne("AppIdentity.Data.ApplicationUser", null) |
||||
.WithMany() |
||||
.HasForeignKey("UserId") |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(); |
||||
}); |
||||
#pragma warning restore 612, 618 |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,276 @@ |
||||
// <auto-generated /> |
||||
using AppIdentity.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Infrastructure; |
||||
using Microsoft.EntityFrameworkCore.Metadata; |
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; |
||||
using System; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace AppIdentity.Migrations |
||||
{ |
||||
[DbContext(typeof(ApplicationDbContext))] |
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot |
||||
{ |
||||
protected override void BuildModel(ModelBuilder modelBuilder) |
||||
{ |
||||
#pragma warning disable 612, 618 |
||||
modelBuilder |
||||
.HasAnnotation("ProductVersion", "8.0.0") |
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128); |
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); |
||||
|
||||
modelBuilder.Entity("AppIdentity.Data.ApplicationUser", b => |
||||
{ |
||||
b.Property<string>("Id") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<int>("AccessFailedCount") |
||||
.HasColumnType("int"); |
||||
|
||||
b.Property<string>("ConcurrencyStamp") |
||||
.IsConcurrencyToken() |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("Email") |
||||
.HasMaxLength(256) |
||||
.HasColumnType("nvarchar(256)"); |
||||
|
||||
b.Property<bool>("EmailConfirmed") |
||||
.HasColumnType("bit"); |
||||
|
||||
b.Property<bool>("LockoutEnabled") |
||||
.HasColumnType("bit"); |
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd") |
||||
.HasColumnType("datetimeoffset"); |
||||
|
||||
b.Property<string>("NormalizedEmail") |
||||
.HasMaxLength(256) |
||||
.HasColumnType("nvarchar(256)"); |
||||
|
||||
b.Property<string>("NormalizedUserName") |
||||
.HasMaxLength(256) |
||||
.HasColumnType("nvarchar(256)"); |
||||
|
||||
b.Property<string>("PasswordHash") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("PhoneNumber") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed") |
||||
.HasColumnType("bit"); |
||||
|
||||
b.Property<string>("SecurityStamp") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<bool>("TwoFactorEnabled") |
||||
.HasColumnType("bit"); |
||||
|
||||
b.Property<string>("UserName") |
||||
.HasMaxLength(256) |
||||
.HasColumnType("nvarchar(256)"); |
||||
|
||||
b.HasKey("Id"); |
||||
|
||||
b.HasIndex("NormalizedEmail") |
||||
.HasDatabaseName("EmailIndex"); |
||||
|
||||
b.HasIndex("NormalizedUserName") |
||||
.IsUnique() |
||||
.HasDatabaseName("UserNameIndex") |
||||
.HasFilter("[NormalizedUserName] IS NOT NULL"); |
||||
|
||||
b.ToTable("AspNetUsers", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => |
||||
{ |
||||
b.Property<string>("Id") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("ConcurrencyStamp") |
||||
.IsConcurrencyToken() |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("Name") |
||||
.HasMaxLength(256) |
||||
.HasColumnType("nvarchar(256)"); |
||||
|
||||
b.Property<string>("NormalizedName") |
||||
.HasMaxLength(256) |
||||
.HasColumnType("nvarchar(256)"); |
||||
|
||||
b.HasKey("Id"); |
||||
|
||||
b.HasIndex("NormalizedName") |
||||
.IsUnique() |
||||
.HasDatabaseName("RoleNameIndex") |
||||
.HasFilter("[NormalizedName] IS NOT NULL"); |
||||
|
||||
b.ToTable("AspNetRoles", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => |
||||
{ |
||||
b.Property<int>("Id") |
||||
.ValueGeneratedOnAdd() |
||||
.HasColumnType("int"); |
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); |
||||
|
||||
b.Property<string>("ClaimType") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("ClaimValue") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("RoleId") |
||||
.IsRequired() |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.HasKey("Id"); |
||||
|
||||
b.HasIndex("RoleId"); |
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => |
||||
{ |
||||
b.Property<int>("Id") |
||||
.ValueGeneratedOnAdd() |
||||
.HasColumnType("int"); |
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); |
||||
|
||||
b.Property<string>("ClaimType") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("ClaimValue") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("UserId") |
||||
.IsRequired() |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.HasKey("Id"); |
||||
|
||||
b.HasIndex("UserId"); |
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => |
||||
{ |
||||
b.Property<string>("LoginProvider") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("ProviderKey") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("ProviderDisplayName") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.Property<string>("UserId") |
||||
.IsRequired() |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey"); |
||||
|
||||
b.HasIndex("UserId"); |
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b => |
||||
{ |
||||
b.Property<string>("UserId") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("RoleId") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.HasKey("UserId", "RoleId"); |
||||
|
||||
b.HasIndex("RoleId"); |
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b => |
||||
{ |
||||
b.Property<string>("UserId") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("LoginProvider") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("Name") |
||||
.HasColumnType("nvarchar(450)"); |
||||
|
||||
b.Property<string>("Value") |
||||
.HasColumnType("nvarchar(max)"); |
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name"); |
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => |
||||
{ |
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) |
||||
.WithMany() |
||||
.HasForeignKey("RoleId") |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => |
||||
{ |
||||
b.HasOne("AppIdentity.Data.ApplicationUser", null) |
||||
.WithMany() |
||||
.HasForeignKey("UserId") |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => |
||||
{ |
||||
b.HasOne("AppIdentity.Data.ApplicationUser", null) |
||||
.WithMany() |
||||
.HasForeignKey("UserId") |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b => |
||||
{ |
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) |
||||
.WithMany() |
||||
.HasForeignKey("RoleId") |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(); |
||||
|
||||
b.HasOne("AppIdentity.Data.ApplicationUser", null) |
||||
.WithMany() |
||||
.HasForeignKey("UserId") |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(); |
||||
}); |
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b => |
||||
{ |
||||
b.HasOne("AppIdentity.Data.ApplicationUser", null) |
||||
.WithMany() |
||||
.HasForeignKey("UserId") |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(); |
||||
}); |
||||
#pragma warning restore 612, 618 |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,77 @@ |
||||
using AppIdentity.Client.Pages; |
||||
using AppIdentity.Components; |
||||
using AppIdentity.Components.Account; |
||||
using AppIdentity.Data; |
||||
using Microsoft.AspNetCore.Components.Authorization; |
||||
using Microsoft.AspNetCore.Identity; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace AppIdentity |
||||
{ |
||||
public class Program |
||||
{ |
||||
public static void Main(string[] args) |
||||
{ |
||||
var builder = WebApplication.CreateBuilder(args); |
||||
|
||||
// Add services to the container. |
||||
builder.Services.AddRazorComponents() |
||||
.AddInteractiveServerComponents() |
||||
.AddInteractiveWebAssemblyComponents(); |
||||
|
||||
builder.Services.AddCascadingAuthenticationState(); |
||||
builder.Services.AddScoped<IdentityUserAccessor>(); |
||||
builder.Services.AddScoped<IdentityRedirectManager>(); |
||||
builder.Services.AddScoped<AuthenticationStateProvider, PersistingRevalidatingAuthenticationStateProvider>(); |
||||
|
||||
builder.Services.AddAuthentication(options => |
||||
{ |
||||
options.DefaultScheme = IdentityConstants.ApplicationScheme; |
||||
options.DefaultSignInScheme = IdentityConstants.ExternalScheme; |
||||
}) |
||||
.AddIdentityCookies(); |
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); |
||||
builder.Services.AddDbContext<ApplicationDbContext>(options => |
||||
options.UseSqlServer(connectionString)); |
||||
builder.Services.AddDatabaseDeveloperPageExceptionFilter(); |
||||
|
||||
builder.Services.AddIdentityCore<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true) |
||||
.AddEntityFrameworkStores<ApplicationDbContext>() |
||||
.AddSignInManager() |
||||
.AddDefaultTokenProviders(); |
||||
|
||||
builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>(); |
||||
|
||||
var app = builder.Build(); |
||||
|
||||
// Configure the HTTP request pipeline. |
||||
if (app.Environment.IsDevelopment()) |
||||
{ |
||||
app.UseWebAssemblyDebugging(); |
||||
app.UseMigrationsEndPoint(); |
||||
} |
||||
else |
||||
{ |
||||
app.UseExceptionHandler("/Error"); |
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. |
||||
app.UseHsts(); |
||||
} |
||||
|
||||
app.UseHttpsRedirection(); |
||||
|
||||
app.UseStaticFiles(); |
||||
app.UseAntiforgery(); |
||||
|
||||
app.MapRazorComponents<App>() |
||||
.AddInteractiveServerRenderMode() |
||||
.AddInteractiveWebAssemblyRenderMode() |
||||
.AddAdditionalAssemblies(typeof(Client._Imports).Assembly); |
||||
|
||||
// Add additional endpoints required by the Identity /Account Razor components. |
||||
app.MapAdditionalIdentityEndpoints(); |
||||
|
||||
app.Run(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,41 @@ |
||||
{ |
||||
"$schema": "http://json.schemastore.org/launchsettings.json", |
||||
"iisSettings": { |
||||
"windowsAuthentication": false, |
||||
"anonymousAuthentication": true, |
||||
"iisExpress": { |
||||
"applicationUrl": "http://localhost:12565", |
||||
"sslPort": 44387 |
||||
} |
||||
}, |
||||
"profiles": { |
||||
"http": { |
||||
"commandName": "Project", |
||||
"dotnetRunMessages": true, |
||||
"launchBrowser": true, |
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", |
||||
"applicationUrl": "http://localhost:5106", |
||||
"environmentVariables": { |
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
||||
} |
||||
}, |
||||
"https": { |
||||
"commandName": "Project", |
||||
"dotnetRunMessages": true, |
||||
"launchBrowser": true, |
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", |
||||
"applicationUrl": "https://localhost:7197;http://localhost:5106", |
||||
"environmentVariables": { |
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
||||
} |
||||
}, |
||||
"IIS Express": { |
||||
"commandName": "IISExpress", |
||||
"launchBrowser": true, |
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", |
||||
"environmentVariables": { |
||||
"ASPNETCORE_ENVIRONMENT": "Development" |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
{ |
||||
"dependencies": { |
||||
"mssql1": { |
||||
"type": "mssql", |
||||
"connectionId": "ConnectionStrings:DefaultConnection" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
{ |
||||
"dependencies": { |
||||
"mssql1": { |
||||
"type": "mssql.local", |
||||
"connectionId": "ConnectionStrings:DefaultConnection" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
{ |
||||
"Logging": { |
||||
"LogLevel": { |
||||
"Default": "Information", |
||||
"Microsoft.AspNetCore": "Warning" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,12 @@ |
||||
{ |
||||
"ConnectionStrings": { |
||||
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-AppIdentity-8ce3ac08-489c-4e92-9a9f-090045dd22bd;Trusted_Connection=True;MultipleActiveResultSets=true" |
||||
}, |
||||
"Logging": { |
||||
"LogLevel": { |
||||
"Default": "Information", |
||||
"Microsoft.AspNetCore": "Warning" |
||||
} |
||||
}, |
||||
"AllowedHosts": "*" |
||||
} |
@ -0,0 +1,51 @@ |
||||
html, body { |
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; |
||||
} |
||||
|
||||
a, .btn-link { |
||||
color: #006bb7; |
||||
} |
||||
|
||||
.btn-primary { |
||||
color: #fff; |
||||
background-color: #1b6ec2; |
||||
border-color: #1861ac; |
||||
} |
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { |
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; |
||||
} |
||||
|
||||
.content { |
||||
padding-top: 1.1rem; |
||||
} |
||||
|
||||
h1:focus { |
||||
outline: none; |
||||
} |
||||
|
||||
.valid.modified:not([type=checkbox]) { |
||||
outline: 1px solid #26b050; |
||||
} |
||||
|
||||
.invalid { |
||||
outline: 1px solid #e50000; |
||||
} |
||||
|
||||
.validation-message { |
||||
color: #e50000; |
||||
} |
||||
|
||||
.blazor-error-boundary { |
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; |
||||
padding: 1rem 1rem 1rem 3.7rem; |
||||
color: white; |
||||
} |
||||
|
||||
.blazor-error-boundary::after { |
||||
content: "An error has occurred." |
||||
} |
||||
|
||||
.darker-border-checkbox.form-check-input { |
||||
border-color: #929292; |
||||
} |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 1.1 KiB |
Loading…
Reference in new issue