Improved Manage Email Page

This commit is contained in:
Mia Rose Winter 2024-01-20 19:08:01 +01:00
parent bb7e888ffa
commit 91993275ee
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
5 changed files with 426 additions and 69 deletions

View file

@ -11,113 +11,113 @@
@inject IEmailSender<ApplicationUser> EmailSender @inject IEmailSender<ApplicationUser> EmailSender
@inject IdentityUserAccessor UserAccessor @inject IdentityUserAccessor UserAccessor
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IStringLocalizer<Email> Localizer
<PageTitle>Manage email</PageTitle> <PageTitle>@Localizer["Title"]</PageTitle>
<h3>Manage email</h3> <StatusMessage Message="@Message"/>
<BoardComponent>
<StatusMessage Message="@message"/> <BoardCardComponent Heading="@Localizer["Title"]">
<div class="row">
<div class="col-md-6">
<form @onsubmit="OnSendEmailVerificationAsync" @formname="send-verification" id="send-verification-form" method="post"> <form @onsubmit="OnSendEmailVerificationAsync" @formname="send-verification" id="send-verification-form" method="post">
<AntiforgeryToken /> <AntiforgeryToken />
</form> </form>
<EditForm Model="Input" FormName="change-email" OnValidSubmit="OnValidSubmitAsync" method="post">
<EditForm Model="Input" FormName="change-email" OnValidSubmit="OnValidSubmitAsync" method="post" class="w-full">
<DataAnnotationsValidator /> <DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert" />
@if (isEmailConfirmed) <InputLabelComponent LabelText="@Localizer["Email_Label"]">
{ @if (IsEmailConfirmed) {
<div class="form-floating mb-3 input-group"> <div class="join">
<input type="text" value="@email" class="form-control" placeholder="Please enter your email." disabled /> <input type="text" value="@_email" class="input input-bordered flex-1 join-item"
<div class="input-group-append"> placeholder="@Localizer["Email_Placeholder"]" disabled />
<span class="h-100 input-group-text text-success font-weight-bold">✓</span> <span class="btn btn-square btn-disabled join-item">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-success">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</span>
</div> </div>
<label for="email" class="form-label">Email</label> } else {
<div class="join">
<input type="text" value="@_email" class="input input-bordered flex-1 join-item"
placeholder="@Localizer["Email_Placeholder"]" disabled />
<span class="btn btn-square btn-disabled join-item">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-error">
<path stroke-linecap="round" stroke-linejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</span>
</div> </div>
<button type="submit" class="btn btn-link mt-3" form="send-verification-form">@Localizer["EmailVerification_Submit"]</button>
} }
else </InputLabelComponent>
{ <InputLabelComponent LabelText="@Localizer["NewEmail_Label"]" For="() => Input.NewEmail">
<div class="form-floating mb-3"> <InputText @bind-Value="Input.NewEmail" class="input input-bordered w-full" autocomplete="email"
<input type="text" value="@email" class="form-control" placeholder="Please enter your email." disabled /> required aria-required="true" placeholder="@Localizer["NewEmail_Placeholder"]" />
<label for="email" class="form-label">Email</label> </InputLabelComponent>
<button type="submit" class="btn btn-link" form="send-verification-form">Send verification email</button>
</div> <button type="submit" class="btn btn-primary w-full">
} @Localizer["NewEmail_Submit"]
<div class="form-floating mb-3"> </button>
<InputText @bind-Value="Input.NewEmail" class="form-control" autocomplete="email" aria-required="true" placeholder="Please enter new email." />
<label for="new-email" class="form-label">New email</label>
<ValidationMessage For="() => Input.NewEmail" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Change email</button>
</EditForm> </EditForm>
</div> </BoardCardComponent>
</div> </BoardComponent>
@code { @code {
private string? message;
private ApplicationUser user = default!;
private string? email;
private bool isEmailConfirmed;
[CascadingParameter] [CascadingParameter]
private HttpContext HttpContext { get; set; } = default!; private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm(FormName = "change-email")] [SupplyParameterFromForm(FormName = "change-email")]
private InputModel Input { get; set; } = new(); private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync() private string? Message { get; set; }
{ private ApplicationUser User { get; set; } = default!;
user = await UserAccessor.GetRequiredUserAsync(HttpContext); private string? _email;
email = await UserManager.GetEmailAsync(user); public bool IsEmailConfirmed { get; set; }
isEmailConfirmed = await UserManager.IsEmailConfirmedAsync(user);
Input.NewEmail ??= email; protected override async Task OnInitializedAsync() {
User = await UserAccessor.GetRequiredUserAsync(HttpContext);
_email = await UserManager.GetEmailAsync(User);
IsEmailConfirmed = await UserManager.IsEmailConfirmedAsync(User);
Input.NewEmail ??= _email;
} }
private async Task OnValidSubmitAsync() private async Task OnValidSubmitAsync() {
{ if (Input.NewEmail is null || Input.NewEmail == _email) {
if (Input.NewEmail is null || Input.NewEmail == email) Message = Localizer["NewEmail_Unchanged"];
{
message = "Your email is unchanged.";
return; return;
} }
var userId = await UserManager.GetUserIdAsync(user); string userId = await UserManager.GetUserIdAsync(User);
var code = await UserManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail); string code = await UserManager.GenerateChangeEmailTokenAsync(User, Input.NewEmail);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters( string callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmailChange").AbsoluteUri, NavigationManager.ToAbsoluteUri("Account/ConfirmEmailChange").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["email"] = Input.NewEmail, ["code"] = code }); new Dictionary<string, object?> { ["userId"] = userId, ["email"] = Input.NewEmail, ["code"] = code });
await EmailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl)); await EmailSender.SendConfirmationLinkAsync(User, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl));
Message = Localizer["NewEmail_Success"];
message = "Confirmation link to change email sent. Please check your email.";
} }
private async Task OnSendEmailVerificationAsync() private async Task OnSendEmailVerificationAsync() {
{ if (_email is null) {
if (email is null)
{
return; return;
} }
var userId = await UserManager.GetUserIdAsync(user); string userId = await UserManager.GetUserIdAsync(User);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); string code = await UserManager.GenerateEmailConfirmationTokenAsync(User);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters( string callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code }); new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code });
await EmailSender.SendConfirmationLinkAsync(user, email, HtmlEncoder.Default.Encode(callbackUrl)); await EmailSender.SendConfirmationLinkAsync(User, _email, HtmlEncoder.Default.Encode(callbackUrl));
Message = Localizer["EmailVerification_Success"];
message = "Verification email sent. Please check your email.";
} }
private sealed class InputModel private sealed class InputModel {
{
[Required] [Required]
[EmailAddress] [EmailAddress]
[Display(Name = "New email")] [Display(Name = "New Email")]
public string? NewEmail { get; set; } public string? NewEmail { get; set; }
} }
} }

View file

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="EmailVerification_Submit" xml:space="preserve">
<value>Bestätigungsmail senden</value>
</data>
<data name="EmailVerification_Success" xml:space="preserve">
<value>Bestätigungsmail versandt</value>
</data>
<data name="NewEmail_Success" xml:space="preserve">
<value>Erfolgreich Bestätigungsmail für neue Mailadresse versand</value>
</data>
<data name="NewEmail_Unchanged" xml:space="preserve">
<value>Ihre Mailadresse ist unverändert</value>
</data>
<data name="Title" xml:space="preserve">
<value>Email Verwalten</value>
</data>
<data name="Email_Label" xml:space="preserve">
<value>E-Mail</value>
</data>
<data name="NewEmail_Label" xml:space="preserve">
<value>Neue E-Mail</value>
</data>
<data name="NewEmail_Submit" xml:space="preserve">
<value>Bestätigungsmail versenden</value>
</data>
</root>

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View file

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Manage Email</value>
</data>
<data name="Email_Label" xml:space="preserve">
<value>Email</value>
</data>
<data name="EmailVerification_Submit" xml:space="preserve">
<value>Send verification mail</value>
</data>
<data name="Email_Placeholder" xml:space="preserve">
<value>name@example.com</value>
</data>
<data name="NewEmail_Label" xml:space="preserve">
<value>New Email</value>
</data>
<data name="NewEmail_Placeholder" xml:space="preserve">
<value>name@example.com</value>
</data>
<data name="NewEmail_Submit" xml:space="preserve">
<value>Send verification mail</value>
</data>
<data name="NewEmail_Unchanged" xml:space="preserve">
<value>You email has not been changed</value>
</data>
<data name="NewEmail_Success" xml:space="preserve">
<value>Successfully send mail to verify new email</value>
</data>
<data name="EmailVerification_Success" xml:space="preserve">
<value>Verification mail send</value>
</data>
</root>

File diff suppressed because one or more lines are too long