-
Notifications
You must be signed in to change notification settings - Fork 3
DSI-8877/8886: Cancel pending change email #242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e45f92a
22a64f7
6c74b7a
181628d
3e144a6
7a6fa43
fc8576d
4af6642
55d5f49
acd6c3d
071f410
e1a0a21
4ee48e1
efd0c98
5b340b6
0a9769e
5ed16c3
6fc8e5e
6089316
83bd4e1
aeeb790
4c10eae
459cb76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| namespace Dfe.SignIn.Core.Contracts.Features.Users.Shared; | ||
|
|
||
| /// <summary> | ||
| /// Represents the type of user code used for various user actions, such as changing email or resetting password. | ||
| /// </summary> | ||
| public enum UserCodeType | ||
| { | ||
| /// <summary> | ||
| /// Represents a user code type used for changing a user's email address. | ||
| /// </summary> | ||
| ChangeEmail, | ||
|
|
||
| /// <summary> | ||
| /// Represents a user code type used for password reset actions. | ||
| /// </summary> | ||
| PasswordReset, | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Provides extension methods for the <see cref="UserCodeType"/> enum to convert it to database values. | ||
| /// </summary> | ||
| public static class UserCodeTypeExtensions | ||
| { | ||
| /// <summary> | ||
| /// Converts the <see cref="UserCodeType"/> enum value to its corresponding database string representation. | ||
| /// </summary> | ||
| /// <param name="codeType">The <see cref="UserCodeType"/> enum value to convert.</param> | ||
| /// <returns>The database string representation of the <see cref="UserCodeType"/> enum value.</returns> | ||
| /// <exception cref="ArgumentOutOfRangeException">Thrown when the <see cref="UserCodeType"/> value is not recognized.</exception> | ||
| public static string ToDbValue(this UserCodeType codeType) | ||
| { | ||
| return codeType switch { | ||
| UserCodeType.ChangeEmail => "changeemail", | ||
| UserCodeType.PasswordReset => "PasswordReset", | ||
| _ => throw new ArgumentOutOfRangeException(nameof(codeType), codeType, null) | ||
| }; | ||
| } | ||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,5 +24,3 @@ public partial class UserCodeEntity | |
|
|
||
| public string? ContextData { get; set; } | ||
| } | ||
| #pragma warning restore CS1591 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| namespace Dfe.SignIn.InternalApi.Endpoints; | ||
|
|
||
| /// <summary> | ||
| /// A compile-time safe registry for class-based Minimal API endpoints. | ||
| /// Handles both Dependency Injection registration and HTTP route mapping | ||
| /// using strongly-typed generics (no reflection). | ||
| /// </summary> | ||
| public sealed class EndpointRegistry | ||
| { | ||
| private readonly List<(Type Type, ServiceLifetime Lifetime)> endpoints = []; | ||
| private readonly List<Action<IEndpointRouteBuilder>> mappers = []; | ||
|
|
||
| /// <summary> | ||
| /// Registers an endpoint class for DI and captures its static Map delegate. | ||
| /// </summary> | ||
| /// <typeparam name="T">The endpoint class implementing <see cref="IEndpoint"/>.</typeparam> | ||
| /// <param name="lifetime">The DI service lifetime. Defaults to Transient.</param> | ||
| public EndpointRegistry Add<T>(ServiceLifetime lifetime = ServiceLifetime.Transient) | ||
| where T : class, IEndpoint | ||
| { | ||
| this.endpoints.Add((typeof(T), lifetime)); | ||
| this.mappers.Add(T.Map); | ||
| return this; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Registers all added endpoint classes with the DI container. | ||
| /// </summary> | ||
| public void RegisterServices(IServiceCollection services) | ||
| { | ||
| foreach (var (type, lifetime) in this.endpoints) { | ||
| services.Add(new ServiceDescriptor(type, type, lifetime)); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Maps all added endpoint routes to the application. | ||
| /// </summary> | ||
| public void MapRoutes(IEndpointRouteBuilder app) | ||
| { | ||
| foreach (var map in this.mappers) { | ||
| map(app); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| using Dfe.SignIn.Core.Contracts.Audit; | ||
| using Dfe.SignIn.Core.Contracts.Features.Users; | ||
| using Dfe.SignIn.Core.Contracts.Features.Users.Shared; | ||
| using Dfe.SignIn.Gateways.EntityFramework; | ||
| using Dfe.SignIn.InternalApi.Endpoints; | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.EntityFrameworkCore; | ||
|
|
||
| namespace Dfe.SignIn.InternalApi.Features.Users.ChangeEmail; | ||
|
|
||
| /// <summary> | ||
| /// An endpoint to cancel the change of a user's email address. | ||
| /// </summary> | ||
| public sealed class CancelChangeEmailAddressEndpoint( | ||
| DbDirectoriesContext directoriesDbContext, | ||
| IAuditWriter auditWriter, | ||
| ILogger<CancelChangeEmailAddressEndpoint> logger, | ||
| IUserLookupService userLookupService) : IEndpoint | ||
| { | ||
| /// <summary> | ||
| /// Maps the endpoint to the specified <see cref="IEndpointRouteBuilder"/>. | ||
| /// </summary> | ||
| /// <param name="app">The endpoint route builder to map the endpoint to.</param> | ||
| public static void Map(IEndpointRouteBuilder app) | ||
| { | ||
| app.MapDelete(UsersApiRoutes.CancelChangeEmail, async ( | ||
| [FromRoute] Guid userId, | ||
| [FromServices] CancelChangeEmailAddressEndpoint endpoint, | ||
| CancellationToken cancellationToken) => | ||
| await endpoint.HandleAsync(userId, cancellationToken)) | ||
| .WithName("Cancel Change Email Address") | ||
| .WithTags("Users") | ||
| .Produces(StatusCodes.Status200OK) | ||
| .Produces(StatusCodes.Status400BadRequest) | ||
| .Produces(StatusCodes.Status404NotFound) | ||
| .Produces(StatusCodes.Status401Unauthorized) | ||
| .WithOpenApi(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Handles the cancellation of a user's email address change request. | ||
| /// </summary> | ||
| public async Task<IResult> HandleAsync( | ||
| Guid userId, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| logger.LogInformation("Cancelling email change for user {UserId}", userId); | ||
|
Check warning on line 47 in src/Dfe.SignIn.InternalApi/Features/Users/ChangeEmail/CancelChangeEmailAddressEndpoint.cs
|
||
|
|
||
| var userInfo = await userLookupService.GetUserInfoAsync(userId, cancellationToken); | ||
|
|
||
| if (userInfo is null) { | ||
| logger.LogWarning("User {UserId} not found", userId); | ||
| return Results.NotFound(new { Message = "User not found" }); | ||
| } | ||
|
|
||
| await auditWriter.Log(new WriteToAuditRequest { | ||
| EventCategory = AuditEventCategoryNames.ChangeEmail, | ||
| EventName = AuditChangeEmailEventNames.CancelChangeEmail, | ||
| Message = $"Cancel change email request from {userInfo.EmailAddress} (id: {userId})", | ||
| }); | ||
|
|
||
| try { | ||
| await directoriesDbContext.UserCodes | ||
| .Where(uc => uc.Uid == userId) | ||
| .Where(uc => uc.CodeType == UserCodeType.ChangeEmail.ToDbValue()) | ||
| .ExecuteDeleteAsync(cancellationToken); | ||
| } | ||
| catch (Exception ex) { | ||
|
Check warning on line 68 in src/Dfe.SignIn.InternalApi/Features/Users/ChangeEmail/CancelChangeEmailAddressEndpoint.cs
|
||
| logger.LogError(ex, "Error cancelling email change for user {UserId}", userId); | ||
| throw; | ||
| } | ||
|
|
||
| logger.LogInformation("Successfully cancelled email change for user {UserId}", userId); | ||
|
Check warning on line 73 in src/Dfe.SignIn.InternalApi/Features/Users/ChangeEmail/CancelChangeEmailAddressEndpoint.cs
|
||
| return Results.Ok(); | ||
| } | ||
| } | ||
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.