Implement password recovery feature with reset token and email notifications
All checks were successful
Build And Push Dev Docker Image / docker (push) Successful in 1m28s
All checks were successful
Build And Push Dev Docker Image / docker (push) Successful in 1m28s
This commit is contained in:
@@ -95,6 +95,16 @@ namespace FoodsharingSiegen.Contracts.Entity
|
||||
set => EncryptedPassword = Cryptor.Encrypt(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reset token for password recovery (ab)
|
||||
/// </summary>
|
||||
public string? ResetToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the expiry date for the reset token (ab)
|
||||
/// </summary>
|
||||
public DateTime? ResetTokenExpiry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the type (ab)
|
||||
/// </summary>
|
||||
|
||||
@@ -50,6 +50,11 @@ namespace FoodsharingSiegen.Server.Auth
|
||||
/// </summary>
|
||||
private User? _user;
|
||||
|
||||
/// <summary>
|
||||
/// The mail service
|
||||
/// </summary>
|
||||
private readonly IMailService _mailService;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Setup/Teardown
|
||||
@@ -60,14 +65,17 @@ namespace FoodsharingSiegen.Server.Auth
|
||||
/// <param name="context">The context</param>
|
||||
/// <param name="localStorageService">The local storage service</param>
|
||||
/// <param name="authenticationStateProvider">The authentication state provider</param>
|
||||
/// <param name="mailService">The mail service</param>
|
||||
public AuthService(
|
||||
FsContext context,
|
||||
LocalStorageService localStorageService,
|
||||
AuthenticationStateProvider authenticationStateProvider)
|
||||
AuthenticationStateProvider authenticationStateProvider,
|
||||
IMailService mailService)
|
||||
{
|
||||
Context = context;
|
||||
_localStorageService = localStorageService;
|
||||
_authenticationStateProvider = authenticationStateProvider;
|
||||
_mailService = mailService;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -186,5 +194,52 @@ namespace FoodsharingSiegen.Server.Auth
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Password Recovery
|
||||
|
||||
public async Task InitiatePasswordReset(string email, string baseUri)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(email)) return;
|
||||
|
||||
var user = await Context.Users!.FirstOrDefaultAsync(x => x.Mail.ToLower() == email.ToLower());
|
||||
if (user == null) return; // Do not leak existence
|
||||
|
||||
var resetToken = Guid.NewGuid().ToString("N");
|
||||
user.ResetToken = resetToken;
|
||||
user.ResetTokenExpiry = DateTime.UtcNow.AddMinutes(30);
|
||||
|
||||
await Context.SaveChangesAsync();
|
||||
|
||||
var resetLink = $"{baseUri.TrimEnd('/')}/reset-password/{resetToken}";
|
||||
var mailBody = $"Hallo {user.Name},<br><br>Um dein Passwort zurückzusetzen, klicke bitte auf den folgenden Link (dieser ist 30 Minuten gültig):<br><a href='{resetLink}'>{resetLink}</a><br><br>Viele Grüße<br>Dein Foodsharing Team";
|
||||
|
||||
await _mailService.SendEmailAsync(user.Mail, "Passwort zurücksetzen", mailBody);
|
||||
}
|
||||
|
||||
public async Task<OperationResult> ResetPassword(string token, string newPassword)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token)) return new OperationResult(new Exception("Ungültiges Token."));
|
||||
if (string.IsNullOrWhiteSpace(newPassword)) return new OperationResult(new Exception("Passwort darf nicht leer sein."));
|
||||
|
||||
var user = await Context.Users!.FirstOrDefaultAsync(x => x.ResetToken == token && x.ResetTokenExpiry > DateTime.UtcNow);
|
||||
if (user == null) return new OperationResult(new Exception("Token ist ungültig oder abgelaufen."));
|
||||
|
||||
user.Password = newPassword;
|
||||
user.ResetToken = null;
|
||||
user.ResetTokenExpiry = null;
|
||||
|
||||
await Context.SaveChangesAsync();
|
||||
|
||||
return new OperationResult();
|
||||
}
|
||||
|
||||
public async Task<bool> IsResetTokenValid(string token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token)) return false;
|
||||
var user = await Context.Users!.FirstOrDefaultAsync(x => x.ResetToken == token && x.ResetTokenExpiry > DateTime.UtcNow);
|
||||
return user != null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
223
FoodsharingSiegen.Server/Migrations/20260418111848_PasswordRecovery.Designer.cs
generated
Normal file
223
FoodsharingSiegen.Server/Migrations/20260418111848_PasswordRecovery.Designer.cs
generated
Normal file
@@ -0,0 +1,223 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FoodsharingSiegen.Server.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FoodsharingSiegen.Server.Migrations
|
||||
{
|
||||
[DbContext(typeof(FsContext))]
|
||||
[Migration("20260418111848_PasswordRecovery")]
|
||||
partial class PasswordRecovery
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
|
||||
|
||||
modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Audit", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data1")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data2")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("UserID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserID");
|
||||
|
||||
b.ToTable("Audits");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Interaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Alert")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Feedback")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("FeedbackInfo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Info1")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Info2")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("NotNeeded")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ProspectID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("UserID")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProspectID");
|
||||
|
||||
b.HasIndex("UserID");
|
||||
|
||||
b.ToTable("Interactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Prospect", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("FsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Memo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Modified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RecordState")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Warning")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Prospects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedPassword")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("ForceLogout")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Groups")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Mail")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Memo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Network")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ResetToken")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ResetTokenExpiry")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Verified")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Audit", b =>
|
||||
{
|
||||
b.HasOne("FoodsharingSiegen.Contracts.Entity.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserID");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Interaction", b =>
|
||||
{
|
||||
b.HasOne("FoodsharingSiegen.Contracts.Entity.Prospect", "Prospect")
|
||||
.WithMany("Interactions")
|
||||
.HasForeignKey("ProspectID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("FoodsharingSiegen.Contracts.Entity.User", "User")
|
||||
.WithMany("Interactions")
|
||||
.HasForeignKey("UserID")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Prospect");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.Prospect", b =>
|
||||
{
|
||||
b.Navigation("Interactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FoodsharingSiegen.Contracts.Entity.User", b =>
|
||||
{
|
||||
b.Navigation("Interactions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FoodsharingSiegen.Server.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PasswordRecovery : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ResetToken",
|
||||
table: "Users",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ResetTokenExpiry",
|
||||
table: "Users",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ResetToken",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ResetTokenExpiry",
|
||||
table: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,6 +160,12 @@ namespace FoodsharingSiegen.Server.Migrations
|
||||
b.Property<int>("Network")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ResetToken")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ResetTokenExpiry")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
54
FoodsharingSiegen.Server/Pages/ForgotPassword.razor
Normal file
54
FoodsharingSiegen.Server/Pages/ForgotPassword.razor
Normal file
@@ -0,0 +1,54 @@
|
||||
@page "/forgot-password"
|
||||
@using FoodsharingSiegen.Shared.Helper
|
||||
@layout LoginLayout
|
||||
|
||||
@inherits FoodsharingSiegen.Server.BaseClasses.FsBase
|
||||
|
||||
<PageTitle>@AppSettings.Terms.Title - Passwort vergessen</PageTitle>
|
||||
|
||||
<div class="d-flex justify-content-center align-items-center" style="min-height: 100vh; background-color: #f4f6f8;">
|
||||
<div class="card shadow border-0" style="width: 100%; max-width: 420px; border-radius: 12px; margin: 1rem;">
|
||||
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<div class="text-center mb-4">
|
||||
<i class="fa-solid fa-leaf mb-3" style="font-size: 3rem; color: #64ae24;"></i>
|
||||
<h4 class="font-weight-bold" style="color: #533a20;"><small style="font-size: .6em;">Einarbeitungen</small> @AppSettings.Terms.Title</h4>
|
||||
<p class="text-muted">Passwort zurücksetzen</p>
|
||||
</div>
|
||||
|
||||
@if (IsSubmitted)
|
||||
{
|
||||
<div class="alert alert-success text-center">
|
||||
Wenn ein Benutzerkonto mit dieser E-Mail-Adresse existiert, wurde eine E-Mail mit weiteren Anweisungen versendet.
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<a href="/login" class="btn btn-outline-primary"><i class="fas fa-arrow-left mr-2"></i> Zurück zum Login</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Validation Validator="ValidationHelper.ValidateMail" @bind-Status="@IsValidMail">
|
||||
<Field>
|
||||
<FieldLabel>E-Mail Adresse</FieldLabel>
|
||||
<TextEdit @bind-Text="MailAddress" Role="TextRole.Email" Placeholder="E-Mail" KeyUp="TextEdit_KeyUp" Size="Size.Large"></TextEdit>
|
||||
</Field>
|
||||
</Validation>
|
||||
|
||||
<Button Class="mt-4 w-100" Color="Color.Primary" Size="Size.Large" Clicked="SubmitRequest" Disabled="@(IsValidMail != ValidationStatus.Success || IsLoading)">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<i class="fas fa-spinner fa-spin mr-2"></i> <span>Wird gesendet...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fas fa-paper-plane mr-2"></i> <span>Passwort zurücksetzen</span>
|
||||
}
|
||||
</Button>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="/login" style="font-size: 0.85rem; color: #64ae24; text-decoration: none;"><i class="fas fa-arrow-left mr-1"></i> Zurück zum Login</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
41
FoodsharingSiegen.Server/Pages/ForgotPassword.razor.cs
Normal file
41
FoodsharingSiegen.Server/Pages/ForgotPassword.razor.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Blazorise;
|
||||
using FoodsharingSiegen.Server.BaseClasses;
|
||||
using FoodsharingSiegen.Server.Auth;
|
||||
using FoodsharingSiegen.Shared.Helper;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FoodsharingSiegen.Server.Pages
|
||||
{
|
||||
public partial class ForgotPassword : FsBase
|
||||
{
|
||||
public string MailAddress { get; set; } = string.Empty;
|
||||
public ValidationStatus IsValidMail { get; set; } = ValidationStatus.None;
|
||||
|
||||
public bool IsSubmitted { get; set; }
|
||||
public bool IsLoading { get; set; }
|
||||
|
||||
public async Task SubmitRequest()
|
||||
{
|
||||
if (IsValidMail != ValidationStatus.Success) return;
|
||||
|
||||
IsLoading = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await AuthService.InitiatePasswordReset(MailAddress, NavigationManager.BaseUri);
|
||||
|
||||
IsSubmitted = true;
|
||||
IsLoading = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public async Task TextEdit_KeyUp(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter" && IsValidMail == ValidationStatus.Success)
|
||||
{
|
||||
await SubmitRequest();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,9 @@
|
||||
<Button Class="mt-4 w-100" Color="Color.Primary" Size="Size.Large" Clicked="PerformLogin" Disabled="@(IsValidMail != ValidationStatus.Success || IsValidPassword != ValidationStatus.Success)">
|
||||
<i class="fas fa-sign-in-alt mr-2"></i> Einloggen
|
||||
</Button>
|
||||
<div class="d-flex justify-content-center align-items-center mt-2">
|
||||
<a href="/forgot-password" tabindex="-1" style="font-size: 0.85rem; color: #64ae24; text-decoration: none;">Passwort vergessen?</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
82
FoodsharingSiegen.Server/Pages/ResetPassword.razor
Normal file
82
FoodsharingSiegen.Server/Pages/ResetPassword.razor
Normal file
@@ -0,0 +1,82 @@
|
||||
@page "/reset-password/{Token}"
|
||||
@using FoodsharingSiegen.Shared.Helper
|
||||
@layout LoginLayout
|
||||
|
||||
@inherits FoodsharingSiegen.Server.BaseClasses.FsBase
|
||||
|
||||
<PageTitle>@AppSettings.Terms.Title - Neues Passwort setzen</PageTitle>
|
||||
|
||||
<div class="d-flex justify-content-center align-items-center" style="min-height: 100vh; background-color: #f4f6f8;">
|
||||
<div class="card shadow border-0" style="width: 100%; max-width: 420px; border-radius: 12px; margin: 1rem;">
|
||||
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<div class="text-center mb-4">
|
||||
<i class="fa-solid fa-leaf mb-3" style="font-size: 3rem; color: #64ae24;"></i>
|
||||
<h4 class="font-weight-bold" style="color: #533a20;"><small style="font-size: .6em;">Einarbeitungen</small> @AppSettings.Terms.Title</h4>
|
||||
<p class="text-muted">Neues Passwort festlegen</p>
|
||||
</div>
|
||||
|
||||
@if (IsInitializing)
|
||||
{
|
||||
<div class="text-center mt-4">
|
||||
<i class="fas fa-spinner fa-spin fa-2x mb-3" style="color: #64ae24;"></i>
|
||||
<p>Token wird überprüft...</p>
|
||||
</div>
|
||||
}
|
||||
else if (!IsTokenValid)
|
||||
{
|
||||
<div class="alert alert-danger text-center">
|
||||
<i class="fas fa-exclamation-triangle fa-2x mb-2 d-block"></i>
|
||||
Der Link zum Zurücksetzen des Passworts ist ungültig oder abgelaufen. Bitte fordere einen neuen an.
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<a href="/forgot-password" class="btn btn-outline-primary w-100 mb-2"><i class="fas fa-redo mr-2"></i> Neuen Link anfordern</a>
|
||||
<a href="/login" class="btn btn-link w-100" style="color: #64ae24;">Zurück zum Login</a>
|
||||
</div>
|
||||
}
|
||||
else if (IsSuccess)
|
||||
{
|
||||
<div class="alert alert-success text-center">
|
||||
Passwort erfolgreich aktualisiert. Du kannst dich jetzt anmelden.
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<a href="/login" class="btn btn-outline-primary"><i class="fas fa-sign-in-alt mr-2"></i> Zum Login</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Validation Validator="ValidationHelper.ValidatePassword" @bind-Status="@IsValidPassword">
|
||||
<Field>
|
||||
<FieldLabel>Neues Passwort</FieldLabel>
|
||||
<TextEdit @bind-Text="NewPassword" Role="TextRole.Password" Placeholder="Passwort (min. 8 Zeichen, min. 1 Zahl)" KeyUp="TextEdit_KeyUp" Size="Size.Large"></TextEdit>
|
||||
</Field>
|
||||
</Validation>
|
||||
|
||||
<Validation Validator="ValidatePasswordConfirmation" @bind-Status="@IsValidPasswordConfirmation">
|
||||
<Field Class="mt-3">
|
||||
<FieldLabel>Passwort bestätigen</FieldLabel>
|
||||
<TextEdit @bind-Text="ConfirmPassword" Role="TextRole.Password" Placeholder="Passwort bestätigen" KeyUp="TextEdit_KeyUp" Size="Size.Large"></TextEdit>
|
||||
</Field>
|
||||
</Validation>
|
||||
|
||||
@if (!string.IsNullOrEmpty(ErrorMessage))
|
||||
{
|
||||
<div class="text-danger mt-3 text-center">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i> @ErrorMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<Button Class="mt-4 w-100" Color="Color.Primary" Size="Size.Large" Clicked="SubmitReset" Disabled="@(IsValidPassword != ValidationStatus.Success || IsValidPasswordConfirmation != ValidationStatus.Success || IsLoading)">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<i class="fas fa-spinner fa-spin mr-2"></i> <span>Speichern...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fas fa-save mr-2"></i> <span>Passwort speichern</span>
|
||||
}
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
77
FoodsharingSiegen.Server/Pages/ResetPassword.razor.cs
Normal file
77
FoodsharingSiegen.Server/Pages/ResetPassword.razor.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Blazorise;
|
||||
using FoodsharingSiegen.Server.BaseClasses;
|
||||
using FoodsharingSiegen.Server.Auth;
|
||||
using FoodsharingSiegen.Shared.Helper;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FoodsharingSiegen.Server.Pages
|
||||
{
|
||||
public partial class ResetPassword : FsBase
|
||||
{
|
||||
[Parameter]
|
||||
public string Token { get; set; } = string.Empty;
|
||||
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
public string ConfirmPassword { get; set; } = string.Empty;
|
||||
|
||||
public ValidationStatus IsValidPassword { get; set; } = ValidationStatus.None;
|
||||
public ValidationStatus IsValidPasswordConfirmation { get; set; } = ValidationStatus.None;
|
||||
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
public bool IsSuccess { get; set; }
|
||||
public bool IsLoading { get; set; }
|
||||
public bool IsInitializing { get; set; } = true;
|
||||
public bool IsTokenValid { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsTokenValid = await AuthService.IsResetTokenValid(Token);
|
||||
IsInitializing = false;
|
||||
}
|
||||
|
||||
public void ValidatePasswordConfirmation(ValidatorEventArgs args)
|
||||
{
|
||||
var confirmPassword = System.Convert.ToString(args.Value);
|
||||
if (string.IsNullOrWhiteSpace(confirmPassword))
|
||||
{
|
||||
args.Status = ValidationStatus.None;
|
||||
return;
|
||||
}
|
||||
|
||||
args.Status = confirmPassword == NewPassword ? ValidationStatus.Success : ValidationStatus.Error;
|
||||
}
|
||||
|
||||
public async Task SubmitReset()
|
||||
{
|
||||
if (IsValidPassword != ValidationStatus.Success || IsValidPasswordConfirmation != ValidationStatus.Success) return;
|
||||
|
||||
IsLoading = true;
|
||||
ErrorMessage = string.Empty;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
var result = await AuthService.ResetPassword(Token, NewPassword);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
IsSuccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessage = result.Exception?.Message ?? "Ein Fehler ist aufgetreten.";
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public async Task TextEdit_KeyUp(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter" && IsValidPassword == ValidationStatus.Success && IsValidPasswordConfirmation == ValidationStatus.Success)
|
||||
{
|
||||
await SubmitReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ namespace FoodsharingSiegen.Shared.Helper
|
||||
return;
|
||||
}
|
||||
|
||||
var isValid = password.Length > 3;
|
||||
|
||||
var isValid = password.Length >= 8 && password.Any(char.IsDigit);
|
||||
|
||||
args.Status = isValid ? ValidationStatus.Success : ValidationStatus.Error;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user