Update manage account page style (#113)

This commit is contained in:
Leendert de Borst
2024-07-22 11:38:58 +02:00
parent 050470453a
commit 022370f799
29 changed files with 610 additions and 995 deletions

View File

@@ -6,12 +6,23 @@ using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
/// <summary>
/// Revalidating identity authentication state provider.
/// </summary>
/// <typeparam name="TUser">The user object.</typeparam>
public class RevalidatingIdentityAuthenticationStateProvider<TUser>
: RevalidatingServerAuthenticationStateProvider where TUser : class
: RevalidatingServerAuthenticationStateProvider
where TUser : class
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IdentityOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="RevalidatingIdentityAuthenticationStateProvider{TUser}"/> class.
/// </summary>
/// <param name="loggerFactory">ILoggerFactory instance.</param>
/// <param name="scopeFactory">IServiceScopeFactory instance.</param>
/// <param name="optionsAccessor">IOptions instance.</param>
public RevalidatingIdentityAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
@@ -22,8 +33,17 @@ public class RevalidatingIdentityAuthenticationStateProvider<TUser>
_options = optionsAccessor.Value;
}
/// <summary>
/// The revalidation interval.
/// </summary>
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
/// <summary>
/// Validate the authentication state.
/// </summary>
/// <param name="authenticationState">AuthenticationState instance.</param>
/// <param name="cancellationToken">CancellationToken.</param>
/// <returns>Boolean indicating whether the currently logged on user is still valid.</returns>
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{

View File

@@ -1,81 +1,3 @@
.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;

View File

@@ -1,8 +0,0 @@
@page "/Account/AccessDenied"
<LayoutPageTitle>Access denied</LayoutPageTitle>
<header>
<h1 class="text-danger">Access denied</h1>
<p class="text-danger">You do not have access to this resource.</p>
</header>

View File

@@ -1,49 +1,8 @@
@page "/user/forgot-password"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@inject UserManager<AdminUser> UserManager
@inject NavigationService NavigationService
<LayoutPageTitle>Forgot your password?</LayoutPageTitle>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Forgot your password?
</h2>
<p>If you have forgotten your password, please consult with the server admin.</p>
@code {
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
private async Task OnValidSubmitAsync()
{
var user = await UserManager.FindByEmailAsync(Input.Email);
if (user is null || !(await UserManager.IsEmailConfirmedAsync(user)))
{
// Don't reveal that the user does not exist or is not confirmed
NavigationService.RedirectTo("Account/ForgotPasswordConfirmation");
}
// For more information on how to enable account confirmation and password reset please
// visit https://go.microsoft.com/fwlink/?LinkID=532713
var code = await UserManager.GeneratePasswordResetTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationService.GetUriWithQueryParameters(
NavigationService.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri,
new Dictionary<string, object?> { ["code"] = code });
// await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
NavigationService.RedirectTo("user/forgot-password-confirm");
}
private sealed class InputModel
{
[Required] [EmailAddress] public string Email { get; set; } = "";
}
}

View File

@@ -1,12 +0,0 @@
@page "/user/forgot-password-confirm"
<LayoutPageTitle>Forgot password confirmation</LayoutPageTitle>
<div class="max-w-md mx-auto mt-8 p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Forgot password confirmation
</h2>
<p class="text-gray-700 dark:text-gray-300">
Please check your email to reset your password.
</p>
</div>

View File

@@ -1,8 +0,0 @@
@page "/Account/InvalidPasswordReset"
<LayoutPageTitle>Invalid password reset</LayoutPageTitle>
<h1>Invalid password reset</h1>
<p>
The password reset link is invalid.
</p>

View File

@@ -1,7 +0,0 @@
@page "/Account/InvalidUser"
<LayoutPageTitle>Invalid user</LayoutPageTitle>
<h3>Invalid user</h3>
<StatusMessage/>

View File

@@ -1,4 +1,4 @@
@page "/Account/Lockout"
@page "/user/lockout"
<LayoutPageTitle>Locked out</LayoutPageTitle>

View File

@@ -77,7 +77,7 @@
else if (result.IsLockedOut)
{
Logger.LogWarning("User account locked out.");
NavigationService.RedirectTo("Account/Lockout");
NavigationService.RedirectTo("user/lockout");
}
else
{

View File

@@ -74,7 +74,7 @@
else if (result.IsLockedOut)
{
Logger.LogWarning("User with ID '{UserId}' account locked out.", userId);
NavigationService.RedirectTo("Account/Lockout");
NavigationService.RedirectTo("user/lockout");
}
else
{

View File

@@ -63,7 +63,7 @@
else if (result.IsLockedOut)
{
Logger.LogWarning("User account locked out.");
NavigationService.RedirectTo("Account/Lockout");
NavigationService.RedirectTo("user/lockout");
}
else
{

View File

@@ -1,3 +1,5 @@
namespace AliasVault.Admin2.Auth.Providers;
using System.Security.Claims;
using AliasServerDb;
using Microsoft.AspNetCore.Components.Authorization;
@@ -5,18 +7,30 @@ using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace AliasVault.Admin2.Auth;
// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user
// every 30 minutes an interactive circuit is connected.
internal sealed class IdentityRevalidatingAuthenticationStateProvider(
/// <summary>
/// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user
/// every 30 minutes an interactive circuit is connected.
/// </summary>
/// <param name="loggerFactory"></param>
/// <param name="scopeFactory"></param>
/// <param name="options"></param>
internal sealed class RevalidatingAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> options)
: RevalidatingServerAuthenticationStateProvider(loggerFactory)
{
/// <summary>
/// The revalidation interval.
/// </summary>
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
/// <summary>
/// Validate the authentication state.
/// </summary>
/// <param name="authenticationState">AuthenticationState instance.</param>
/// <param name="cancellationToken">CancellationToken.</param>
/// <returns>Boolean indicating whether the currently logged on user is still valid.</returns>
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{

View File

@@ -1,83 +0,0 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.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");
}
.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 a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -1,81 +1,3 @@
.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;

View File

@@ -1,83 +0,0 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.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");
}
.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 a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -1,46 +1,44 @@
@page "/account/manage/ChangePassword"
@using System.ComponentModel.DataAnnotations
@using AliasVault.Admin2.Services
@using Microsoft.AspNetCore.Identity
@inject UserManager<AdminUser> UserManager
@inject SignInManager<AdminUser> SignInManager
@inject NavigationService NavigationService
@inject ILogger<ChangePassword> Logger
<PageTitleLayout>Change password</PageTitleLayout>
<LayoutPageTitle>Change password</LayoutPageTitle>
<h3>Change password</h3>
<StatusMessage Message="@message"/>
<div class="row">
<div class="col-md-6">
<EditForm Model="Input" FormName="change-password" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator/>
<ValidationSummary class="text-danger" role="alert"/>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.OldPassword" class="form-control" autocomplete="current-password" aria-required="true" placeholder="Please enter your old password."/>
<label for="old-password" class="form-label">Old password</label>
<ValidationMessage For="() => Input.OldPassword" class="text-danger"/>
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.NewPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please enter your new password."/>
<label for="new-password" class="form-label">New password</label>
<ValidationMessage For="() => Input.NewPassword" class="text-danger"/>
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please confirm your new password."/>
<label for="confirm-password" class="form-label">Confirm password</label>
<ValidationMessage For="() => Input.ConfirmPassword" class="text-danger"/>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Update password</button>
</EditForm>
</div>
<div class="max-w-2xl mx-auto">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Change password</h3>
<StatusMessage Message="@message" class="mb-6"/>
<EditForm Model="Input" FormName="change-password" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-6">
<DataAnnotationsValidator/>
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
<div>
<label for="old-password" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Old password</label>
<InputText type="password" @bind-Value="Input.OldPassword" id="old-password" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm" autocomplete="current-password" aria-required="true" placeholder="Please enter your old password."/>
<ValidationMessage For="() => Input.OldPassword" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
</div>
<div>
<label for="new-password" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">New password</label>
<InputText type="password" @bind-Value="Input.NewPassword" id="new-password" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm" autocomplete="new-password" aria-required="true" placeholder="Please enter your new password."/>
<ValidationMessage For="() => Input.NewPassword" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
</div>
<div>
<label for="confirm-password" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Confirm password</label>
<InputText type="password" @bind-Value="Input.ConfirmPassword" id="confirm-password" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm" autocomplete="new-password" aria-required="true" placeholder="Please confirm your new password."/>
<ValidationMessage For="() => Input.ConfirmPassword" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
</div>
<div>
<button type="submit" class="w-full px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 rounded-md">
Update password
</button>
</div>
</EditForm>
</div>
@code {
private string? message;
private AdminUser user = default!;
private bool hasPassword;
[CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
@@ -58,17 +56,17 @@
private async Task OnValidSubmitAsync()
{
var changePasswordResult = await UserManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword);
var changePasswordResult = await UserManager.ChangePasswordAsync(UserService.User(), Input.OldPassword, Input.NewPassword);
if (!changePasswordResult.Succeeded)
{
message = $"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}";
return;
}
await SignInManager.RefreshSignInAsync(user);
// await SignInManager.RefreshSignInAsync(user);
Logger.LogInformation("User changed their password successfully.");
GlobalNotificationService.AddSuccessMessage("Your password has been changed.");
GlobalNotificationService.AddSuccessMessage("Your password has been changed.", true);
NavigationService.RedirectToCurrentPage();
}

View File

@@ -1,77 +0,0 @@
@page "/account/manage/DeletePersonalData"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@inject UserManager<AdminUser> UserManager
@inject SignInManager<AdminUser> SignInManager
@inject ILogger<DeletePersonalData> Logger
<PageTitleLayout>Delete Personal Data</PageTitleLayout>
<StatusMessage Message="@message"/>
<h3>Delete Personal Data</h3>
<div class="alert alert-warning" role="alert">
<p>
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
</p>
</div>
<div>
<EditForm Model="Input" FormName="delete-user" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator/>
<ValidationSummary class="text-danger" role="alert"/>
@if (requirePassword)
{
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="Please enter your password."/>
<label for="password" class="form-label">Password</label>
<ValidationMessage For="() => Input.Password" class="text-danger"/>
</div>
}
<button class="w-100 btn btn-lg btn-danger" type="submit">Delete data and close my account</button>
</EditForm>
</div>
@code {
private string? message;
private bool requirePassword;
[CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
Input ??= new();
requirePassword = await UserManager.HasPasswordAsync(UserService.User());
}
private async Task OnValidSubmitAsync()
{
if (requirePassword && !await UserManager.CheckPasswordAsync(UserService.User(), Input.Password))
{
message = "Error: Incorrect password.";
return;
}
var result = await UserManager.DeleteAsync(UserService.User());
if (!result.Succeeded)
{
throw new InvalidOperationException("Unexpected error occurred deleting user.");
}
await SignInManager.SignOutAsync();
var userId = await UserManager.GetUserIdAsync(UserService.User());
Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
}
private sealed class InputModel
{
[DataType(DataType.Password)] public string Password { get; set; } = "";
}
}

View File

@@ -5,24 +5,24 @@
@inject UserManager<AdminUser> UserManager
@inject ILogger<Disable2fa> Logger
<PageTitleLayout>Disable two-factor authentication (2FA)</PageTitleLayout>
<LayoutPageTitle>Disable two-factor authentication (2FA)</LayoutPageTitle>
<h3>Disable two-factor authentication (2FA)</h3>
<h3 class="text-xl font-bold mb-4">Disable two-factor authentication (2FA)</h3>
<div class="alert alert-warning" role="alert">
<p>
<strong>This action only disables 2FA.</strong>
<div class="bg-primary-100 border-l-4 border-primary-500 text-primary-700 p-4 mb-4" role="alert">
<p class="font-bold mb-2">
This action only disables 2FA.
</p>
<p>
Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key
used in an authenticator app you should <a href="account/manage/ResetAuthenticator">reset your authenticator keys.</a>
used in an authenticator app you should <a href="account/manage/ResetAuthenticator" class="text-primary-600 hover:text-primary-800 underline">reset your authenticator keys.</a>
</p>
</div>
<div>
<form @formname="disable-2fa" @onsubmit="OnSubmitAsync" method="post">
<AntiforgeryToken/>
<button class="btn btn-danger" type="submit">Disable 2FA</button>
<button class="bg-primary-600 hover:bg-primary-700 text-white font-bold py-2 px-4 rounded" type="submit">Disable 2FA</button>
</form>
</div>

View File

@@ -1,118 +0,0 @@
@page "/account/manage/Email"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@inject UserManager<AdminUser> UserManager
@inject NavigationManager NavigationManager
<PageTitleLayout>Manage email</PageTitleLayout>
<h3>Manage email</h3>
<StatusMessage Message="@message"/>
<div class="row">
<div class="col-md-6">
<form @onsubmit="OnSendEmailVerificationAsync" @formname="send-verification" id="send-verification-form" method="post">
<AntiforgeryToken/>
</form>
<EditForm Model="Input" FormName="change-email" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator/>
<ValidationSummary class="text-danger" role="alert"/>
@if (isEmailConfirmed)
{
<div class="form-floating mb-3 input-group">
<input type="text" value="@email" class="form-control" placeholder="Please enter your email." disabled/>
<div class="input-group-append">
<span class="h-100 input-group-text text-success font-weight-bold">✓</span>
</div>
<label for="email" class="form-label">Email</label>
</div>
}
else
{
<div class="form-floating mb-3">
<input type="text" value="@email" class="form-control" placeholder="Please enter your email." disabled/>
<label for="email" class="form-label">Email</label>
<button type="submit" class="btn btn-link" form="send-verification-form">Send verification email</button>
</div>
}
<div class="form-floating mb-3">
<InputText @bind-Value="Input.NewEmail" class="form-control" autocomplete="email" aria-required="true" placeholder="Please enter new email."/>
<label for="new-email" class="form-label">New email</label>
<ValidationMessage For="() => Input.NewEmail" class="text-danger"/>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Change email</button>
</EditForm>
</div>
</div>
@code {
private string? message;
private string? email;
private bool isEmailConfirmed;
[CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm(FormName = "change-email")]
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync()
{
email = await UserManager.GetEmailAsync(UserService.User());
isEmailConfirmed = await UserManager.IsEmailConfirmedAsync(UserService.User());
Input.NewEmail ??= email;
}
private async Task OnValidSubmitAsync()
{
if (Input.NewEmail is null || Input.NewEmail == email)
{
message = "Your email is unchanged.";
return;
}
var userId = await UserManager.GetUserIdAsync(UserService.User());
var code = await UserManager.GenerateChangeEmailTokenAsync(UserService.User(), Input.NewEmail);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmailChange").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["email"] = Input.NewEmail, ["code"] = code });
// await EmailSender.SendConfirmationLinkAsync(UserService.User(), Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl));
message = "Confirmation link to change email sent. Please check your email.";
}
private async Task OnSendEmailVerificationAsync()
{
if (email is null)
{
return;
}
var userId = await UserManager.GetUserIdAsync(UserService.User());
var code = await UserManager.GenerateEmailConfirmationTokenAsync(UserService.User());
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code });
// await EmailSender.SendConfirmationLinkAsync(UserService.User(), email, HtmlEncoder.Default.Encode(callbackUrl));
message = "Verification email sent. Please check your email.";
}
private sealed class InputModel
{
[Required]
[EmailAddress]
[Display(Name = "New email")]
public string? NewEmail { get; set; }
}
}

View File

@@ -10,7 +10,7 @@
@inject UrlEncoder UrlEncoder
@inject ILogger<EnableAuthenticator> Logger
<PageTitleLayout>Configure authenticator app</PageTitleLayout>
<LayoutPageTitle>Configure authenticator app</LayoutPageTitle>
@if (recoveryCodes is not null)
{
@@ -18,48 +18,53 @@
}
else
{
<StatusMessage Message="@message"/>
<h3>Configure authenticator app</h3>
<div>
<p>To use an authenticator app go through the following steps:</p>
<ol class="list">
<li>
<p>
Download a two-factor authenticator app like Microsoft Authenticator for
<a href="https://go.microsoft.com/fwlink/?Linkid=825072">Android</a> and
<a href="https://go.microsoft.com/fwlink/?Linkid=825073">iOS</a> or
Google Authenticator for
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&amp;hl=en">Android</a> and
<a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">iOS</a>.
</p>
</li>
<li>
<p>Scan the QR Code or enter this key <kbd>@sharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
<div class="alert alert-info">Learn how to <a href="https://go.microsoft.com/fwlink/?Linkid=852423">enable QR code generation</a>.</div>
<div></div>
<div id="authenticator-uri" data-url="@authenticatorUri"></div>
</li>
<li>
<p>
Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
with a unique code. Enter the code in the confirmation box below.
</p>
<div class="row">
<div class="col-md-6">
<EditForm Model="Input" FormName="send-code" OnValidSubmit="OnValidSubmitAsync" method="post">
<div class="max-w-2xl mx-auto">
<StatusMessage Message="@message" class="mb-6"/>
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Configure authenticator app</h3>
<div class="space-y-6">
<p class="text-gray-700 dark:text-gray-300">To use an authenticator app go through the following steps:</p>
<ol class="list-decimal space-y-4">
<li>
<p class="text-gray-700 dark:text-gray-300">
Download a two-factor authenticator app like Microsoft Authenticator for
<a href="https://go.microsoft.com/fwlink/?Linkid=825072" class="text-blue-600 hover:underline dark:text-blue-400">Android</a> and
<a href="https://go.microsoft.com/fwlink/?Linkid=825073" class="text-blue-600 hover:underline dark:text-blue-400">iOS</a> or
Google Authenticator for
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&amp;hl=en" class="text-blue-600 hover:underline dark:text-blue-400">Android</a> and
<a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8" class="text-blue-600 hover:underline dark:text-blue-400">iOS</a>.
</p>
</li>
<li>
<p class="text-gray-700 dark:text-gray-300">Scan the QR Code or enter this key <kbd class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500">@sharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
<div class="mt-2 p-4 bg-blue-100 text-blue-700 rounded-md dark:bg-blue-900 dark:text-blue-300">
Learn how to <a href="https://go.microsoft.com/fwlink/?Linkid=852423" class="text-blue-800 hover:underline dark:text-blue-200">enable QR code generation</a>.
</div>
<div id="authenticator-uri" data-url="@authenticatorUri" class="mt-4"></div>
</li>
<li>
<p class="text-gray-700 dark:text-gray-300">
Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
with a unique code. Enter the code in the confirmation box below.
</p>
<div class="mt-4">
<EditForm Model="Input" FormName="send-code" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-4">
<DataAnnotationsValidator/>
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Code" class="form-control" autocomplete="off" placeholder="Please enter the code."/>
<label for="code" class="control-label form-label">Verification Code</label>
<ValidationMessage For="() => Input.Code" class="text-danger"/>
<div>
<label for="code" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Verification Code</label>
<InputText @bind-Value="Input.Code" id="code" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" autocomplete="off" placeholder="Please enter the code."/>
<ValidationMessage For="() => Input.Code" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Verify</button>
<ValidationSummary class="text-danger" role="alert"/>
<div>
<button type="submit" class="w-full px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 rounded-md dark:bg-primary-500 dark:hover:bg-primary-600">
Verify
</button>
</div>
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
</EditForm>
</div>
</div>
</li>
</ol>
</li>
</ol>
</div>
</div>
}

View File

@@ -5,7 +5,7 @@
@inject UserManager<AdminUser> UserManager
@inject ILogger<GenerateRecoveryCodes> Logger
<PageTitleLayout>Generate two-factor authentication (2FA) recovery codes</PageTitleLayout>
<LayoutPageTitle>Generate two-factor authentication (2FA) recovery codes</LayoutPageTitle>
@if (recoveryCodes is not null)
{
@@ -13,22 +13,24 @@
}
else
{
<h3>Generate two-factor authentication (2FA) recovery codes</h3>
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<h3 class="text-xl font-bold mb-4">Generate two-factor authentication (2FA) recovery codes</h3>
<div class="bg-primary-100 border-l-4 border-primary-500 text-primary-700 p-4 mb-4" role="alert">
<p class="mb-2">
<svg class="inline w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<strong>Put these codes in a safe place.</strong>
</p>
<p>
<p class="mb-2">
If you lose your device and don't have the recovery codes you will lose access to your account.
</p>
<p>
Generating new recovery codes does not change the keys used in authenticator apps. If you wish to change the key
used in an authenticator app you should <a href="account/manage/ResetAuthenticator">reset your authenticator keys.</a>
used in an authenticator app you should <a href="account/manage/ResetAuthenticator" class="text-primary-600 hover:text-primary-800 underline">reset your authenticator keys.</a>
</p>
</div>
<div>
<button class="btn btn-danger" @onclick="GenerateCodes" type="submit">Generate Recovery Codes</button>
<button class="bg-primary-600 hover:bg-primary-700 text-white font-bold py-2 px-4 rounded" @onclick="GenerateCodes" type="submit">Generate Recovery Codes</button>
</div>
}

View File

@@ -4,27 +4,29 @@
@inject UserManager<AdminUser> UserManager
<PageTitleLayout>Profile</PageTitleLayout>
<LayoutPageTitle>Profile</LayoutPageTitle>
<h3>Profile</h3>
<div class="max-w-2xl mx-auto">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Profile</h3>
<div class="flex flex-wrap -mx-3">
<div class="w-full md:w-1/2 px-3">
<EditForm Model="Input" FormName="profile" OnValidSubmit="OnValidSubmitAsync">
<DataAnnotationsValidator/>
<ValidationSummary class="text-red-500 mb-4" role="alert"/>
<div class="mb-4">
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Username</label>
<input type="text" value="@username" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-gray-100 cursor-not-allowed" placeholder="Please choose your username." disabled/>
</div>
<div class="mb-4">
<label for="phone-number" class="block text-sm font-medium text-gray-700 mb-1">Phone number</label>
<InputText @bind-Value="Input.PhoneNumber" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" placeholder="Please enter your phone number."/>
<ValidationMessage For="() => Input.PhoneNumber" class="text-red-500 text-sm mt-1"/>
</div>
<button type="submit" class="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md transition duration-150 ease-in-out">Save</button>
</EditForm>
</div>
<EditForm Model="Input" FormName="profile" OnValidSubmit="OnValidSubmitAsync" class="space-y-6">
<DataAnnotationsValidator/>
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
<div>
<label for="username" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Username</label>
<input type="text" value="@username" id="username" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-gray-100 cursor-not-allowed dark:bg-gray-700 dark:border-gray-600 dark:text-gray-400" placeholder="Please choose your username." disabled/>
</div>
<div>
<label for="phone-number" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Phone number</label>
<InputText @bind-Value="Input.PhoneNumber" id="phone-number" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" placeholder="Please enter your phone number."/>
<ValidationMessage For="() => Input.PhoneNumber" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
</div>
<div>
<button type="submit" class="w-full px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 rounded-md dark:bg-primary-500 dark:hover:bg-primary-600">
Save
</button>
</div>
</EditForm>
</div>
@code {

View File

@@ -1,22 +0,0 @@
@page "/account/manage/PersonalData"
<PageTitleLayout>Personal Data</PageTitleLayout>
<StatusMessage/>
<h3>Personal Data</h3>
<div class="row">
<div class="col-md-6">
<p>Your account contains personal data that you have given us. This page allows you to download or delete that data.</p>
<p>
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
</p>
<form action="account/manage/DownloadPersonalData" method="post">
<AntiforgeryToken/>
<button class="btn btn-primary" type="submit">Download</button>
</form>
<p>
<a href="account/manage/DeletePersonalData" class="btn btn-danger">Delete</a>
</p>
</div>
</div>

View File

@@ -6,13 +6,14 @@
@inject SignInManager<AdminUser> SignInManager
@inject ILogger<ResetAuthenticator> Logger
<PageTitleLayout>Reset authenticator key</PageTitleLayout>
<LayoutPageTitle>Reset authenticator key</LayoutPageTitle>
<StatusMessage/>
<h3>Reset authenticator key</h3>
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<h3 class="text-xl font-bold mb-4">Reset authenticator key</h3>
<div class="bg-primary-100 border-l-4 border-primary-500 text-primary-700 p-4 mb-4" role="alert">
<p class="mb-2">
<svg class="inline w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<strong>If you reset your authenticator key your authenticator app will not work until you reconfigure it.</strong>
</p>
<p>
@@ -23,7 +24,7 @@
<div>
<form @formname="reset-authenticator" @onsubmit="OnSubmitAsync" method="post">
<AntiforgeryToken/>
<button class="btn btn-danger" type="submit">Reset authenticator key</button>
<button class="bg-primary-600 hover:bg-primary-700 text-white font-bold py-2 px-4 rounded" type="submit">Reset authenticator key</button>
</form>
</div>

View File

@@ -6,7 +6,7 @@
@inject UserManager<AdminUser> UserManager
@inject SignInManager<AdminUser> SignInManager
<PageTitleLayout>Set password</PageTitleLayout>
<LayoutPageTitle>Set password</LayoutPageTitle>
<h3>Set your password</h3>
<StatusMessage Message="@message"/>

View File

@@ -5,48 +5,62 @@
@inject UserManager<AdminUser> UserManager
@inject SignInManager<AdminUser> SignInManager
<PageTitleLayout>Two-factor authentication (2FA)</PageTitleLayout>
<h3>Two-factor authentication (2FA)</h3>
<LayoutPageTitle>Two-factor authentication (2FA)</LayoutPageTitle>
@if (is2faEnabled)
{
if (recoveryCodesLeft == 0)
{
<div class="alert alert-danger">
<strong>You have no recovery codes left.</strong>
<p>You must <a href="account/manage/GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
</div>
}
else if (recoveryCodesLeft == 1)
{
<div class="alert alert-danger">
<strong>You have 1 recovery code left.</strong>
<p>You can <a href="account/manage/GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
</div>
}
else if (recoveryCodesLeft <= 3)
{
<div class="alert alert-warning">
<strong>You have @recoveryCodesLeft recovery codes left.</strong>
<p>You should <a href="account/manage/GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
</div>
}
<div class="mx-auto mt-8 p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Two-factor authentication (2FA)</h3>
<a href="account/manage/Disable2fa" class="btn btn-primary">Disable 2FA</a>
<a href="account/manage/GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
@if (recoveryCodesLeft == 0)
{
<div class="mb-4 p-4 bg-red-100 border-l-4 border-red-500 text-red-700 dark:bg-red-900 dark:text-red-100">
<p class="font-bold">You have no recovery codes left.</p>
<p>You must <a href="account/manage/GenerateRecoveryCodes" class="text-red-800 dark:text-red-200 underline">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
</div>
}
else if (recoveryCodesLeft == 1)
{
<div class="mb-4 p-4 bg-red-100 border-l-4 border-red-500 text-red-700 dark:bg-red-900 dark:text-red-100">
<p class="font-bold">You have 1 recovery code left.</p>
<p>You can <a href="account/manage/GenerateRecoveryCodes" class="text-red-800 dark:text-red-200 underline">generate a new set of recovery codes</a>.</p>
</div>
}
else if (recoveryCodesLeft <= 3)
{
<div class="mb-4 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-100">
<p class="font-bold">You have @recoveryCodesLeft recovery codes left.</p>
<p>You should <a href="account/manage/GenerateRecoveryCodes" class="text-yellow-800 dark:text-yellow-200 underline">generate a new set of recovery codes</a>.</p>
</div>
}
<div class="flex space-x-4">
<a href="account/manage/Disable2fa" class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg text-sm focus:ring-4 focus:outline-none focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Disable 2FA</a>
<a href="account/manage/GenerateRecoveryCodes" class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg text-sm focus:ring-4 focus:outline-none focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Reset recovery codes</a>
</div>
</div>
}
<h4>Authenticator app</h4>
@if (!hasAuthenticator)
{
<a href="account/manage/EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
}
else
{
<a href="account/manage/EnableAuthenticator" class="btn btn-primary">Set up authenticator app</a>
<a href="account/manage/ResetAuthenticator" class="btn btn-primary">Reset authenticator app</a>
}
<div class="mt-6 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Authenticator app</h4>
<div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
@if (!hasAuthenticator)
{
<a href="account/manage/EnableAuthenticator" class="inline-block px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg text-sm text-center focus:ring-4 focus:outline-none focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
Add authenticator app
</a>
}
else
{
<a href="account/manage/EnableAuthenticator" class="inline-block px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg text-sm text-center focus:ring-4 focus:outline-none focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
Set up authenticator app
</a>
<a href="account/manage/ResetAuthenticator" class="inline-block px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg text-sm text-center focus:ring-4 focus:outline-none focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
Reset authenticator app
</a>
}
</div>
</div>
@code {
private bool hasAuthenticator;

View File

@@ -6,24 +6,12 @@
<li>
<NavLink href="account/manage" Match="NavLinkMatch.All" class="block px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150">Profile</NavLink>
</li>
<li>
<NavLink href="account/manage/Email" class="block px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150">Email</NavLink>
</li>
<li>
<NavLink href="account/manage/ChangePassword" class="block px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150">Password</NavLink>
</li>
@if (hasExternalLogins)
{
<li>
<NavLink href="account/manage/ExternalLogins" class="block px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150">External logins</NavLink>
</li>
}
<li>
<NavLink href="account/manage/TwoFactorAuthentication" class="block px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150">Two-factor authentication</NavLink>
</li>
<li>
<NavLink href="account/manage/PersonalData" class="block px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150">Personal data</NavLink>
</li>
</ul>
@code {

View File

@@ -3,10 +3,9 @@ using AliasServerDb;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using AliasVault.Admin2.Auth;
using AliasVault.Admin2.Auth.Providers;
using AliasVault.Admin2.Main;
using AliasVault.Admin2.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.Data.Sqlite;
var builder = WebApplication.CreateBuilder(args);
@@ -20,7 +19,7 @@ builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<JsInvokeService>();
builder.Services.AddScoped<GlobalNotificationService>();
builder.Services.AddScoped<NavigationService>();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider, RevalidatingAuthenticationStateProvider>();
builder.Services.AddAuthentication(options =>
{

View File

@@ -644,6 +644,11 @@ video {
grid-column: 1 / -1;
}
.-mx-3 {
margin-left: -0.75rem;
margin-right: -0.75rem;
}
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
@@ -659,9 +664,12 @@ video {
margin-bottom: 1rem;
}
.-mx-3 {
margin-left: -0.75rem;
margin-right: -0.75rem;
.mb-1 {
margin-bottom: 0.25rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-3 {
@@ -676,10 +684,30 @@ video {
margin-bottom: 1.25rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.ml-1 {
margin-left: 0.25rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.ml-3 {
margin-left: 0.75rem;
}
.ml-auto {
margin-left: auto;
}
.mr-14 {
margin-right: 3.5rem;
}
@@ -704,55 +732,24 @@ video {
margin-top: 0px;
}
.mt-4 {
margin-top: 1rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.mt-1 {
margin-top: 0.25rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.ml-3 {
margin-left: 0.75rem;
}
.ml-auto {
margin-left: auto;
}
.mt-8 {
margin-top: 2rem;
.mt-4 {
margin-top: 1rem;
}
.mt-6 {
margin-top: 1.5rem;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
.mt-8 {
margin-top: 2rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.line-clamp-1 {
@@ -770,6 +767,10 @@ video {
display: inline-block;
}
.inline {
display: inline;
}
.flex {
display: flex;
}
@@ -790,6 +791,10 @@ video {
display: none;
}
.h-4 {
height: 1rem;
}
.h-5 {
height: 1.25rem;
}
@@ -810,14 +815,14 @@ video {
height: 100%;
}
.h-4 {
height: 1rem;
}
.w-1\/2 {
width: 50%;
}
.w-4 {
width: 1rem;
}
.w-5 {
width: 1.25rem;
}
@@ -842,8 +847,12 @@ video {
width: 100%;
}
.w-4 {
width: 1rem;
.max-w-2xl {
max-width: 42rem;
}
.max-w-md {
max-width: 28rem;
}
.max-w-screen-2xl {
@@ -854,10 +863,6 @@ video {
max-width: 36rem;
}
.max-w-md {
max-width: 28rem;
}
.flex-shrink-0 {
flex-shrink: 0;
}
@@ -870,10 +875,18 @@ video {
cursor: not-allowed;
}
.list-inside {
list-style-position: inside;
}
.list-none {
list-style-type: none;
}
.list-decimal {
list-style-type: decimal;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@@ -918,16 +931,18 @@ video {
justify-content: space-between;
}
.gap-4 {
gap: 1rem;
}
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.25rem * var(--tw-space-x-reverse));
margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-6 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1.5rem * var(--tw-space-x-reverse));
@@ -940,6 +955,12 @@ video {
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
}
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
}
.space-y-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
@@ -1009,18 +1030,22 @@ video {
border-bottom-right-radius: 0.5rem;
}
.border-2 {
border-width: 2px;
}
.border {
border-width: 1px;
}
.border-2 {
border-width: 2px;
}
.border-b {
border-bottom-width: 1px;
}
.border-l-4 {
border-left-width: 4px;
}
.border-t {
border-top-width: 1px;
}
@@ -1035,9 +1060,29 @@ video {
border-color: rgb(209 213 219 / var(--tw-border-opacity));
}
.bg-amber-200 {
.border-red-500 {
--tw-border-opacity: 1;
border-color: rgb(239 68 68 / var(--tw-border-opacity));
}
.border-yellow-500 {
--tw-border-opacity: 1;
border-color: rgb(234 179 8 / var(--tw-border-opacity));
}
.border-primary-500 {
--tw-border-opacity: 1;
border-color: rgb(244 149 65 / var(--tw-border-opacity));
}
.bg-blue-500 {
--tw-bg-opacity: 1;
background-color: rgb(253 230 138 / var(--tw-bg-opacity));
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
.bg-gray-200 {
@@ -1065,19 +1110,9 @@ video {
background-color: rgb(240 253 244 / var(--tw-bg-opacity));
}
.bg-red-50 {
.bg-indigo-600 {
--tw-bg-opacity: 1;
background-color: rgb(254 242 242 / var(--tw-bg-opacity));
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-primary-200 {
--tw-bg-opacity: 1;
background-color: rgb(251 203 116 / var(--tw-bg-opacity));
background-color: rgb(79 70 229 / var(--tw-bg-opacity));
}
.bg-primary-100 {
@@ -1085,19 +1120,9 @@ video {
background-color: rgb(253 222 133 / var(--tw-bg-opacity));
}
.bg-blue-500 {
.bg-primary-600 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
.bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
}
.bg-primary-700 {
@@ -1105,9 +1130,34 @@ video {
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
}
.bg-primary-600 {
.bg-red-100 {
--tw-bg-opacity: 1;
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
}
.bg-red-50 {
--tw-bg-opacity: 1;
background-color: rgb(254 242 242 / var(--tw-bg-opacity));
}
.bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-yellow-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 249 195 / var(--tw-bg-opacity));
}
.bg-blue-100 {
--tw-bg-opacity: 1;
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
}
.bg-opacity-50 {
@@ -1130,6 +1180,11 @@ video {
padding: 1.5rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
@@ -1140,6 +1195,16 @@ video {
padding-right: 1rem;
}
.px-5 {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
@@ -1150,6 +1215,11 @@ video {
padding-bottom: 0.5rem;
}
.py-2\.5 {
padding-top: 0.625rem;
padding-bottom: 0.625rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
@@ -1160,34 +1230,14 @@ video {
padding-bottom: 1.5rem;
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px-5 {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.py-2\.5 {
padding-top: 0.625rem;
padding-bottom: 0.625rem;
.py-1\.5 {
padding-top: 0.375rem;
padding-bottom: 0.375rem;
}
.ps-2 {
@@ -1224,6 +1274,11 @@ video {
line-height: 1.5rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
@@ -1234,20 +1289,11 @@ video {
line-height: 1.75rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.font-black {
font-weight: 900;
}
.font-bold {
font-weight: 700;
}
@@ -1305,11 +1351,31 @@ video {
color: rgb(22 101 52 / var(--tw-text-opacity));
}
.text-primary-600 {
--tw-text-opacity: 1;
color: rgb(214 131 56 / var(--tw-text-opacity));
}
.text-primary-700 {
--tw-text-opacity: 1;
color: rgb(184 112 47 / var(--tw-text-opacity));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.text-red-600 {
--tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity));
}
.text-red-700 {
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity));
}
.text-red-800 {
--tw-text-opacity: 1;
color: rgb(153 27 27 / var(--tw-text-opacity));
@@ -1320,9 +1386,29 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.text-gray-600 {
.text-yellow-700 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
color: rgb(161 98 7 / var(--tw-text-opacity));
}
.text-yellow-800 {
--tw-text-opacity: 1;
color: rgb(133 77 14 / var(--tw-text-opacity));
}
.text-blue-600 {
--tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity));
}
.text-blue-700 {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
.text-blue-800 {
--tw-text-opacity: 1;
color: rgb(30 64 175 / var(--tw-text-opacity));
}
.text-gray-800 {
@@ -1330,19 +1416,8 @@ video {
color: rgb(31 41 55 / var(--tw-text-opacity));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.text-primary-600 {
--tw-text-opacity: 1;
color: rgb(214 131 56 / var(--tw-text-opacity));
}
.text-red-600 {
--tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity));
.underline {
text-decoration-line: underline;
}
.opacity-0 {
@@ -1361,22 +1436,16 @@ video {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-sm {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-md {
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.transition-opacity {
transition-property: opacity;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
.shadow-sm {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.transition {
@@ -1393,22 +1462,27 @@ video {
transition-duration: 150ms;
}
.duration-300 {
transition-duration: 300ms;
.transition-opacity {
transition-property: opacity;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration-150 {
transition-duration: 150ms;
}
.duration-300 {
transition-duration: 300ms;
}
.ease-in-out {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.hover\:scale-105:hover {
--tw-scale-x: 1.05;
--tw-scale-y: 1.05;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
.hover\:bg-blue-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
}
.hover\:bg-gray-100:hover {
@@ -1421,14 +1495,9 @@ video {
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.hover\:bg-blue-600:hover {
.hover\:bg-indigo-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
}
.hover\:bg-primary-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(154 93 38 / var(--tw-bg-opacity));
background-color: rgb(67 56 202 / var(--tw-bg-opacity));
}
.hover\:bg-primary-700:hover {
@@ -1436,6 +1505,11 @@ video {
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
}
.hover\:bg-primary-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(154 93 38 / var(--tw-bg-opacity));
}
.hover\:text-gray-900:hover {
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity));
@@ -1451,14 +1525,13 @@ video {
color: rgb(184 112 47 / var(--tw-text-opacity));
}
.hover\:underline:hover {
text-decoration-line: underline;
.hover\:text-primary-800:hover {
--tw-text-opacity: 1;
color: rgb(154 93 38 / var(--tw-text-opacity));
}
.hover\:shadow-lg:hover {
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
.hover\:underline:hover {
text-decoration-line: underline;
}
.focus\:border-blue-500:focus {
@@ -1466,6 +1539,11 @@ video {
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}
.focus\:border-indigo-500:focus {
--tw-border-opacity: 1;
border-color: rgb(99 102 241 / var(--tw-border-opacity));
}
.focus\:border-primary-500:focus {
--tw-border-opacity: 1;
border-color: rgb(244 149 65 / var(--tw-border-opacity));
@@ -1481,16 +1559,27 @@ video {
outline-offset: 2px;
}
.focus\:ring-1:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-2:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-4:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-1:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
.focus\:ring-blue-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
}
.focus\:ring-gray-200:focus {
@@ -1503,14 +1592,9 @@ video {
--tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity));
}
.focus\:ring-blue-500:focus {
.focus\:ring-indigo-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
}
.focus\:ring-primary-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(244 149 65 / var(--tw-ring-opacity));
--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity));
}
.focus\:ring-primary-300:focus {
@@ -1518,24 +1602,38 @@ video {
--tw-ring-color: rgb(248 185 99 / var(--tw-ring-opacity));
}
.focus\:ring-primary-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(244 149 65 / var(--tw-ring-opacity));
}
.focus\:ring-primary-600:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(214 131 56 / var(--tw-ring-opacity));
}
.focus\:ring-offset-2:focus {
--tw-ring-offset-width: 2px;
}
.dark\:divide-gray-600:is(.dark *) > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-divide-opacity));
}
.dark\:border-gray-600:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity));
}
.dark\:border-gray-700:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(55 65 81 / var(--tw-border-opacity));
}
.dark\:border-gray-600:is(.dark *) {
.dark\:border-gray-500:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity));
border-color: rgb(107 114 128 / var(--tw-border-opacity));
}
.dark\:bg-gray-700:is(.dark *) {
@@ -1558,10 +1656,45 @@ video {
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
}
.dark\:bg-red-900:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
}
.dark\:bg-yellow-900:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
}
.dark\:bg-indigo-500:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(99 102 241 / var(--tw-bg-opacity));
}
.dark\:bg-blue-900:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(30 58 138 / var(--tw-bg-opacity));
}
.dark\:bg-gray-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
}
.dark\:bg-primary-500:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(244 149 65 / var(--tw-bg-opacity));
}
.dark\:bg-opacity-80:is(.dark *) {
--tw-bg-opacity: 0.8;
}
.dark\:text-gray-200:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity));
}
.dark\:text-gray-300:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity));
@@ -1587,6 +1720,16 @@ video {
color: rgb(244 149 65 / var(--tw-text-opacity));
}
.dark\:text-red-100:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(254 226 226 / var(--tw-text-opacity));
}
.dark\:text-red-200:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(254 202 202 / var(--tw-text-opacity));
}
.dark\:text-red-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity));
@@ -1597,6 +1740,36 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:text-yellow-100:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(254 249 195 / var(--tw-text-opacity));
}
.dark\:text-yellow-200:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(254 240 138 / var(--tw-text-opacity));
}
.dark\:text-blue-200:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(191 219 254 / var(--tw-text-opacity));
}
.dark\:text-blue-300:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(147 197 253 / var(--tw-text-opacity));
}
.dark\:text-blue-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(96 165 250 / var(--tw-text-opacity));
}
.dark\:text-gray-100:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(243 244 246 / var(--tw-text-opacity));
}
.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder {
--tw-placeholder-opacity: 1;
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
@@ -1626,6 +1799,16 @@ video {
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-indigo-600:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(79 70 229 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-primary-600:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
}
.dark\:hover\:text-primary-500:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(244 149 65 / var(--tw-text-opacity));
@@ -1636,14 +1819,19 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:focus\:border-blue-500:focus:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}
.dark\:focus\:border-primary-500:focus:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(244 149 65 / var(--tw-border-opacity));
}
.dark\:focus\:border-blue-500:focus:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity));
.dark\:focus\:ring-blue-500:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-gray-600:focus:is(.dark *) {
@@ -1671,11 +1859,6 @@ video {
--tw-ring-color: rgb(154 93 38 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-blue-500:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
}
@media (min-width: 640px) {
.sm\:flex {
display: flex;
@@ -1685,6 +1868,22 @@ video {
width: auto;
}
.sm\:flex-row {
flex-direction: row;
}
.sm\:space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0px * var(--tw-space-y-reverse));
}
.sm\:rounded-lg {
border-radius: 0.5rem;
}
@@ -1729,6 +1928,10 @@ video {
height: 100vh;
}
.md\:w-1\/2 {
width: 50%;
}
.md\:w-1\/4 {
width: 25%;
}
@@ -1737,14 +1940,6 @@ video {
width: 75%;
}
.md\:w-1\/2 {
width: 50%;
}
.md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.md\:flex-row {
flex-direction: row;
}
@@ -1782,14 +1977,14 @@ video {
order: 2;
}
.lg\:mt-0 {
margin-top: 0px;
}
.lg\:mb-10 {
margin-bottom: 2.5rem;
}
.lg\:mt-0 {
margin-top: 0px;
}
.lg\:flex {
display: flex;
}
@@ -1802,10 +1997,6 @@ video {
width: auto;
}
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.lg\:flex-row {
flex-direction: row;
}
@@ -1834,10 +2025,6 @@ video {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.xl\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.xl\:gap-4 {
gap: 1rem;
}