mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-07 14:56:02 -04:00
Style login/2FA pages with tailwind CSS (#113)
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
@page "/Account/ForgotPassword"
|
||||
|
||||
@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 IEmailSender<AdminUser> EmailSender
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
|
||||
<PageTitleLayout>Forgot your password?</PageTitleLayout>
|
||||
|
||||
<h1>Forgot your password?</h1>
|
||||
<h2>Enter your email.</h2>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<EditForm Model="Input" FormName="forgot-password" OnValidSubmit="OnValidSubmitAsync" method="post">
|
||||
<DataAnnotationsValidator/>
|
||||
<ValidationSummary class="text-danger" role="alert"/>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<InputText @bind-Value="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com"/>
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<ValidationMessage For="() => Input.Email" class="text-danger"/>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset password</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@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
|
||||
RedirectManager.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 = NavigationManager.GetUriWithQueryParameters(
|
||||
NavigationManager.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri,
|
||||
new Dictionary<string, object?> { ["code"] = code });
|
||||
|
||||
await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
|
||||
|
||||
RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation");
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Required] [EmailAddress] public string Email { get; set; } = "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
@page "/Account/ForgotPasswordConfirmation"
|
||||
|
||||
<PageTitleLayout>Forgot password confirmation</PageTitleLayout>
|
||||
|
||||
<h1>Forgot password confirmation</h1>
|
||||
<p>
|
||||
Please check your email to reset your password.
|
||||
</p>
|
||||
@@ -1,114 +0,0 @@
|
||||
@page "/user/login"
|
||||
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Authentication
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject SignInManager<AdminUser> SignInManager
|
||||
@inject ILogger<Login> Logger
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
|
||||
<PageTitleLayout>Log in</PageTitleLayout>
|
||||
|
||||
<h1>Log in</h1>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<section>
|
||||
<StatusMessage Message="@errorMessage"/>
|
||||
<EditForm Model="Input" method="post" OnValidSubmit="LoginUser" FormName="login">
|
||||
<DataAnnotationsValidator/>
|
||||
<hr/>
|
||||
<ValidationSummary class="text-danger" role="alert"/>
|
||||
<div class="form-floating mb-3">
|
||||
<InputText @bind-Value="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com"/>
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<ValidationMessage For="() => Input.Email" class="text-danger"/>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="password"/>
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<ValidationMessage For="() => Input.Password" class="text-danger"/>
|
||||
</div>
|
||||
<div class="checkbox mb-3">
|
||||
<label class="form-label">
|
||||
<InputCheckbox @bind-Value="Input.RememberMe" class="darker-border-checkbox form-check-input"/>
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<a href="Account/ForgotPassword">Forgot your password?</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="@(NavigationManager.GetUriWithQueryParameters("user/register", new Dictionary<string, object?> { ["ReturnUrl"] = ReturnUrl }))">Register as a new user</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="Account/ResendEmailConfirmation">Resend email confirmation</a>
|
||||
</p>
|
||||
</div>
|
||||
</EditForm>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string? errorMessage;
|
||||
|
||||
[CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
|
||||
|
||||
[SupplyParameterFromQuery] private string? ReturnUrl { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (HttpMethods.IsGet(HttpContext.Request.Method))
|
||||
{
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoginUser()
|
||||
{
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
Logger.LogInformation("User logged in.");
|
||||
RedirectManager.RedirectTo(ReturnUrl);
|
||||
}
|
||||
else if (result.RequiresTwoFactor)
|
||||
{
|
||||
RedirectManager.RedirectTo(
|
||||
"user/loginWith2fa",
|
||||
new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
Logger.LogWarning("User account locked out.");
|
||||
RedirectManager.RedirectTo("Account/Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Error: Invalid login attempt.";
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Required] [EmailAddress] public string Email { get; set; } = "";
|
||||
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; } = "";
|
||||
|
||||
[Display(Name = "Remember me?")] public bool RememberMe { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
@page "/user/loginWith2fa"
|
||||
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject SignInManager<AdminUser> SignInManager
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
@inject ILogger<LoginWith2fa> Logger
|
||||
|
||||
<PageTitleLayout>Two-factor authentication</PageTitleLayout>
|
||||
|
||||
<h1>Two-factor authentication</h1>
|
||||
<hr/>
|
||||
<StatusMessage Message="@message"/>
|
||||
<p>Your login is protected with an authenticator app. Enter your authenticator code below.</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<EditForm Model="Input" FormName="login-with-2fa" OnValidSubmit="OnValidSubmitAsync" method="post">
|
||||
<input type="hidden" name="ReturnUrl" value="@ReturnUrl"/>
|
||||
<input type="hidden" name="RememberMe" value="@RememberMe"/>
|
||||
<DataAnnotationsValidator/>
|
||||
<ValidationSummary class="text-danger" role="alert"/>
|
||||
<div class="form-floating mb-3">
|
||||
<InputText @bind-Value="Input.TwoFactorCode" class="form-control" autocomplete="off"/>
|
||||
<label for="two-factor-code" class="form-label">Authenticator code</label>
|
||||
<ValidationMessage For="() => Input.TwoFactorCode" class="text-danger"/>
|
||||
</div>
|
||||
<div class="checkbox mb-3">
|
||||
<label for="remember-machine" class="form-label">
|
||||
<InputCheckbox @bind-Value="Input.RememberMachine"/>
|
||||
Remember this machine
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Don't have access to your authenticator device? You can
|
||||
<a href="user/loginWithRecoveryCode?ReturnUrl=@ReturnUrl">log in with a recovery code</a>.
|
||||
</p>
|
||||
|
||||
@code {
|
||||
private string? message;
|
||||
private AdminUser user = default!;
|
||||
|
||||
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
|
||||
|
||||
[SupplyParameterFromQuery] private string? ReturnUrl { get; set; }
|
||||
|
||||
[SupplyParameterFromQuery] private bool RememberMe { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ??
|
||||
throw new InvalidOperationException("Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
private async Task OnValidSubmitAsync()
|
||||
{
|
||||
var authenticatorCode = Input.TwoFactorCode!.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
var result = await SignInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, RememberMe, Input.RememberMachine);
|
||||
var userId = await UserManager.GetUserIdAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", userId);
|
||||
RedirectManager.RedirectTo(ReturnUrl);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
Logger.LogWarning("User with ID '{UserId}' account locked out.", userId);
|
||||
RedirectManager.RedirectTo("Account/Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId);
|
||||
message = "Error: Invalid authenticator code.";
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Authenticator code")]
|
||||
public string? TwoFactorCode { get; set; }
|
||||
|
||||
[Display(Name = "Remember this machine")]
|
||||
public bool RememberMachine { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
@page "/user/logout"
|
||||
@using AliasVault.Admin2.Services
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@inject SignInManager<IdentityUser> SignInManager
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
|
||||
@code {
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Sign out the user
|
||||
await SignInManager.SignOutAsync();
|
||||
GlobalNotificationService.ClearMessages();
|
||||
|
||||
// Redirect to the home page with hard refresh.
|
||||
NavigationManager.NavigateTo("/", true);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
@using AliasVault.Admin2.Account.Shared
|
||||
@using AliasVault.Admin2.Main.Layout
|
||||
@layout AuthLayout
|
||||
@@ -1,51 +0,0 @@
|
||||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="page">
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="user/register">
|
||||
<span class="bi bi-person-nav-menu" aria-hidden="true"></span> Register
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="user/login">
|
||||
<span class="bi bi-person-badge-nav-menu" aria-hidden="true"></span> Login
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string? currentUrl;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
|
||||
NavigationManager.LocationChanged += OnLocationChanged;
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
NavigationManager.LocationChanged -= OnLocationChanged;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<StatusMessage Message="@StatusMessage"/>
|
||||
<h3>Recovery codes</h3>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>
|
||||
<strong>Put these codes in a safe place.</strong>
|
||||
</p>
|
||||
<p>
|
||||
If you lose your device and don't have the recovery codes you will lose access to your account.
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@foreach (var recoveryCode in RecoveryCodes)
|
||||
{
|
||||
<div>
|
||||
<code class="recovery-code">@recoveryCode</code>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string[] RecoveryCodes { get; set; } = [];
|
||||
|
||||
[Parameter] public string? StatusMessage { get; set; }
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
@if (!string.IsNullOrEmpty(DisplayMessage))
|
||||
{
|
||||
var statusMessageClass = DisplayMessage.StartsWith("Error") ? "danger" : "success";
|
||||
<div class="alert alert-@statusMessageClass" role="alert">
|
||||
@DisplayMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private string? messageFromCookie;
|
||||
|
||||
[Parameter] public string? Message { get; set; }
|
||||
|
||||
[CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
private string? DisplayMessage => Message ?? messageFromCookie;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
messageFromCookie = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName];
|
||||
|
||||
if (messageFromCookie is not null)
|
||||
{
|
||||
HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,28 +26,27 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Account\Pages\AccessDenied.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\ConfirmEmail.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\ConfirmEmailChange.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\ExternalLogin.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\ForgotPassword.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\ForgotPasswordConfirmation.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\InvalidPasswordReset.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\InvalidUser.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\Lockout.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\Login.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\LoginWith2fa.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\LoginWithRecoveryCode.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\Register.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\RegisterConfirmation.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\ResendEmailConfirmation.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\ResetPassword.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\ResetPasswordConfirmation.razor" />
|
||||
<AdditionalFiles Include="Account\Pages\_Imports.razor" />
|
||||
<AdditionalFiles Include="Account\Shared\AuthLayout.razor" />
|
||||
<AdditionalFiles Include="Account\Shared\RedirectToLogin.razor" />
|
||||
<AdditionalFiles Include="Account\Shared\ShowRecoveryCodes.razor" />
|
||||
<AdditionalFiles Include="Account\Shared\StatusMessage.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\AccessDenied.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\ConfirmEmail.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\ConfirmEmailChange.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\ForgotPassword.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\ForgotPasswordConfirmation.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\InvalidPasswordReset.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\InvalidUser.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\Lockout.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\Login.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\LoginWith2fa.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\LoginWithRecoveryCode.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\Register.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\RegisterConfirmation.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\ResendEmailConfirmation.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\ResetPassword.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\ResetPasswordConfirmation.razor" />
|
||||
<AdditionalFiles Include="Auth\Pages\_Imports.razor" />
|
||||
<AdditionalFiles Include="Auth\Layout\AuthLayout.razor" />
|
||||
<AdditionalFiles Include="Auth\Layout\RedirectToLogin.razor" />
|
||||
<AdditionalFiles Include="Auth\Layout\ShowRecoveryCodes.razor" />
|
||||
<AdditionalFiles Include="Auth\Layout\StatusMessage.razor" />
|
||||
<AdditionalFiles Include="Main\Components\Alerts\AlertMessageError.razor" />
|
||||
<AdditionalFiles Include="Main\Components\Alerts\AlertMessageSuccess.razor" />
|
||||
<AdditionalFiles Include="Main\Components\Alerts\GlobalNotificationDisplay.razor" />
|
||||
|
||||
41
src/AliasVault.Admin2/Auth/Components/InputTextField.razor
Normal file
41
src/AliasVault.Admin2/Auth/Components/InputTextField.razor
Normal file
@@ -0,0 +1,41 @@
|
||||
@using System.Linq.Expressions
|
||||
|
||||
<InputText @attributes="AdditionalAttributes"
|
||||
id="@Id"
|
||||
Value="@Value"
|
||||
ValueChanged="ValueChanged"
|
||||
ValueExpression="ValueExpression"
|
||||
placeholder="@Placeholder"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the input field.
|
||||
/// </summary>
|
||||
[Parameter] public string Id { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the input field.
|
||||
/// </summary>
|
||||
[Parameter] public string Value { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the event callback that is triggered when the value changes.
|
||||
/// </summary>
|
||||
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the expression that identifies the value property.
|
||||
/// </summary>
|
||||
[Parameter] public Expression<Func<string>> ValueExpression { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the placeholder text for the input field.
|
||||
/// </summary>
|
||||
[Parameter] public string Placeholder { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional attributes for the input field.
|
||||
/// </summary>
|
||||
[Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object?>? AdditionalAttributes { get; set; } = new();
|
||||
}
|
||||
5
src/AliasVault.Admin2/Auth/Components/Logo.razor
Normal file
5
src/AliasVault.Admin2/Auth/Components/Logo.razor
Normal file
@@ -0,0 +1,5 @@
|
||||
<a href="/" class="flex items-center justify-center mb-8 text-2xl font-semibold lg:mb-10 dark:text-white">
|
||||
<img src="horizontal-logo-cropped.png" alt="AliasVault" class="img-fluid" style="max-width: 330px;"/>
|
||||
<span class="ps-2 self-center hidden sm:flex text-lg font-bold whitespace-nowrap text-white bg-red-600 rounded-full px-2 py-1 ml-2">Admin</span>
|
||||
|
||||
</a>
|
||||
@@ -1,13 +1,9 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using AliasServerDb;
|
||||
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 AliasVault.Admin2.Account.Pages;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing;
|
||||
|
||||
@@ -2,7 +2,7 @@ using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
|
||||
namespace AliasVault.Admin2.Account;
|
||||
namespace AliasVault.Admin2.Auth;
|
||||
|
||||
// Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation.
|
||||
internal sealed class IdentityNoOpEmailSender : IEmailSender<AdminUser>
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AliasVault.Admin2.Account;
|
||||
namespace AliasVault.Admin2.Auth;
|
||||
|
||||
internal sealed class IdentityRedirectManager(NavigationManager navigationManager)
|
||||
{
|
||||
@@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace AliasVault.Admin2.Account;
|
||||
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.
|
||||
34
src/AliasVault.Admin2/Auth/Layout/AuthLayout.razor
Normal file
34
src/AliasVault.Admin2/Auth/Layout/AuthLayout.razor
Normal file
@@ -0,0 +1,34 @@
|
||||
@inherits LayoutComponentBase
|
||||
@using AliasVault.Admin2.Auth.Components
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="flex flex-col items-center justify-center px-6 pt-8 mx-auto md:h-screen pt:mt-0 dark:bg-gray-900">
|
||||
<Logo />
|
||||
<div class="w-full max-w-xl p-6 space-y-4 sm:p-8 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
@Body
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
NavigationManager.LocationChanged += OnLocationChanged;
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
NavigationManager.LocationChanged -= OnLocationChanged;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/Account/AccessDenied"
|
||||
|
||||
<PageTitleLayout>Access denied</PageTitleLayout>
|
||||
<LayoutPageTitle>Access denied</LayoutPageTitle>
|
||||
|
||||
<header>
|
||||
<h1 class="text-danger">Access denied</h1>
|
||||
@@ -8,7 +8,7 @@
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
|
||||
<PageTitleLayout>Confirm email</PageTitleLayout>
|
||||
<LayoutPageTitle>Confirm email</LayoutPageTitle>
|
||||
|
||||
<h1>Confirm email</h1>
|
||||
<StatusMessage Message="@statusMessage"/>
|
||||
@@ -8,7 +8,7 @@
|
||||
@inject SignInManager<AdminUser> SignInManager
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
|
||||
<PageTitleLayout>Confirm email change</PageTitleLayout>
|
||||
<LayoutPageTitle>Confirm email change</LayoutPageTitle>
|
||||
|
||||
<h1>Confirm email change</h1>
|
||||
|
||||
67
src/AliasVault.Admin2/Auth/Pages/ForgotPassword.razor
Normal file
67
src/AliasVault.Admin2/Auth/Pages/ForgotPassword.razor
Normal file
@@ -0,0 +1,67 @@
|
||||
@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 IEmailSender<AdminUser> EmailSender
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
|
||||
<LayoutPageTitle>Forgot your password?</LayoutPageTitle>
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Forgot your password?
|
||||
</h2>
|
||||
<h3 class="text-xl text-gray-700 dark:text-gray-300 mb-4">
|
||||
Enter your email.
|
||||
</h3>
|
||||
<hr class="mb-6"/>
|
||||
<div class="w-full max-w-md">
|
||||
<EditForm Model="Input" FormName="forgot-password" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Email</label>
|
||||
<InputText @bind-Value="Input.Email" id="email" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="username" aria-required="true" placeholder="name@example.com"/>
|
||||
<ValidationMessage For="() => Input.Email" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
|
||||
</div>
|
||||
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Reset password</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@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
|
||||
RedirectManager.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 = NavigationManager.GetUriWithQueryParameters(
|
||||
NavigationManager.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri,
|
||||
new Dictionary<string, object?> { ["code"] = code });
|
||||
|
||||
await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
|
||||
|
||||
RedirectManager.RedirectTo("user/forgot-password-confirm");
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Required] [EmailAddress] public string Email { get; set; } = "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
@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>
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/Account/InvalidPasswordReset"
|
||||
|
||||
<PageTitleLayout>Invalid password reset</PageTitleLayout>
|
||||
<LayoutPageTitle>Invalid password reset</LayoutPageTitle>
|
||||
|
||||
<h1>Invalid password reset</h1>
|
||||
<p>
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/Account/InvalidUser"
|
||||
|
||||
<PageTitleLayout>Invalid user</PageTitleLayout>
|
||||
<LayoutPageTitle>Invalid user</LayoutPageTitle>
|
||||
|
||||
<h3>Invalid user</h3>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/Account/Lockout"
|
||||
|
||||
<PageTitleLayout>Locked out</PageTitleLayout>
|
||||
<LayoutPageTitle>Locked out</LayoutPageTitle>
|
||||
|
||||
<header>
|
||||
<h1 class="text-danger">Locked out</h1>
|
||||
104
src/AliasVault.Admin2/Auth/Pages/Login.razor
Normal file
104
src/AliasVault.Admin2/Auth/Pages/Login.razor
Normal file
@@ -0,0 +1,104 @@
|
||||
@page "/user/login"
|
||||
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Authentication
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject SignInManager<AdminUser> SignInManager
|
||||
@inject ILogger<Login> Logger
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
|
||||
<LayoutPageTitle>Log in</LayoutPageTitle>
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Sign in to AliasVault Admin
|
||||
</h2>
|
||||
|
||||
<EditForm Model="Input" FormName="LoginForm" OnValidSubmit="LoginUser" class="mt-8 space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||
<InputTextField id="email" @bind-Value="Input.Email" placeholder="name@company.com" />
|
||||
<ValidationMessage For="() => Input.Email"/>
|
||||
</div>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
|
||||
<InputTextField id="password" @bind-Value="Input.Password" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => Input.Password"/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input id="remember" aria-describedby="remember" name="remember" type="checkbox" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600">
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="remember" class="font-medium text-gray-900 dark:text-white">Remember me</label>
|
||||
</div>
|
||||
<a href="/user/forgot-password" class="ml-auto text-sm text-primary-700 hover:underline dark:text-primary-500">Lost Password?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Login to your account</button>
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Not registered? <a href="/user/register" class="text-primary-700 hover:underline dark:text-primary-500">Create account</a>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
|
||||
@code {
|
||||
private string? errorMessage;
|
||||
|
||||
[CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
|
||||
|
||||
[SupplyParameterFromQuery] private string? ReturnUrl { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (HttpMethods.IsGet(HttpContext.Request.Method))
|
||||
{
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoginUser()
|
||||
{
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
Logger.LogInformation("User logged in.");
|
||||
RedirectManager.RedirectTo(ReturnUrl);
|
||||
}
|
||||
else if (result.RequiresTwoFactor)
|
||||
{
|
||||
RedirectManager.RedirectTo(
|
||||
"user/loginWith2fa",
|
||||
new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
Logger.LogWarning("User account locked out.");
|
||||
RedirectManager.RedirectTo("Account/Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Error: Invalid login attempt.";
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Required] [EmailAddress] public string Email { get; set; } = "";
|
||||
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; } = "";
|
||||
|
||||
[Display(Name = "Remember me?")] public bool RememberMe { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
98
src/AliasVault.Admin2/Auth/Pages/LoginWith2fa.razor
Normal file
98
src/AliasVault.Admin2/Auth/Pages/LoginWith2fa.razor
Normal file
@@ -0,0 +1,98 @@
|
||||
@page "/user/loginWith2fa"
|
||||
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject SignInManager<AdminUser> SignInManager
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
@inject ILogger<LoginWith2fa> Logger
|
||||
|
||||
<LayoutPageTitle>Two-factor authentication</LayoutPageTitle>
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Two-factor authentication
|
||||
</h2>
|
||||
<hr class="mb-4"/>
|
||||
<StatusMessage Message="@message" class="mb-4"/>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">Your login is protected with an authenticator app. Enter your authenticator code below.</p>
|
||||
<div class="w-full max-w-md">
|
||||
<EditForm Model="Input" FormName="login-with-2fa" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-6">
|
||||
<input type="hidden" name="ReturnUrl" value="@ReturnUrl"/>
|
||||
<input type="hidden" name="RememberMe" value="@RememberMe"/>
|
||||
<DataAnnotationsValidator/>
|
||||
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
|
||||
<div>
|
||||
<label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Authenticator code</label>
|
||||
<InputText @bind-Value="Input.TwoFactorCode" id="two-factor-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off"/>
|
||||
<ValidationMessage For="() => Input.TwoFactorCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<InputCheckbox @bind-Value="Input.RememberMachine" id="remember-machine" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800"/>
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="remember-machine" class="font-medium text-gray-900 dark:text-white">Remember this machine</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Log in</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
<p class="mt-6 text-sm text-gray-700 dark:text-gray-300">
|
||||
Don't have access to your authenticator device? You can
|
||||
<a href="user/loginWithRecoveryCode?ReturnUrl=@ReturnUrl" class="text-primary-600 hover:underline dark:text-primary-500">log in with a recovery code</a>.
|
||||
</p>
|
||||
|
||||
@code {
|
||||
private string? message;
|
||||
private AdminUser user = default!;
|
||||
|
||||
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
|
||||
|
||||
[SupplyParameterFromQuery] private string? ReturnUrl { get; set; }
|
||||
|
||||
[SupplyParameterFromQuery] private bool RememberMe { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ??
|
||||
throw new InvalidOperationException("Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
private async Task OnValidSubmitAsync()
|
||||
{
|
||||
var authenticatorCode = Input.TwoFactorCode!.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
var result = await SignInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, RememberMe, Input.RememberMachine);
|
||||
var userId = await UserManager.GetUserIdAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", userId);
|
||||
RedirectManager.RedirectTo(ReturnUrl);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
Logger.LogWarning("User with ID '{UserId}' account locked out.", userId);
|
||||
RedirectManager.RedirectTo("Account/Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId);
|
||||
message = "Error: Invalid authenticator code.";
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Authenticator code")]
|
||||
public string? TwoFactorCode { get; set; }
|
||||
|
||||
[Display(Name = "Remember this machine")]
|
||||
public bool RememberMachine { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,28 +8,28 @@
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
@inject ILogger<LoginWithRecoveryCode> Logger
|
||||
|
||||
<PageTitleLayout>Recovery code verification</PageTitleLayout>
|
||||
<LayoutPageTitle>Recovery code verification</LayoutPageTitle>
|
||||
|
||||
<h1>Recovery code verification</h1>
|
||||
<hr/>
|
||||
<StatusMessage Message="@message"/>
|
||||
<p>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Recovery code verification
|
||||
</h2>
|
||||
<hr class="mb-4"/>
|
||||
<StatusMessage Message="@message" class="mb-4"/>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">
|
||||
You have requested to log in with a recovery code. This login will not be remembered until you provide
|
||||
an authenticator app code at log in or disable 2FA and log in again.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<EditForm Model="Input" FormName="login-with-recovery-code" OnValidSubmit="OnValidSubmitAsync" method="post">
|
||||
<DataAnnotationsValidator/>
|
||||
<ValidationSummary class="text-danger" role="alert"/>
|
||||
<div class="form-floating mb-3">
|
||||
<InputText @bind-Value="Input.RecoveryCode" class="form-control" autocomplete="off" placeholder="RecoveryCode"/>
|
||||
<label for="recovery-code" class="form-label">Recovery Code</label>
|
||||
<ValidationMessage For="() => Input.RecoveryCode" class="text-danger"/>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
<div class="w-full max-w-md">
|
||||
<EditForm Model="Input" FormName="login-with-recovery-code" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
|
||||
<div>
|
||||
<label for="recovery-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Recovery Code</label>
|
||||
<InputText @bind-Value="Input.RecoveryCode" id="recovery-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off" placeholder="Enter your recovery code"/>
|
||||
<ValidationMessage For="() => Input.RecoveryCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
|
||||
</div>
|
||||
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Log in</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
38
src/AliasVault.Admin2/Auth/Pages/Logout.razor
Normal file
38
src/AliasVault.Admin2/Auth/Pages/Logout.razor
Normal file
@@ -0,0 +1,38 @@
|
||||
@page "/user/logout"
|
||||
@using AliasVault.Admin2.Services
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@inject SignInManager<AdminUser> SignInManager
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
|
||||
@code {
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Sign out the user.
|
||||
// NOTE: the try/catch below is a workaround for the issue that the sign out does not work when
|
||||
// the server session is already started.
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
await SignInManager.SignOutAsync();
|
||||
GlobalNotificationService.ClearMessages();
|
||||
|
||||
// Redirect to the home page with hard refresh.
|
||||
NavigationManager.NavigateTo("/", true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Hard refresh current page if sign out fails. When an interactive server session is already started
|
||||
// the sign out will fail because it tries to mutate cookies which is only possible when the server
|
||||
// session is not started yet.
|
||||
NavigationManager.NavigateTo(NavigationManager.Uri, true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Redirect to the home page with hard refresh.
|
||||
NavigationManager.NavigateTo("/", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,44 +14,35 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
|
||||
<PageTitleLayout>Register</PageTitleLayout>
|
||||
<LayoutPageTitle>Register</LayoutPageTitle>
|
||||
|
||||
<h1>Register</h1>
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Register admin account
|
||||
</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<StatusMessage Message="@Message"/>
|
||||
<EditForm Model="Input" asp-route-returnUrl="@ReturnUrl" method="post" OnValidSubmit="RegisterUser" FormName="register">
|
||||
<DataAnnotationsValidator/>
|
||||
<h2>Create a new account.</h2>
|
||||
<hr/>
|
||||
<ValidationSummary class="text-danger" role="alert"/>
|
||||
<div class="form-floating mb-3">
|
||||
<InputText @bind-Value="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com"/>
|
||||
<label for="email">Email</label>
|
||||
<ValidationMessage For="() => Input.Email" class="text-danger"/>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password"/>
|
||||
<label for="password">Password</label>
|
||||
<ValidationMessage For="() => Input.Password" 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="password"/>
|
||||
<label for="confirm-password">Confirm Password</label>
|
||||
<ValidationMessage For="() => Input.ConfirmPassword" class="text-danger"/>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
|
||||
</EditForm>
|
||||
<EditForm Model="Input" OnValidSubmit="RegisterUser" FormName="register" class="mt-8 space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||
<InputTextField id="email" @bind-Value="Input.Email" placeholder="name@company.com" />
|
||||
<ValidationMessage For="() => Input.Email"/>
|
||||
</div>
|
||||
<div class="col-md-6 col-md-offset-2">
|
||||
<section>
|
||||
<h3>Use another service to register.</h3>
|
||||
<hr/>
|
||||
<ExternalLoginPicker/>
|
||||
</section>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
|
||||
<InputTextField id="password" @bind-Value="Input.Password" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => Input.Password"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm password</label>
|
||||
<InputTextField id="password2" @bind-Value="Input.ConfirmPassword" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => Input.ConfirmPassword"/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Create account</button>
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Already registered? <a href="/user/login" class="text-primary-700 hover:underline dark:text-primary-500">Login here</a>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@code {
|
||||
private IEnumerable<IdentityError>? identityErrors;
|
||||
@@ -9,7 +9,7 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
|
||||
<PageTitleLayout>Register confirmation</PageTitleLayout>
|
||||
<LayoutPageTitle>Register confirmation</LayoutPageTitle>
|
||||
|
||||
<h1>Register confirmation</h1>
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
@inject IEmailSender<AdminUser> EmailSender
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
|
||||
<PageTitleLayout>Resend email confirmation</PageTitleLayout>
|
||||
<LayoutPageTitle>Resend email confirmation</LayoutPageTitle>
|
||||
|
||||
<h1>Resend email confirmation</h1>
|
||||
<h2>Enter your email.</h2>
|
||||
@@ -8,7 +8,7 @@
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
|
||||
<PageTitleLayout>Reset password</PageTitleLayout>
|
||||
<LayoutPageTitle>Reset password</LayoutPageTitle>
|
||||
|
||||
<h1>Reset password</h1>
|
||||
<h2>Reset your password.</h2>
|
||||
@@ -1,5 +1,5 @@
|
||||
@page "/Account/ResetPasswordConfirmation"
|
||||
<PageTitleLayout>Reset password confirmation</PageTitleLayout>
|
||||
<LayoutPageTitle>Reset password confirmation</LayoutPageTitle>
|
||||
|
||||
<h1>Reset password confirmation</h1>
|
||||
<p>
|
||||
5
src/AliasVault.Admin2/Auth/Pages/_Imports.razor
Normal file
5
src/AliasVault.Admin2/Auth/Pages/_Imports.razor
Normal file
@@ -0,0 +1,5 @@
|
||||
@using AliasVault.Admin2.Auth.Components
|
||||
@using AliasVault.Admin2.Auth.Layout
|
||||
@using AliasVault.Admin2.Main.Components.Layout
|
||||
@using AliasVault.Admin2.Main.Layout
|
||||
@layout AuthLayout
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
<body class="bg-gray-50 dark:bg-gray-800">
|
||||
<Routes @rendermode="RenderModeForPage"/>
|
||||
<script src="lib/qrcode.min.js?v=dev"></script>
|
||||
<script src="js/dark-mode.js?v=dev"></script>
|
||||
<script src="js/utilities.js?v=dev"></script>
|
||||
|
||||
@@ -47,6 +48,10 @@
|
||||
}
|
||||
};
|
||||
|
||||
window.isFunctionDefined = function(functionName) {
|
||||
return typeof window[functionName] === 'function';
|
||||
};
|
||||
|
||||
// Primarily used by E2E tests.
|
||||
window.blazorNavigate = (url) => {
|
||||
Blazor.navigateTo(url);
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
@inject UrlEncoder UrlEncoder
|
||||
@inject IdentityRedirectManager RedirectManager
|
||||
@inject ILogger<EnableAuthenticator> Logger
|
||||
|
||||
<PageTitleLayout>Configure authenticator app</PageTitleLayout>
|
||||
@@ -38,7 +37,7 @@ else
|
||||
<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 data-url="@authenticatorUri"></div>
|
||||
<div id="authenticator-uri" data-url="@authenticatorUri"></div>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
@@ -79,6 +78,8 @@ else
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
await LoadSharedKeyAndQrCodeUriAsync(UserService.User());
|
||||
|
||||
await JsInvokeService.RetryInvokeAsync("generateQrCode", TimeSpan.Zero, 5, "authenticator-uri");
|
||||
}
|
||||
|
||||
private async Task OnValidSubmitAsync()
|
||||
@@ -151,7 +152,7 @@ else
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
AuthenticatorUriFormat,
|
||||
UrlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"),
|
||||
UrlEncoder.Encode("AliasVault Admin"),
|
||||
UrlEncoder.Encode(email),
|
||||
unformattedKey);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@layout ManageLayout
|
||||
@inherits MainBase
|
||||
@using AliasVault.Admin2.Account
|
||||
@using AliasVault.Admin2.Auth
|
||||
@using AliasVault.Admin2.Main.Pages.Account.Manage.Components
|
||||
@using AliasVault.Admin2.Main.Components.Layout
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace AliasVault.Admin2.Main.Pages;
|
||||
namespace AliasVault.Admin2.Main.Pages;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Admin2.Main.Models;
|
||||
using AliasVault.Admin2.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@using AliasVault.Admin2.Account.Shared
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
|
||||
<NotAuthorized>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Microsoft.JSInterop
|
||||
@using AliasVault.Admin2
|
||||
@using AliasVault.Admin2.Auth.Components
|
||||
@using AliasVault.Admin2.Main
|
||||
@using AliasVault.Admin2.Main.Components
|
||||
@using AliasVault.Admin2.Main.Components.Alerts
|
||||
|
||||
@@ -3,7 +3,7 @@ using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using AliasVault.Admin2.Account;
|
||||
using AliasVault.Admin2.Auth;
|
||||
using AliasVault.Admin2.Main;
|
||||
using AliasVault.Admin2.Services;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
@@ -728,6 +728,26 @@ video {
|
||||
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-6 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
@@ -790,6 +810,10 @@ video {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.h-4 {
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
@@ -818,10 +842,22 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.max-w-screen-2xl {
|
||||
max-width: 1536px;
|
||||
}
|
||||
|
||||
.max-w-xl {
|
||||
max-width: 36rem;
|
||||
}
|
||||
|
||||
.max-w-md {
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -858,6 +894,10 @@ video {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
@@ -900,6 +940,18 @@ video {
|
||||
margin-bottom: calc(0.25rem * 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)));
|
||||
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-y-6 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.divide-y > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-divide-y-reverse: 0;
|
||||
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
||||
@@ -1048,6 +1100,16 @@ video {
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -1064,6 +1126,10 @@ video {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.px-3 {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
@@ -1114,6 +1180,16 @@ video {
|
||||
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;
|
||||
}
|
||||
|
||||
.ps-2 {
|
||||
padding-inline-start: 0.5rem;
|
||||
}
|
||||
@@ -1126,6 +1202,10 @@ video {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.pt-8 {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -1255,6 +1335,16 @@ video {
|
||||
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));
|
||||
}
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -1336,6 +1426,16 @@ video {
|
||||
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));
|
||||
}
|
||||
|
||||
.hover\:bg-primary-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-gray-900:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||
@@ -1366,6 +1466,16 @@ video {
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.focus\:border-primary-500:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(244 149 65 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.focus\:border-primary-600:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(214 131 56 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.focus\:outline-none:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
@@ -1398,6 +1508,21 @@ video {
|
||||
--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));
|
||||
}
|
||||
|
||||
.focus\:ring-primary-300:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(248 185 99 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-primary-600:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(214 131 56 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark\:divide-gray-600:is(.dark *) > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-divide-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-divide-opacity));
|
||||
@@ -1408,6 +1533,11 @@ video {
|
||||
border-color: rgb(55 65 81 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-600:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-700:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
|
||||
@@ -1423,6 +1553,11 @@ video {
|
||||
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-primary-600:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-opacity-80:is(.dark *) {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
@@ -1462,6 +1597,20 @@ video {
|
||||
color: rgb(255 255 255 / 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));
|
||||
}
|
||||
|
||||
.dark\:placeholder-gray-400:is(.dark *)::placeholder {
|
||||
--tw-placeholder-opacity: 1;
|
||||
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
|
||||
}
|
||||
|
||||
.dark\:ring-offset-gray-800:is(.dark *) {
|
||||
--tw-ring-offset-color: #1f2937;
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-gray-600:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
|
||||
@@ -1472,6 +1621,11 @@ video {
|
||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-primary-700:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(184 112 47 / 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));
|
||||
@@ -1482,6 +1636,16 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-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-gray-600:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity));
|
||||
@@ -1492,19 +1656,52 @@ video {
|
||||
--tw-ring-color: rgb(55 65 81 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:ring-primary-500:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(244 149 65 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:ring-primary-600:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(214 131 56 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:ring-primary-800:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--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;
|
||||
}
|
||||
|
||||
.sm\:w-auto {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.sm\:rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.sm\:p-8 {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.sm\:text-2xl {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.sm\:text-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -1528,6 +1725,10 @@ video {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.md\:h-screen {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.md\:w-1\/4 {
|
||||
width: 25%;
|
||||
}
|
||||
@@ -1585,6 +1786,10 @@ video {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.lg\:mb-10 {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.lg\:flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -9,3 +9,46 @@ function downloadFileFromStream(fileName, contentStreamReference) {
|
||||
anchorElement.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a QR code for the given id element that has a data-url attribute.
|
||||
* @param id
|
||||
*/
|
||||
function generateQrCode(id) {
|
||||
console.log(`Generating QR code for element with id "${id}".`);
|
||||
// Find the element by id
|
||||
const element = document.getElementById(id);
|
||||
|
||||
// Check if the element exists
|
||||
if (!element) {
|
||||
console.log(`Element with id "${id}" not found. QR code generation aborted.`);
|
||||
return; // Silently fail
|
||||
}
|
||||
|
||||
// Get the data-url attribute
|
||||
const dataUrl = element.getAttribute('data-url');
|
||||
|
||||
// Check if data-url exists
|
||||
if (!dataUrl) {
|
||||
console.log(`No data-url attribute found on element with id "${id}". QR code generation aborted.`);
|
||||
return; // Silently fail
|
||||
}
|
||||
|
||||
// Create a container for the QR code
|
||||
const qrContainer = document.createElement('div');
|
||||
qrContainer.id = `qrcode-${id}`;
|
||||
element.appendChild(qrContainer);
|
||||
|
||||
// Initialize QRCode object
|
||||
const qrcode = new QRCode(qrContainer, {
|
||||
text: dataUrl,
|
||||
width: 256,
|
||||
height: 256,
|
||||
colorDark : "#000000",
|
||||
colorLight : "#ffffff",
|
||||
correctLevel : QRCode.CorrectLevel.H
|
||||
});
|
||||
|
||||
console.log(`QR code generated successfully for element with id "${id}".`);
|
||||
}
|
||||
|
||||
|
||||
1
src/AliasVault.Admin2/wwwroot/lib/qrcode.min.js
vendored
Normal file
1
src/AliasVault.Admin2/wwwroot/lib/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user