diff --git a/src/web/Jordnaer/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs b/src/web/Jordnaer/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs index d2d65842..8e607d2e 100644 --- a/src/web/Jordnaer/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs +++ b/src/web/Jordnaer/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs @@ -46,7 +46,9 @@ public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEn [FromForm] string returnUrl) => { await signInManager.SignOutAsync(); - return TypedResults.LocalRedirect($"~/{returnUrl}"); + return TypedResults.LocalRedirect(string.IsNullOrEmpty(returnUrl) + ? "~/" + : returnUrl); }); var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization(); diff --git a/src/web/Jordnaer/Components/Account/IdentityRedirectManager.cs b/src/web/Jordnaer/Components/Account/IdentityRedirectManager.cs index 2d044c4f..30427456 100644 --- a/src/web/Jordnaer/Components/Account/IdentityRedirectManager.cs +++ b/src/web/Jordnaer/Components/Account/IdentityRedirectManager.cs @@ -17,7 +17,7 @@ internal sealed class IdentityRedirectManager(NavigationManager navigationManage }; [DoesNotReturn] - public void RedirectTo(string? uri) + public void RedirectTo(string? uri, bool forceLoad = false) { uri ??= ""; @@ -29,7 +29,7 @@ public void RedirectTo(string? uri) // During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect. // So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown. - navigationManager.NavigateTo(uri); + navigationManager.NavigateTo(uri, forceLoad); throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering."); } @@ -60,7 +60,7 @@ public void RedirectToWithStatus(string uri, AlertMessage? message, HttpContext private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path); [DoesNotReturn] - public void RedirectToCurrentPage() => RedirectTo(CurrentPath); + public void RedirectToCurrentPage(bool forceLoad = false) => RedirectTo(CurrentPath, forceLoad); [DoesNotReturn] public void RedirectToCurrentPageWithStatus(string message, HttpContext context) diff --git a/src/web/Jordnaer/Components/Account/Pages/ConfirmEmail.razor b/src/web/Jordnaer/Components/Account/Pages/ConfirmEmail.razor index 3e9529a2..0d790dd8 100644 --- a/src/web/Jordnaer/Components/Account/Pages/ConfirmEmail.razor +++ b/src/web/Jordnaer/Components/Account/Pages/ConfirmEmail.razor @@ -3,6 +3,7 @@ @using System.Text @using Microsoft.AspNetCore.WebUtilities +@inject SignInManager SignInManager @inject UserManager UserManager @inject IdentityRedirectManager RedirectManager @@ -23,6 +24,9 @@ [SupplyParameterFromQuery] private string? Code { get; set; } + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + protected override async Task OnInitializedAsync() { if (UserId is null || Code is null) @@ -34,7 +38,7 @@ if (user is null) { HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; - _statusMessage = new AlertMessage("Fejl ved indlæsning af bruger med ID {UserId}", true); + _statusMessage = new AlertMessage($"Fejl ved indlæsning af bruger med ID {UserId}", true); } else { @@ -42,7 +46,11 @@ var result = await UserManager.ConfirmEmailAsync(user, code); if (result.Succeeded) { - _statusMessage = new AlertMessage("Tak fordi du bekræftede din email."); + _statusMessage = new AlertMessage("Tak fordi du bekræftede din email, du bliver nu logget ind."); + + await SignInManager.SignInAsync(user, isPersistent: true); + + RedirectManager.RedirectTo(ReturnUrl ?? "/profile", forceLoad: true); } else { diff --git a/src/web/Jordnaer/Components/Account/Pages/ExternalLogin.razor b/src/web/Jordnaer/Components/Account/Pages/ExternalLogin.razor index c417b474..cbbcc028 100644 --- a/src/web/Jordnaer/Components/Account/Pages/ExternalLogin.razor +++ b/src/web/Jordnaer/Components/Account/Pages/ExternalLogin.razor @@ -14,7 +14,7 @@ @inject JordnaerDbContext Context @inject IMediator Mediator - +

Registrér

@@ -150,7 +150,7 @@ var result = await UserManager.CreateAsync(user); if (!result.Succeeded) { - _message = new AlertMessage(result.Errors.Select(error => error.Description), true); + _message = new AlertMessage(result.Errors.Select(error => error.Description), true); return; } @@ -175,7 +175,7 @@ // If account confirmation is required, we need to show the link if we don't have a real email sender if (UserManager.Options.SignIn.RequireConfirmedAccount) { - RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email }); + RedirectManager.RedirectTo("Account/RegisterConfirmation", queryParameters: new() { ["email"] = Input.Email }); } await SignInManager.SignInAsync(user, isPersistent: false, _externalLoginInfo.LoginProvider); diff --git a/src/web/Jordnaer/Components/Account/Pages/Login.razor b/src/web/Jordnaer/Components/Account/Pages/Login.razor index 29660fd6..921fe4a8 100644 --- a/src/web/Jordnaer/Components/Account/Pages/Login.razor +++ b/src/web/Jordnaer/Components/Account/Pages/Login.razor @@ -3,43 +3,45 @@ @inject SignInManager SignInManager @inject ILogger Logger @inject IdentityRedirectManager RedirectManager -@inject ILocalStorageService LocalStorage + + +

Log ind

+
+
- + - +
+
- + - -
-
- +
+ Log ind + Glemt din adgangskode? Gensend emailbekræftelse - Opret ny konto +
@@ -70,7 +72,10 @@ public async Task LoginUser() { - var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); + var result = await SignInManager.PasswordSignInAsync(Input.Email, + Input.Password, + isPersistent: true, + lockoutOnFailure: true); if (result.Succeeded) { Logger.LogInformation("User logged in."); @@ -81,7 +86,7 @@ { RedirectManager.RedirectTo( "Account/LoginWith2fa", - new Dictionary { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe }); + new Dictionary { ["returnUrl"] = ReturnUrl, ["rememberMe"] = true }); } else if (result.IsLockedOut) { @@ -104,8 +109,5 @@ [DataType(DataType.Password)] [Display(Name = "Adgangskode")] public string Password { get; set; } = ""; - - [Display(Name = "Husk mig?")] - public bool RememberMe { get; set; } } } diff --git a/src/web/Jordnaer/Components/Account/Pages/Register.razor b/src/web/Jordnaer/Components/Account/Pages/Register.razor index 1a3fd0c7..4cac8cf6 100644 --- a/src/web/Jordnaer/Components/Account/Pages/Register.razor +++ b/src/web/Jordnaer/Components/Account/Pages/Register.razor @@ -31,13 +31,13 @@
- +
- +
@@ -45,7 +45,7 @@
- Login med eksisterende konto + Log ind med eksisterende konto
@@ -104,7 +104,7 @@ { RedirectManager.RedirectTo( "Account/RegisterConfirmation", - new() { ["email"] = Input.Email, ["returnUrl"] = ReturnUrl }); + queryParameters: new() { ["email"] = Input.Email, ["returnUrl"] = ReturnUrl }); } await SignInManager.SignInAsync(user, isPersistent: false); diff --git a/src/web/Jordnaer/Components/Account/Pages/RegisterConfirmation.razor b/src/web/Jordnaer/Components/Account/Pages/RegisterConfirmation.razor index f46292d7..38a91f0c 100644 --- a/src/web/Jordnaer/Components/Account/Pages/RegisterConfirmation.razor +++ b/src/web/Jordnaer/Components/Account/Pages/RegisterConfirmation.razor @@ -1,19 +1,15 @@ @page "/Account/RegisterConfirmation" @inject UserManager UserManager -@inject IEmailSender EmailSender -@inject NavigationManager NavigationManager @inject IdentityRedirectManager RedirectManager -

Registreringsbekræftelse

+

Registreringsbekræftelse

-@*TODO: Opdater denne side, når emailen er bekræftet*@ -

Check venligst din email for at bekræfte din konto.

-@*TODO: Allow resending email after 30 seconds in case it never arrives*@ +

Check venligst din email for at bekræfte din konto. Du kan lukke denne side.

@code { private AlertMessage? _statusMessage; @@ -31,7 +27,7 @@ { if (Email is null) { - RedirectManager.RedirectTo(""); + RedirectManager.RedirectTo(ReturnUrl ?? ""); } var user = await UserManager.FindByEmailAsync(Email); diff --git a/src/web/Jordnaer/Consumers/SendEmailConsumer.cs b/src/web/Jordnaer/Consumers/SendEmailConsumer.cs index 9b7384b2..2d9cb91b 100644 --- a/src/web/Jordnaer/Consumers/SendEmailConsumer.cs +++ b/src/web/Jordnaer/Consumers/SendEmailConsumer.cs @@ -89,7 +89,8 @@ public async Task Consume(ConsumeContext consumeContext) } else { - logger.LogInformation("Email sent to {@Recipient}. Subject: {Subject}", message.To, message.Subject); + logger.LogInformation("Email sent to {@Recipient}. Subject: {Subject}", + message.To?.Select(x => x.Email), message.Subject); } } } \ No newline at end of file diff --git a/src/web/Jordnaer/Features/Authentication/EmailConfirmed.cs b/src/web/Jordnaer/Features/Authentication/EmailConfirmed.cs deleted file mode 100644 index cfd88fcd..00000000 --- a/src/web/Jordnaer/Features/Authentication/EmailConfirmed.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Jordnaer.Features.Authentication; - -public class ConfirmEmailStateContainer -{ - public string? UserId { get; private set; } - - public event Action? OnEmailConfirmed; - - public void EmailConfirmed(string userId) - { - UserId = userId; - NotifyStateChanged(); - } - - private void NotifyStateChanged() => OnEmailConfirmed?.Invoke(); -} \ No newline at end of file diff --git a/src/web/Jordnaer/Features/DeleteUser/DeleteUserService.cs b/src/web/Jordnaer/Features/DeleteUser/DeleteUserService.cs index f20ddf5e..59f34395 100644 --- a/src/web/Jordnaer/Features/DeleteUser/DeleteUserService.cs +++ b/src/web/Jordnaer/Features/DeleteUser/DeleteUserService.cs @@ -19,60 +19,43 @@ public interface IDeleteUserService Task VerifyTokenAsync(string userId, string token, CancellationToken cancellationToken = default); } -public class DeleteUserService : IDeleteUserService +public class DeleteUserService( + UserManager userManager, + ILogger logger, + IPublishEndpoint publishEndpoint, + IServer server, + IDbContextFactory contextFactory, + IDiagnosticContext diagnosticContext, + IImageService imageService) + : IDeleteUserService { public const string TokenPurpose = "delete-user"; public static readonly string TokenProvider = TokenOptions.DefaultEmailProvider; - private readonly UserManager _userManager; - private readonly ILogger _logger; - private readonly IPublishEndpoint _publishEndpoint; - private readonly IServer _server; - private readonly IDbContextFactory _contextFactory; - private readonly IDiagnosticContext _diagnosticContext; - private readonly IImageService _imageService; - - public DeleteUserService(UserManager userManager, - ILogger logger, - IPublishEndpoint publishEndpoint, - IServer server, - IDbContextFactory contextFactory, - IDiagnosticContext diagnosticContext, - IImageService imageService) - { - _userManager = userManager; - _logger = logger; - _publishEndpoint = publishEndpoint; - _server = server; - _contextFactory = contextFactory; - _diagnosticContext = diagnosticContext; - _imageService = imageService; - } - public async Task InitiateDeleteUserAsync(string userId, CancellationToken cancellationToken = default) { - _diagnosticContext.Set("UserId", userId); + diagnosticContext.Set("UserId", userId); - await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var user = await context.Users.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); if (user is null) { - _logger.LogError("Cannot initiate deletion of user, the user has no ApplicationUser"); + logger.LogError("Cannot initiate deletion of user, the user has no ApplicationUser"); return false; } - var serverAddressFeature = _server.Features.Get(); + var serverAddressFeature = server.Features.Get(); var serverAddress = serverAddressFeature?.Addresses.FirstOrDefault(); if (serverAddress is null) { - _logger.LogError("No addresses found in the IServerAddressFeature. A Delete User Url cannot be created."); + logger.LogError("No addresses found in the IServerAddressFeature. A Delete User Url cannot be created."); return false; } var to = new EmailAddress(user.Email); - var token = await _userManager.GenerateUserTokenAsync(user, TokenProvider, TokenPurpose); + var token = await userManager.GenerateUserTokenAsync(user, TokenProvider, TokenPurpose); var deletionLink = $"{serverAddress}/delete-user/{token}"; @@ -86,102 +69,105 @@ public async Task InitiateDeleteUserAsync(string userId, CancellationToken To = [to] }; - await _publishEndpoint.Publish(email, cancellationToken); + await publishEndpoint.Publish(email, cancellationToken); return true; } public async Task DeleteUserAsync(string userId, string token, CancellationToken cancellationToken = default) { - _diagnosticContext.Set("UserId", userId); + diagnosticContext.Set("UserId", userId); - await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var user = await context.Users.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); if (user is null) { - _logger.LogError("Cannot delete user, the user has no ApplicationUser"); + logger.LogError("Cannot delete user, the user has no ApplicationUser"); return false; } - var tokenIsValid = await _userManager.VerifyUserTokenAsync(user, TokenProvider, TokenPurpose, token); + var tokenIsValid = await userManager.VerifyUserTokenAsync(user, TokenProvider, TokenPurpose, token); if (tokenIsValid is false) { - _logger.LogWarning("The token provided by User {UserId} is not valid for " + + logger.LogWarning("The token provided by User {UserId} is not valid for " + "the token purpose {tokenPurpose}, " + "stopping the deletion of the user.", userId, TokenPurpose); return false; } - await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken); - try + var executionStrategy = context.Database.CreateExecutionStrategy(); + return await executionStrategy.ExecuteAsync(async () => { - // TODO: Make sure all user data is deleted here, and that owned groups are assigned new admins - var identityResult = await _userManager.DeleteAsync(user); - if (identityResult.Succeeded is false) + await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken); + try { - _logger.LogError("Failed to delete user. Errors: {@identityResultErrors}", - identityResult.Errors.Select(error => $"ErrorCode {error.Code}: {error.Description}")); + // TODO: Make sure all user data is deleted here, and that owned groups are assigned new admins + var identityResult = await userManager.DeleteAsync(user); + if (identityResult.Succeeded is false) + { + logger.LogError("Failed to delete user. Errors: {@identityResultErrors}", + identityResult.Errors.Select(error => $"ErrorCode {error.Code}: {error.Description}")); - await transaction.RollbackAsync(cancellationToken); - return false; - } + await transaction.RollbackAsync(cancellationToken); + return false; + } - var modifiedRows = await context.UserProfiles - .Where(userProfile => userProfile.Id == userId) - .ExecuteDeleteAsync(cancellationToken); + var modifiedRows = await context.UserProfiles + .Where(userProfile => userProfile.Id == userId) + .ExecuteDeleteAsync(cancellationToken); - if (modifiedRows <= 0) - { - _logger.LogError("Failed to delete the user profile."); + if (modifiedRows <= 0) + { + logger.LogError("Failed to delete the user profile."); - await transaction.RollbackAsync(cancellationToken); - return false; - } + await transaction.RollbackAsync(cancellationToken); + return false; + } - await context.SaveChangesAsync(cancellationToken); - await transaction.CommitAsync(cancellationToken); + await context.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); - // Delete all saved images owned by the user - await _imageService.DeleteImageAsync(userId, ProfileImageService.UserProfilePicturesContainerName, cancellationToken); + // Delete all saved images owned by the user + await imageService.DeleteImageAsync(userId, ProfileImageService.UserProfilePicturesContainerName, cancellationToken); - var childrenIds = await context.ChildProfiles - .Where(child => child.UserProfileId == userId) - .Select(child => child.Id) - .ToListAsync(cancellationToken); - foreach (var id in childrenIds) - { - await _imageService.DeleteImageAsync(id.ToString(), ProfileImageService.ChildProfilePicturesContainerName, cancellationToken); - } + var childrenIds = await context.ChildProfiles + .Where(child => child.UserProfileId == userId) + .Select(child => child.Id) + .ToListAsync(cancellationToken); + foreach (var id in childrenIds) + { + await imageService.DeleteImageAsync(id.ToString(), ProfileImageService.ChildProfilePicturesContainerName, cancellationToken); + } - _logger.LogInformation("User {UserId} has been deleted.", userId); + logger.LogInformation("User {UserId} has been deleted.", userId); - await _publishEndpoint.Publish(new UserDeleted(userId), cancellationToken); + await publishEndpoint.Publish(new UserDeleted(userId), cancellationToken); - return true; - } - catch (Exception exception) - { - await transaction.RollbackAsync(cancellationToken); - _logger.LogException(exception); - return false; - } + return true; + } + catch (Exception exception) + { + logger.LogException(exception); + return false; + } + }); } public async Task VerifyTokenAsync(string userId, string token, CancellationToken cancellationToken = default) { - await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var user = await context.Users.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); if (user is not null) { - var tokenIsValid = await _userManager.VerifyUserTokenAsync(user, TokenProvider, TokenPurpose, token); + var tokenIsValid = await userManager.VerifyUserTokenAsync(user, TokenProvider, TokenPurpose, token); return tokenIsValid; } - _logger.LogError("Cannot verify user token, the user has no ApplicationUser"); + logger.LogError("Cannot verify user token, the user has no ApplicationUser"); return false; } diff --git a/src/web/Jordnaer/Pages/Users/DeleteUser.razor b/src/web/Jordnaer/Pages/Users/DeleteUser.razor index 980c89ea..b2daa103 100644 --- a/src/web/Jordnaer/Pages/Users/DeleteUser.razor +++ b/src/web/Jordnaer/Pages/Users/DeleteUser.razor @@ -89,7 +89,7 @@ if (userDeleted) { _userDeletionState = UserDeletionState.Completed; - Navigation.NavigateTo("user-deleted", true); + Navigation.NavigateTo("/Account/Logout?returnUrl=/user-deleted", true); } else { diff --git a/src/web/Jordnaer/Pages/Users/UserDeleted.razor b/src/web/Jordnaer/Pages/Users/UserDeleted.razor index be76721a..5e35a7a0 100644 --- a/src/web/Jordnaer/Pages/Users/UserDeleted.razor +++ b/src/web/Jordnaer/Pages/Users/UserDeleted.razor @@ -12,18 +12,4 @@ Vi takker for denne gang, og håber at du fik nogle gode oplevelser ud af Mini Møder.
- - -
- - - - - - \ No newline at end of file + \ No newline at end of file