Add form validation to login/register pages (#23)

This commit is contained in:
Leendert de Borst
2024-06-17 22:08:49 +02:00
parent 7a657ea0f0
commit da25aa43ea
16 changed files with 434 additions and 169 deletions

View File

@@ -5,6 +5,8 @@
// </copyright>
//-----------------------------------------------------------------------
using AliasVault.Shared.Models.WebApi;
namespace AliasVault.Api.Controllers;
using System.IdentityModel.Tokens.Jwt;
@@ -43,7 +45,7 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
return Ok(tokenModel);
}
return Unauthorized();
return BadRequest(ServerValidationErrorResponse.Create("Invalid username or password. Please try again.", 400));
}
/// <summary>
@@ -151,10 +153,9 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
var tokenModel = await GenerateNewTokenForUser(user);
return Ok(tokenModel);
}
else
{
return BadRequest(result.Errors);
}
var errors = result.Errors.Select(e => e.Description).ToArray();
return BadRequest(ServerValidationErrorResponse.Create(errors, 400));
}
private string GenerateJwtToken(IdentityUser user)

View File

@@ -5,6 +5,8 @@
// </copyright>
//-----------------------------------------------------------------------
using System.ComponentModel.DataAnnotations;
namespace AliasVault.Shared.Models;
/// <summary>
@@ -15,10 +17,13 @@ public class LoginModel
/// <summary>
/// Gets or sets the email.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; } = null!;
/// <summary>
/// Gets or sets the password.
/// </summary>
[Required]
public string Password { get; set; } = null!;
}

View File

@@ -5,6 +5,9 @@
// </copyright>
//-----------------------------------------------------------------------
using System.ComponentModel.DataAnnotations;
using AliasVault.Shared.Models.Validation;
namespace AliasVault.Shared.Models;
/// <summary>
@@ -15,20 +18,27 @@ public class RegisterModel
/// <summary>
/// Gets or sets the email.
/// </summary>
[Required]
[EmailAddress]
public string Email { get; set; } = null!;
/// <summary>
/// Gets or sets the password.
/// </summary>
[Required]
[MinLength(8, ErrorMessage = "Password must be at least 8 characters long.")]
public string Password { get; set; } = null!;
/// <summary>
/// Gets or sets the password confirmation.
/// </summary>
[Required]
[Compare("Password", ErrorMessage = "Passwords do not match.")]
public string PasswordConfirm { get; set; } = null!;
/// <summary>
/// Gets or sets a value indicating whether the terms and conditions are accepted or not.
/// </summary>
[MustBeTrue(ErrorMessage = "You must accept the terms and conditions.")]
public bool AcceptTerms { get; set; } = false;
}

View File

@@ -0,0 +1,30 @@
//-----------------------------------------------------------------------
// <copyright file="MustBeTrueAttribute.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Shared.Models.Validation;
using System.ComponentModel.DataAnnotations;
/// <summary>
/// Validation attribute to ensure that a boolean property is true.
/// </summary>
public class MustBeTrueAttribute : ValidationAttribute
{
/// <inheritdoc />
public override bool IsValid(object? value)
{
switch (value)
{
case null:
return false;
case bool b:
return b;
default:
throw new InvalidOperationException("Can only be used on boolean properties.");
}
}
}

View File

@@ -0,0 +1,47 @@
//-----------------------------------------------------------------------
// <copyright file="AliasEdit.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Shared.Models.WebApi;
using System.ComponentModel.DataAnnotations;
/// <summary>
/// Alias model.
/// </summary>
public class AliasEdit
{
/// <summary>
/// Gets or sets the name of the service.
/// </summary>
[Required]
public string ServiceName { get; set; } = null!;
/// <summary>
/// Gets or sets the URL of the service.
/// </summary>
public string? ServiceUrl { get; set; }
/// <summary>
/// Gets or sets the Alias Identity object.
/// </summary>
public Identity Identity { get; set; } = null!;
/// <summary>
/// Gets or sets the Alias Password object.
/// </summary>
public Password Password { get; set; } = null!;
/// <summary>
/// Gets or sets the Alias CreateDate.
/// </summary>
public DateTime CreateDate { get; set; }
/// <summary>
/// Gets or sets the Alias LastUpdate.
/// </summary>
public DateTime LastUpdate { get; set; }
}

View File

@@ -0,0 +1,93 @@
//-----------------------------------------------------------------------
// <copyright file="ServerValidationErrorResponse.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Shared.Models.WebApi;
using System.Text.Json.Serialization;
/// <summary>
/// Represents the structure of a validation error response from the API.
/// </summary>
public class ServerValidationErrorResponse
{
/// <summary>
/// Gets or sets the type of the error.
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; } = null!;
/// <summary>
/// Gets or sets the title of the error.
/// </summary>
[JsonPropertyName("title")]
public string Title { get; set; } = null!;
/// <summary>
/// Gets or sets the HTTP status code of the response.
/// </summary>
[JsonPropertyName("status")]
public int Status { get; set; }
/// <summary>
/// Gets or sets the validation errors. The key is the name of the field that has the error, and the value is an array of error messages for that field.
/// </summary>
[JsonPropertyName("errors")]
public Dictionary<string, string[]> Errors { get; set; } = new();
/// <summary>
/// Gets or sets the trace ID of the error.
/// </summary>
[JsonPropertyName("traceId")]
public string TraceId { get; set; } = null!;
/// <summary>
/// Creates a new instance of <see cref="ServerValidationErrorResponse"/>.
/// </summary>
/// <param name="title">Title of the error.</param>
/// <param name="status">Status code.</param>
/// <returns>ServerValidationErrorResponse object.</returns>
public static ServerValidationErrorResponse Create(string title, int status)
{
var errors = new Dictionary<string, string[]>
{
{ title, [title] },
};
return new ServerValidationErrorResponse
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = title,
Errors = errors,
Status = status,
TraceId = Guid.NewGuid().ToString(),
};
}
/// <summary>
/// Creates a new instance of <see cref="ServerValidationErrorResponse"/>.
/// </summary>
/// <param name="errorArray">Array with errors.</param>
/// <param name="status">Status code.</param>
/// <returns>ServerValidationErrorResponse object.</returns>
public static ServerValidationErrorResponse Create(string[] errorArray, int status)
{
var errors = new Dictionary<string, string[]>();
foreach (var t in errorArray)
{
errors.Add(t, new[] { t });
}
return new ServerValidationErrorResponse
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = errorArray.First(),
Errors = errors,
Status = status,
TraceId = Guid.NewGuid().ToString(),
};
}
}

View File

@@ -7,6 +7,8 @@
namespace AliasVault.Shared.Models.WebApi;
using System.ComponentModel.DataAnnotations;
/// <summary>
/// Service model.
/// </summary>
@@ -15,6 +17,7 @@ public class Service
/// <summary>
/// Gets or sets the name of the service.
/// </summary>
[Required]
public string Name { get; set; } = null!;
/// <summary>

View File

@@ -8,6 +8,7 @@
@inject AuthService AuthService
@using System.Text.Json
@using AliasVault.Shared.Models
@using AliasVault.Shared.Models.WebApi
@using AliasVault.WebApp.Auth.Components
@using AliasVault.WebApp.Auth.Services
@@ -15,14 +16,20 @@
Sign in to AliasVault
</h2>
<EditForm Model="_user" OnSubmit="HandleLogin" class="mt-8 space-y-6">
<FullScreenLoadingIndicator @ref="_loadingIndicator" />
<ServerValidationErrors @ref="_serverValidationErrors" />
<EditForm Model="_loginModel" OnValidSubmit="HandleLogin" 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="_user.Email" placeholder="name@company.com" />
<InputTextField id="email" @bind-Value="_loginModel.Email" placeholder="name@company.com" />
<ValidationMessage For="() => _loginModel.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="_user.Password" type="password" placeholder="••••••••" />
<InputTextField id="password" @bind-Value="_loginModel.Password" type="password" placeholder="••••••••" />
<ValidationMessage For="() => _loginModel.Password"/>
</div>
<div class="flex items-start">
@@ -41,12 +48,10 @@
</div>
</EditForm>
<FullScreenLoadingIndicator @ref="_loadingIndicator" />
@code {
private LoginModel _user = new LoginModel();
private readonly LoginModel _loginModel = new();
private FullScreenLoadingIndicator _loadingIndicator = new();
private ServerValidationErrors _serverValidationErrors = new();
/// <inheritdoc />
protected override async Task OnInitializedAsync()
@@ -62,11 +67,20 @@
private async Task HandleLogin()
{
_loadingIndicator.Show();
_serverValidationErrors.Clear();
try
{
var result = await Http.PostAsJsonAsync("api/Auth/login", _user);
var result = await Http.PostAsJsonAsync("api/Auth/login", _loginModel);
var responseContent = await result.Content.ReadAsStringAsync();
if (!result.IsSuccessStatusCode)
{
_serverValidationErrors.ParseResponse(responseContent);
StateHasChanged();
return;
}
var tokenObject = JsonSerializer.Deserialize<TokenModel>(responseContent);
if (tokenObject != null)
@@ -80,6 +94,7 @@
{
// Handle the case where the token is not present in the response
Console.WriteLine("Token not found in the response.");
return;
}
await AuthStateProvider.GetAuthenticationStateAsync();

View File

@@ -14,26 +14,34 @@
Create a Free Account
</h2>
<EditForm Model="user" OnSubmit="HandleRegister" class="mt-8 space-y-6">
<FullScreenLoadingIndicator @ref="_loadingIndicator" />
<ServerValidationErrors @ref="_serverValidationErrors" />
<EditForm Model="_registerModel" OnValidSubmit="HandleRegister" 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="user.Email" placeholder="name@company.com" />
<InputTextField id="email" @bind-Value="_registerModel.Email" placeholder="name@company.com" />
<ValidationMessage For="() => _registerModel.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="user.Password" type="password" placeholder="••••••••" />
<InputTextField id="password" @bind-Value="_registerModel.Password" type="password" placeholder="••••••••" />
<ValidationMessage For="() => _registerModel.Password"/>
</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="user.PasswordConfirm" type="password" placeholder="••••••••" />
<InputTextField id="password2" @bind-Value="_registerModel.PasswordConfirm" type="password" placeholder="••••••••" />
<ValidationMessage For="() => _registerModel.PasswordConfirm"/>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="terms" aria-describedby="terms" name="terms" 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" required>
<InputCheckbox id="terms" @bind-Value="_registerModel.AcceptTerms" 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="terms" class="font-medium text-gray-900 dark:text-white">I accept the <a href="#" class="text-primary-700 hover:underline dark:text-primary-500">Terms and Conditions</a></label>
<ValidationMessage For="() => _registerModel.AcceptTerms"/>
</div>
</div>
@@ -43,79 +51,48 @@
</div>
</EditForm>
@if (validationErrors.Any())
{
<div class="alert alert-danger">
<ul>
@foreach (var error in validationErrors)
{
<li>@error</li>
}
</ul>
</div>
}
<FullScreenLoadingIndicator @ref="loadingIndicator" />
@code {
RegisterModel user = new();
FullScreenLoadingIndicator loadingIndicator = new();
List<string> validationErrors = [];
private readonly RegisterModel _registerModel = new();
private FullScreenLoadingIndicator _loadingIndicator = new();
private ServerValidationErrors _serverValidationErrors = new();
async Task HandleRegister()
{
loadingIndicator.Show();
validationErrors.Clear();
_loadingIndicator.Show();
_serverValidationErrors.Clear();
try
{
var result = await Http.PostAsJsonAsync("api/Auth/register", user);
var result = await Http.PostAsJsonAsync("api/Auth/register", _registerModel);
var responseContent = await result.Content.ReadAsStringAsync();
if (result.IsSuccessStatusCode)
if (!result.IsSuccessStatusCode)
{
var responseContent = await result.Content.ReadAsStringAsync();
var tokenObject = JsonSerializer.Deserialize<TokenModel>(responseContent);
_serverValidationErrors.ParseResponse(responseContent);
StateHasChanged();
return;
}
if (tokenObject != null)
{
// Store the token as a plain string in local storage
await AuthService.StoreAccessTokenAsync(tokenObject.Token);
await AuthService.StoreRefreshTokenAsync(tokenObject.RefreshToken);
await AuthStateProvider.GetAuthenticationStateAsync();
}
else
{
// Handle the case where the token is not present in the response
Console.WriteLine("Token not found in the response.");
}
var tokenObject = JsonSerializer.Deserialize<TokenModel>(responseContent);
NavigationManager.NavigateTo("/");
if (tokenObject != null)
{
// Store the token as a plain string in local storage
await AuthService.StoreAccessTokenAsync(tokenObject.Token);
await AuthService.StoreRefreshTokenAsync(tokenObject.RefreshToken);
await AuthStateProvider.GetAuthenticationStateAsync();
}
else
{
var responseContent = await result.Content.ReadAsStringAsync();
var errorResponse = System.Text.Json.JsonSerializer.Deserialize<ValidationErrorResponse>(responseContent);
if (errorResponse != null && errorResponse.Errors != null)
{
foreach (var error in errorResponse.Errors.Values)
{
validationErrors.AddRange(error);
}
}
// Handle the case where the token is not present in the response
Console.WriteLine("Token not found in the response.");
}
NavigationManager.NavigateTo("/");
}
finally
{
loadingIndicator.Hide();
_loadingIndicator.Hide();
}
}
public class ValidationErrorResponse
{
public string Type { get; set; } = null!;
public string Title { get; set; } = null!;
public int Status { get; set; }
public Dictionary<string, string[]> Errors { get; set; } = new();
public string TraceId { get; set; } = null!;
}
}

View File

@@ -11,6 +11,9 @@
</div>
@code {
/// <summary>
/// The message to show.
/// </summary>
[Parameter]
public string Message { get; set; } = string.Empty;
}

View File

@@ -11,7 +11,9 @@
</div>
@code {
/// <summary>
/// The message to show.
/// </summary>
[Parameter]
public string Message { get; set; } = string.Empty;
}

View File

@@ -48,7 +48,7 @@
/// <summary>
/// Refreshes the messages by adding any new messages from the PortalMessageService.
/// </summary>
public void RefreshAddMessages()
private void RefreshAddMessages()
{
// We retrieve any additional messages from the GlobalNotificationService that we do not yet have.
var newMessages = GlobalNotificationService.GetMessagesForDisplay();

View File

@@ -0,0 +1,41 @@
@using AliasVault.Shared.Models.WebApi
@if (_errors.Any())
{
@foreach (var error in _errors)
{
<AlertMessageError Message="@error" />
}
}
@code {
private bool IsVisible { get; set; }
private List<string> _errors = [];
/// <summary>
/// Parses the response content and displays the server validation errors.
/// </summary>
public void ParseResponse(string responseContent)
{
_errors.Clear();
var errorResponse = System.Text.Json.JsonSerializer.Deserialize<ServerValidationErrorResponse>(responseContent);
if (errorResponse is not null)
{
foreach (var error in errorResponse.Errors)
{
_errors.AddRange(error.Value);
}
}
StateHasChanged();
}
/// <summary>
/// Clears the server validation errors.
/// </summary>
public void Clear()
{
_errors.Clear();
StateHasChanged();
}
}

View File

@@ -43,7 +43,7 @@
{
<div class="z-50 my-4 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600" id="userMenuDropdown" data-popper-placement="bottom" style="position: absolute; inset: 0px auto auto 0px; margin: 0px; margin-left: -200px; transform: translate3d(1537.5px, 58px, 0px);">
<div class="py-3 px-4">
<span class="block text-sm font-semibold text-gray-900 dark:text-white">@Username</span>
<span class="block text-sm font-semibold text-gray-900 dark:text-white">@_username</span>
</div>
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="userMenuDropdownButton">
<li>
@@ -101,14 +101,16 @@
@code {
private bool isMenuOpen = false;
public string Username { get; set; } = "";
private string _username { get; set; } = "";
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
Username = await GetUsernameAsync();
_username = await GetUsernameAsync();
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);

View File

@@ -43,103 +43,107 @@ else {
}
else
{
<div class="grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
<div class="col">
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">Service</h3>
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="service-name" Label="Service Name" Value="@(Obj.Service.Name)" ValueChanged="@(val => Obj.Service.Name = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="service-url" Label="Service URL" Value="@(Obj.Service.Url)" ValueChanged="@(val => Obj.Service.Url = val)"></EditFormRow>
<EditForm Model="Obj" OnValidSubmit="SaveAlias">
<DataAnnotationsValidator />
<div class="grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
<div class="col">
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">Service</h3>
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="service-name" Label="Service Name" @bind-Value="Obj.ServiceName"></EditFormRow>
<ValidationMessage For="() => Obj.ServiceName"/>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="service-url" Label="Service URL" @bind-Value="Obj.ServiceUrl"></EditFormRow>
</div>
</div>
</div>
</div>
</div>
<div class="col">
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">Login credentials</h3>
<div class="mb-4">
<button class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-800" @onclick="GenerateRandomIdentity">Generate Random Identity</button>
<button class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800" @onclick="SaveAlias">Save Alias</button>
@if (IsIdentityLoading)
{
<p>Loading...</p>
}
</div>
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="email" Label="Email" Value="@(Obj.Identity.EmailPrefix)" ValueChanged="@(val => Obj.Identity.EmailPrefix = val)"></EditFormRow>
<div class="col">
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">Login credentials</h3>
<div class="mb-4">
<button class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-800" @onclick="GenerateRandomIdentity">Generate Random Identity</button>
<button type="submit" class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800">Save Alias</button>
@if (IsIdentityLoading)
{
<p>Loading...</p>
}
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="username" Label="Username" Value="@(Obj.Identity.NickName)" ValueChanged="@(val => Obj.Identity.NickName = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<div class="relative">
<EditFormRow Id="password" Label="Password" Value="@(Obj.Password.Value)" ValueChanged="@(val => Obj.Password.Value = val)"></EditFormRow>
<button type="submit" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomPassword">(Re)generate Random Password</button>
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="email" Label="Email" @bind-Value="Obj.Identity.EmailPrefix"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="username" Label="Username" @bind-Value="Obj.Identity.NickName"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<div class="relative">
<EditFormRow Id="password" Label="Password" @bind-Value="Obj.Password.Value"></EditFormRow>
<button type="submit" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomPassword">(Re)generate Random Password</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
<div class="col">
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">Identity</h3>
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="first-name" Label="First Name" Value="@(Obj.Identity.FirstName)" ValueChanged="@(val => Obj.Identity.FirstName = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="last-name" Label="Last Name" Value="@(Obj.Identity.LastName)" ValueChanged="@(val => Obj.Identity.LastName = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="gender" Label="Gender" Value="@(Obj.Identity.Gender)" ValueChanged="@(val => Obj.Identity.Gender = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="nickname" Label="Nick Name" Value="@(Obj.Identity.NickName)" ValueChanged="@(val => Obj.Identity.NickName = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="birthdate" Label="Birth Date" Value="@(Obj.Identity.BirthDate)" ValueChanged="@(val => Obj.Identity.BirthDate = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="street" Label="Address Street" Value="@(Obj.Identity.AddressStreet)" ValueChanged="@(val => Obj.Identity.AddressStreet = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="city" Label="Address City" Value="@(Obj.Identity.AddressCity)" ValueChanged="@(val => Obj.Identity.AddressCity = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="state" Label="Address State" Value="@(Obj.Identity.AddressState)" ValueChanged="@(val => Obj.Identity.AddressState = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="zipcode" Label="Address Zip Code" Value="@(Obj.Identity.AddressZipCode)" ValueChanged="@(val => Obj.Identity.AddressZipCode = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="country" Label="Address Country" Value="@(Obj.Identity.AddressCountry)" ValueChanged="@(val => Obj.Identity.AddressCountry = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="hobbies" Label="Hobbies" Value="@(Obj.Identity.Hobbies)" ValueChanged="@(val => Obj.Identity.Hobbies = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="phone-mobile" Label="Phone Mobile" Value="@(Obj.Identity.PhoneMobile)" ValueChanged="@(val => Obj.Identity.PhoneMobile = val)"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="iban" Label="Bank Account IBAN" Value="@(Obj.Identity.BankAccountIBAN)" ValueChanged="@(val => Obj.Identity.BankAccountIBAN = val)"></EditFormRow>
<div class="grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
<div class="col">
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">Identity</h3>
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="first-name" Label="First Name" @bind-Value="Obj.Identity.FirstName"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="last-name" Label="Last Name" @bind-Value="Obj.Identity.LastName"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="gender" Label="Gender" @bind-Value="Obj.Identity.Gender"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="nickname" Label="Nick Name" @bind-Value="Obj.Identity.NickName"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="birthdate" Label="Birth Date" @bind-Value="Obj.Identity.BirthDate"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="street" Label="Address Street" @bind-Value="Obj.Identity.AddressStreet"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="city" Label="Address City" @bind-Value="Obj.Identity.AddressCity"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="state" Label="Address State" @bind-Value="Obj.Identity.AddressState"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="zipcode" Label="Address Zip Code" @bind-Value="Obj.Identity.AddressZipCode"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="country" Label="Address Country" @bind-Value="Obj.Identity.AddressCountry"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="hobbies" Label="Hobbies" @bind-Value="Obj.Identity.Hobbies"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="phone-mobile" Label="Phone Mobile" @bind-Value="Obj.Identity.PhoneMobile"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="iban" Label="Bank Account IBAN" @bind-Value="Obj.Identity.BankAccountIBAN"></EditFormRow>
</div>
</div>
</div>
</div>
<button type="submit" class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800">Save Alias</button>
</div>
</div>
<button class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800" @onclick="SaveAlias">Save Alias</button>
</div>
</EditForm>
@if (IsSaving)
{
<p>Saving...</p>
}
@if (IsSaving)
{
<p>Saving...</p>
}
}
@@ -152,7 +156,7 @@ else
private bool EditMode { get; set; }
private bool Loading { get; set; } = true;
private Alias Obj { get; set; } = new();
private AliasEdit Obj { get; set; } = new();
private bool IsIdentityLoading { get; set; }
private bool IsSaving { get; set; }
@@ -211,15 +215,17 @@ else
return;
}
Obj = alias;
Obj = AliasToAliasEdit(alias);
}
else
{
// Create new Obj
Obj = new Alias();
Obj.Identity = new Shared.Models.WebApi.Identity();
Obj.Service = new Shared.Models.WebApi.Service();
Obj.Password = new Shared.Models.WebApi.Password();
var alias = new Alias();
alias.Identity = new Shared.Models.WebApi.Identity();
alias.Service = new Shared.Models.WebApi.Service();
alias.Password = new Shared.Models.WebApi.Password();
Obj = AliasToAliasEdit(alias);
}
// Hide loading spinner
@@ -297,12 +303,12 @@ else
{
if (Id is not null)
{
Id = await AliasService.UpdateAliasAsync(Obj, Id.Value);
Id = await AliasService.UpdateAliasAsync(AliasEditToAlias(Obj), Id.Value);
}
}
else
{
Id = await AliasService.InsertAliasAsync(Obj);
Id = await AliasService.InsertAliasAsync(AliasEditToAlias(Obj));
}
IsSaving = false;
@@ -327,4 +333,33 @@ else
Navigation.NavigateTo("/alias/" + Id);
}
private AliasEdit AliasToAliasEdit(Alias alias)
{
return new AliasEdit
{
ServiceName = alias.Service.Name,
ServiceUrl = alias.Service.Url,
Password = alias.Password,
Identity = alias.Identity,
CreateDate = alias.CreateDate,
LastUpdate = alias.LastUpdate
};
}
private Alias AliasEditToAlias(AliasEdit alias)
{
return new Alias
{
Service = new Service
{
Name = alias.ServiceName,
Url = alias.ServiceUrl
},
Password = alias.Password,
Identity = alias.Identity,
CreateDate = alias.CreateDate,
LastUpdate = alias.LastUpdate
};
}
}

View File

@@ -36,6 +36,7 @@
private bool IsLoading { get; set; } = true;
private List<Shared.Models.WebApi.AliasListEntry> Aliases { get; set; } = new();
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)