Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,13 @@ public interface IUsersApiClient
/// <returns>A task representing the asynchronous operation.</returns>
[Post(UsersApiRoutes.ConfirmChangeEmail)]
Task ConfirmChangeEmailAddress(Guid userId, [Body] ConfirmChangeEmailAddressRequest request, CancellationToken cancellationToken = default);

/// <summary>
/// Cancels the process of changing a user's email address.
/// </summary>
/// <param name="userId">The ID of the user whose email address change is being cancelled.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation.</returns>
[Delete(UsersApiRoutes.CancelChangeEmail)]
Task CancelChangeEmailAddress(Guid userId, CancellationToken cancellationToken = default);
}
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",
Comment thread
amjadmahm marked this conversation as resolved.
_ => throw new ArgumentOutOfRangeException(nameof(codeType), codeType, null)
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,9 @@ public static class UsersApiRoutes
/// The API route for confirming the change of a user's email address.
/// </summary>
public const string ConfirmChangeEmail = "/internal/users/{userId}/confirm-change-email";

/// <summary>
/// The API route for cancelling the change of a user's email address.
/// </summary>
public const string CancelChangeEmail = "/internal/users/{userId}/cancel-change-email";
}

This file was deleted.

2 changes: 0 additions & 2 deletions src/Dfe.SignIn.Core.Entities/Directories/UserCodeEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,3 @@ public partial class UserCodeEntity

public string? ContextData { get; set; }
}
#pragma warning restore CS1591

45 changes: 45 additions & 0 deletions src/Dfe.SignIn.InternalApi/Endpoints/EndpointRegistry.cs
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
@@ -1,4 +1,4 @@
namespace Dfe.SignIn.InternalApi;
namespace Dfe.SignIn.InternalApi.Endpoints;

/// <summary>
/// Defines a contract for an endpoint that can be mapped to an <see cref="IEndpointRouteBuilder"/>.
Expand Down
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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Evaluation of this argument may be expensive and unnecessary if logging is disabled

See more on https://sonarcloud.io/project/issues?id=DFE-Digital_dsi-platform&issues=AaAAF7GvTjGJnha_zEse&open=AaAAF7GvTjGJnha_zEse&pullRequest=242

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Either log this exception and handle it, or rethrow it with some contextual information.

See more on https://sonarcloud.io/project/issues?id=DFE-Digital_dsi-platform&issues=AaAAF7GvTjGJnha_zEsd&open=AaAAF7GvTjGJnha_zEsd&pullRequest=242
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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Evaluation of this argument may be expensive and unnecessary if logging is disabled

See more on https://sonarcloud.io/project/issues?id=DFE-Digital_dsi-platform&issues=AaAAF7GvTjGJnha_zEsf&open=AaAAF7GvTjGJnha_zEsf&pullRequest=242
return Results.Ok();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Dfe.SignIn.Core.Interfaces.ExternalAuth;
using Dfe.SignIn.Core.Interfaces.Notifications;
using Dfe.SignIn.Gateways.EntityFramework;
using Dfe.SignIn.InternalApi.Endpoints;
using Dfe.SignIn.InternalApi.Features.Users.UserCode;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Dfe.SignIn.Core.Contracts.Features.Users;
using Dfe.SignIn.Core.Contracts.Features.Users.ChangeEmailAddress;
using Dfe.SignIn.Gateways.DistributedCache.Interactions;
using Dfe.SignIn.InternalApi.Endpoints;
using Dfe.SignIn.InternalApi.Features.Users.UserCode;
using Humanizer;
using Microsoft.AspNetCore.Mvc;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Dfe.SignIn.Core.Contracts.Features.Users;
using Dfe.SignIn.Core.Contracts.Features.Users.ChangeJobTitle;
using Dfe.SignIn.Gateways.EntityFramework;
using Dfe.SignIn.InternalApi.Endpoints;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Dfe.SignIn.Core.Contracts.Features.Users;
using Dfe.SignIn.Core.Contracts.Features.Users.ChangeName;
using Dfe.SignIn.Gateways.EntityFramework;
using Dfe.SignIn.InternalApi.Endpoints;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Dfe.SignIn.Core.Contracts.Features.Users;
using Dfe.SignIn.Core.Contracts.Features.Users.GetUserProfile;
using Dfe.SignIn.Gateways.EntityFramework;
using Dfe.SignIn.InternalApi.Endpoints;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Dfe.SignIn.Core.Contracts.Organisations;
using Dfe.SignIn.Core.Contracts.Users;
using Dfe.SignIn.Gateways.EntityFramework;
using Dfe.SignIn.InternalApi.Endpoints;
using Dfe.SignIn.InternalApi.Features.Users.ChangeName;
using Microsoft.EntityFrameworkCore;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Dfe.SignIn.Core.Contracts.Organisations;
using Dfe.SignIn.Core.Contracts.Users;
using Dfe.SignIn.Gateways.EntityFramework;
using Dfe.SignIn.InternalApi.Endpoints;
using Microsoft.EntityFrameworkCore;

namespace Dfe.SignIn.InternalApi.Features.Users.PendingApprovalCounter;
Expand Down
11 changes: 11 additions & 0 deletions src/Dfe.SignIn.InternalApi/Features/Users/UsersFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Dfe.SignIn.Core.Interfaces.Notifications;
using Dfe.SignIn.Core.UseCases.Users;
using Dfe.SignIn.Gateways.DistributedCache.Interactions;
using Dfe.SignIn.InternalApi.Endpoints;
using Dfe.SignIn.InternalApi.Features.Users.ChangeEmail;
using Dfe.SignIn.InternalApi.Features.Users.ChangeJobTitle;
using Dfe.SignIn.InternalApi.Features.Users.ChangeName;
Expand All @@ -21,6 +22,9 @@ namespace Dfe.SignIn.InternalApi.Features.Users;
[ExcludeFromCodeCoverage]
public static class UsersFeature
{
private static readonly EndpointRegistry NewEndpointRegistry = new EndpointRegistry()
.Add<CancelChangeEmailAddressEndpoint>();

/// <summary>
/// Maps the user-related endpoints to the specified <see cref="IEndpointRouteBuilder"/>.
/// </summary>
Expand All @@ -34,6 +38,9 @@ public static void MapUsersEndpoints(this IEndpointRouteBuilder app)
IsApproverEndpoint.Map(app);
PendingApprovalCounterEndpoint.Map(app);
ChangeJobTitleEndpoint.Map(app);

// New class-based endpoint mapped via registry
NewEndpointRegistry.MapRoutes(app);
}

/// <summary>
Expand All @@ -50,6 +57,10 @@ public static IServiceCollection AddUserServices(this IServiceCollection service
services.AddScoped<IUserUpdatedPublisher, StubUserUpdatedPublisher>();

services.AddInteractionLimiter<InitiateChangeEmailAddressRequest>(configuration);

// Register class-based endpoints with DI
NewEndpointRegistry.RegisterServices(services);

return services;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,7 @@ public IActionResult PostComplete()
[ValidateAntiForgeryToken]
public async Task<IActionResult> PostCancel()
{
await interaction.DispatchAsync(
new CancelPendingChangeEmailAddressRequest {
UserId = this.User.GetUserId(),
}
);
await usersApiClient.CancelChangeEmailAddress(this.User.GetUserId());

this.SetFlashNotification(
heading: "Email change cancelled",
Expand Down
Loading
Loading