diff --git a/application/account-management/Api/Endpoints/UserEndpoints.cs b/application/account-management/Api/Endpoints/UserEndpoints.cs index 8b04143646..bca436602b 100644 --- a/application/account-management/Api/Endpoints/UserEndpoints.cs +++ b/application/account-management/Api/Endpoints/UserEndpoints.cs @@ -22,10 +22,6 @@ public void MapEndpoints(IEndpointRouteBuilder routes) => await mediator.Send(new GetUserSummaryQuery()) ).Produces(); - group.MapGet("/{id}", async Task> ([AsParameters] GetUserQuery query, IMediator mediator) - => await mediator.Send(query) - ).Produces(); - group.MapPost("/", async Task (CreateUserCommand command, IMediator mediator) => (await mediator.Send(command)).AddResourceUri(RoutesPrefix) ); @@ -43,19 +39,23 @@ public void MapEndpoints(IEndpointRouteBuilder routes) ); // The following endpoints are for the current user only - group.MapPut("/", async Task (UpdateUserCommand command, IMediator mediator) + group.MapGet("/me", async Task> ([AsParameters] GetUserQuery query, IMediator mediator) + => await mediator.Send(query) + ).Produces(); + + group.MapPut("/me", async Task (UpdateCurrentUserCommand command, IMediator mediator) => (await mediator.Send(command)).AddRefreshAuthenticationTokens() ); - group.MapPost("/update-avatar", async Task (IFormFile file, IMediator mediator) + group.MapPost("/me/update-avatar", async Task (IFormFile file, IMediator mediator) => await mediator.Send(new UpdateAvatarCommand(file.OpenReadStream(), file.ContentType)) ).DisableAntiforgery(); // Disable anti-forgery until we implement it - group.MapDelete("/remove-avatar", async Task (IMediator mediator) + group.MapDelete("/me/remove-avatar", async Task (IMediator mediator) => await mediator.Send(new RemoveAvatarCommand()) ); - group.MapPut("/change-locale", async Task (ChangeLocaleCommand command, IMediator mediator) + group.MapPut("/me/change-locale", async Task (ChangeLocaleCommand command, IMediator mediator) => (await mediator.Send(command)).AddRefreshAuthenticationTokens() ); } diff --git a/application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs b/application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs index 969ef88441..fa7ca61c31 100644 --- a/application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs +++ b/application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs @@ -33,10 +33,7 @@ public async Task Handle(CompleteLoginCommand command, CancellationToken { var login = await loginRepository.GetByIdAsync(command.Id, cancellationToken); - if (login is null) - { - return Result.NotFound($"Login with id '{command.Id}' not found."); - } + if (login is null) return Result.NotFound($"Login with id '{command.Id}' not found."); if (login.Completed) { diff --git a/application/account-management/Core/Features/Authentication/Commands/ResendLoginCode.cs b/application/account-management/Core/Features/Authentication/Commands/ResendLoginCode.cs index 9656210ffc..208bd173ae 100644 --- a/application/account-management/Core/Features/Authentication/Commands/ResendLoginCode.cs +++ b/application/account-management/Core/Features/Authentication/Commands/ResendLoginCode.cs @@ -31,10 +31,7 @@ ILogger logger public async Task> Handle(ResendLoginCodeCommand command, CancellationToken cancellationToken) { var login = await loginRepository.GetByIdAsync(command.Id, cancellationToken); - if (login is null) - { - return Result.NotFound($"Login with id '{command.Id}' not found."); - } + if (login is null) return Result.NotFound($"Login with id '{command.Id}' not found."); if (login.Completed) { diff --git a/application/account-management/Core/Features/Signups/Commands/CompleteSignup.cs b/application/account-management/Core/Features/Signups/Commands/CompleteSignup.cs index df73538bb4..fe01962c76 100644 --- a/application/account-management/Core/Features/Signups/Commands/CompleteSignup.cs +++ b/application/account-management/Core/Features/Signups/Commands/CompleteSignup.cs @@ -33,10 +33,7 @@ public async Task Handle(CompleteSignupCommand command, CancellationToke { var signup = await signupRepository.GetByIdAsync(command.Id, cancellationToken); - if (signup is null) - { - return Result.NotFound($"Signup with id '{command.Id}' not found."); - } + if (signup is null) return Result.NotFound($"Signup with id '{command.Id}' not found."); if (signup.Completed) { diff --git a/application/account-management/Core/Features/Signups/Commands/ResendSignupCode.cs b/application/account-management/Core/Features/Signups/Commands/ResendSignupCode.cs index b4403ec8b3..df0aaad7a6 100644 --- a/application/account-management/Core/Features/Signups/Commands/ResendSignupCode.cs +++ b/application/account-management/Core/Features/Signups/Commands/ResendSignupCode.cs @@ -29,10 +29,7 @@ ILogger logger public async Task> Handle(ResendSignupCodeCommand command, CancellationToken cancellationToken) { var signup = await signupRepository.GetByIdAsync(command.Id, cancellationToken); - if (signup is null) - { - return Result.NotFound($"Signup with id '{command.Id}' not found."); - } + if (signup is null) return Result.NotFound($"Signup with id '{command.Id}' not found."); if (signup.Completed) { diff --git a/application/account-management/Core/Features/Users/Commands/ChangeLocale.cs b/application/account-management/Core/Features/Users/Commands/ChangeLocale.cs index 50a88ea1d2..0cfce7feff 100644 --- a/application/account-management/Core/Features/Users/Commands/ChangeLocale.cs +++ b/application/account-management/Core/Features/Users/Commands/ChangeLocale.cs @@ -26,8 +26,6 @@ public sealed class ChangeLocaleHandler(IUserRepository userRepository, ITelemet public async Task Handle(ChangeLocaleCommand command, CancellationToken cancellationToken) { var user = await userRepository.GetLoggedInUserAsync(cancellationToken); - if (user is null) return Result.BadRequest("User not found."); - var fromLocale = user.Locale; user.ChangeLocale(command.Locale); userRepository.Update(user); diff --git a/application/account-management/Core/Features/Users/Commands/ChangeUserRole.cs b/application/account-management/Core/Features/Users/Commands/ChangeUserRole.cs index c07acd5788..65e3fc1a2f 100644 --- a/application/account-management/Core/Features/Users/Commands/ChangeUserRole.cs +++ b/application/account-management/Core/Features/Users/Commands/ChangeUserRole.cs @@ -21,10 +21,7 @@ public sealed class ChangeUserRoleHandler(IUserRepository userRepository, IExecu { public async Task Handle(ChangeUserRoleCommand command, CancellationToken cancellationToken) { - if (executionContext.UserInfo.Id == command.Id) - { - return Result.Forbidden("You cannot change your own user role."); - } + if (executionContext.UserInfo.Id == command.Id) return Result.Forbidden("You cannot change your own user role."); if (executionContext.UserInfo.Role != UserRole.Owner.ToString()) { diff --git a/application/account-management/Core/Features/Users/Commands/DeleteUser.cs b/application/account-management/Core/Features/Users/Commands/DeleteUser.cs index 165f4e90fb..a39df44f0f 100644 --- a/application/account-management/Core/Features/Users/Commands/DeleteUser.cs +++ b/application/account-management/Core/Features/Users/Commands/DeleteUser.cs @@ -15,10 +15,7 @@ public sealed class DeleteUserHandler(IUserRepository userRepository, IExecution { public async Task Handle(DeleteUserCommand command, CancellationToken cancellationToken) { - if (executionContext.UserInfo.Id == command.Id) - { - return Result.Forbidden("You cannot delete yourself."); - } + if (executionContext.UserInfo.Id == command.Id) return Result.Forbidden("You cannot delete yourself."); if (executionContext.UserInfo.Role != UserRole.Owner.ToString()) { diff --git a/application/account-management/Core/Features/Users/Commands/RemoveAvatar.cs b/application/account-management/Core/Features/Users/Commands/RemoveAvatar.cs index 6d42f5b747..e37af2079c 100644 --- a/application/account-management/Core/Features/Users/Commands/RemoveAvatar.cs +++ b/application/account-management/Core/Features/Users/Commands/RemoveAvatar.cs @@ -14,7 +14,6 @@ public sealed class RemoveAvatarCommandHandler(IUserRepository userRepository, I public async Task Handle(RemoveAvatarCommand command, CancellationToken cancellationToken) { var user = await userRepository.GetLoggedInUserAsync(cancellationToken); - if (user is null) return Result.BadRequest("User not found."); user.RemoveAvatar(); userRepository.Update(user); diff --git a/application/account-management/Core/Features/Users/Commands/UpdateAvatar.cs b/application/account-management/Core/Features/Users/Commands/UpdateAvatar.cs index a3efd9db08..5719904f6f 100644 --- a/application/account-management/Core/Features/Users/Commands/UpdateAvatar.cs +++ b/application/account-management/Core/Features/Users/Commands/UpdateAvatar.cs @@ -24,19 +24,12 @@ public UpdateAvatarValidator() } } -public sealed class UpdateAvatarHandler( - IUserRepository userRepository, - AvatarUpdater avatarUpdater, - ITelemetryEventsCollector events -) : IRequestHandler +public sealed class UpdateAvatarHandler(IUserRepository userRepository, AvatarUpdater avatarUpdater, ITelemetryEventsCollector events) + : IRequestHandler { public async Task Handle(UpdateAvatarCommand command, CancellationToken cancellationToken) { var user = await userRepository.GetLoggedInUserAsync(cancellationToken); - if (user is null) - { - return Result.BadRequest("User not found."); - } if (await avatarUpdater.UpdateAvatar(user, false, command.ContentType, command.FileSteam, cancellationToken)) { diff --git a/application/account-management/Core/Features/Users/Commands/UpdateUser.cs b/application/account-management/Core/Features/Users/Commands/UpdateCurrentUser.cs similarity index 72% rename from application/account-management/Core/Features/Users/Commands/UpdateUser.cs rename to application/account-management/Core/Features/Users/Commands/UpdateCurrentUser.cs index d76b7d7782..f0eaf53763 100644 --- a/application/account-management/Core/Features/Users/Commands/UpdateUser.cs +++ b/application/account-management/Core/Features/Users/Commands/UpdateCurrentUser.cs @@ -8,7 +8,7 @@ namespace PlatformPlatform.AccountManagement.Features.Users.Commands; [PublicAPI] -public sealed record UpdateUserCommand : ICommand, IRequest +public sealed record UpdateCurrentUserCommand : ICommand, IRequest { public required string Email { get; init; } @@ -19,9 +19,9 @@ public sealed record UpdateUserCommand : ICommand, IRequest public required string Title { get; init; } } -public sealed class UpdateUserValidator : AbstractValidator +public sealed class UpdateCurrentUserValidator : AbstractValidator { - public UpdateUserValidator() + public UpdateCurrentUserValidator() { RuleFor(x => x.Email).NotEmpty().SetValidator(new SharedValidations.Email()); RuleFor(x => x.FirstName).NotEmpty().MaximumLength(30).WithMessage("First name must be no longer than 30 characters."); @@ -30,13 +30,12 @@ public UpdateUserValidator() } } -public sealed class UpdateUserHandler(IUserRepository userRepository, ITelemetryEventsCollector events) - : IRequestHandler +public sealed class UpdateCurrentUserHandler(IUserRepository userRepository, ITelemetryEventsCollector events) + : IRequestHandler { - public async Task Handle(UpdateUserCommand command, CancellationToken cancellationToken) + public async Task Handle(UpdateCurrentUserCommand command, CancellationToken cancellationToken) { var user = await userRepository.GetLoggedInUserAsync(cancellationToken); - if (user is null) return Result.BadRequest("User not found."); user.UpdateEmail(command.Email); user.Update(command.FirstName, command.LastName, command.Title); diff --git a/application/account-management/Core/Features/Users/Domain/UserRepository.cs b/application/account-management/Core/Features/Users/Domain/UserRepository.cs index 3776fd8821..bd7032d23c 100644 --- a/application/account-management/Core/Features/Users/Domain/UserRepository.cs +++ b/application/account-management/Core/Features/Users/Domain/UserRepository.cs @@ -10,7 +10,7 @@ public interface IUserRepository : ICrudRepository { Task GetByIdUnfilteredAsync(UserId id, CancellationToken cancellationToken); - Task GetLoggedInUserAsync(CancellationToken cancellationToken); + Task GetLoggedInUserAsync(CancellationToken cancellationToken); Task GetUserByEmailUnfilteredAsync(string email, CancellationToken cancellationToken); @@ -48,10 +48,11 @@ internal sealed class UserRepository(AccountManagementDbContext accountManagemen .SingleOrDefaultAsync(u => u.Id == id, cancellationToken); } - public async Task GetLoggedInUserAsync(CancellationToken cancellationToken) + public async Task GetLoggedInUserAsync(CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(executionContext.UserInfo.Id); - return await GetByIdAsync(executionContext.UserInfo.Id, cancellationToken); + return await GetByIdAsync(executionContext.UserInfo.Id, cancellationToken) ?? + throw new InvalidOperationException("Logged in user not found."); } /// diff --git a/application/account-management/Core/Features/Users/Queries/GetUser.cs b/application/account-management/Core/Features/Users/Queries/GetCurrentUser.cs similarity index 73% rename from application/account-management/Core/Features/Users/Queries/GetUser.cs rename to application/account-management/Core/Features/Users/Queries/GetCurrentUser.cs index a832da7155..186945c68f 100644 --- a/application/account-management/Core/Features/Users/Queries/GetUser.cs +++ b/application/account-management/Core/Features/Users/Queries/GetCurrentUser.cs @@ -7,7 +7,7 @@ namespace PlatformPlatform.AccountManagement.Features.Users.Queries; [PublicAPI] -public sealed record GetUserQuery(UserId Id) : IRequest>; +public sealed record GetUserQuery : IRequest>; [PublicAPI] public sealed record UserResponse( @@ -27,7 +27,7 @@ public sealed class GetUserHandler(IUserRepository userRepository) { public async Task> Handle(GetUserQuery query, CancellationToken cancellationToken) { - var user = await userRepository.GetByIdAsync(query.Id, cancellationToken); - return user?.Adapt() ?? Result.NotFound($"User with id '{query.Id}' not found."); + var user = await userRepository.GetLoggedInUserAsync(cancellationToken); + return user.Adapt(); } } diff --git a/application/account-management/Tests/Users/GetUserTests.cs b/application/account-management/Tests/Users/GetCurrentUserTests.cs similarity index 53% rename from application/account-management/Tests/Users/GetUserTests.cs rename to application/account-management/Tests/Users/GetCurrentUserTests.cs index ae965148ed..1b9542a1b7 100644 --- a/application/account-management/Tests/Users/GetUserTests.cs +++ b/application/account-management/Tests/Users/GetCurrentUserTests.cs @@ -1,23 +1,18 @@ -using System.Net; using FluentAssertions; using NJsonSchema; using PlatformPlatform.AccountManagement.Database; -using PlatformPlatform.SharedKernel.Domain; using PlatformPlatform.SharedKernel.Tests; using Xunit; namespace PlatformPlatform.AccountManagement.Tests.Users; -public sealed class GetUserTests : EndpointBaseTest +public sealed class GetCurrentUserTests : EndpointBaseTest { [Fact] - public async Task GetUser_WhenUserExists_ShouldReturnUserWithValidContract() + public async Task GetLoggedInUser_WhenUserExists_ShouldReturnUserWithValidContract() { - // Arrange - var existingUserId = DatabaseSeeder.User1.Id; - // Act - var response = await AuthenticatedHttpClient.GetAsync($"/api/account-management/users/{existingUserId}"); + var response = await AuthenticatedHttpClient.GetAsync("/api/account-management/users/me"); // Assert response.ShouldBeSuccessfulGetRequest(); @@ -47,30 +42,4 @@ public async Task GetUser_WhenUserExists_ShouldReturnUserWithValidContract() var responseBody = await response.Content.ReadAsStringAsync(); schema.Validate(responseBody).Should().BeEmpty(); } - - [Fact] - public async Task GetUser_WhenUserDoesNotExist_ShouldReturnNotFound() - { - // Arrange - var unknownUserId = UserId.NewId(); - - // Act - var response = await AuthenticatedHttpClient.GetAsync($"/api/account-management/users/{unknownUserId}"); - - // Assert - await response.ShouldHaveErrorStatusCode(HttpStatusCode.NotFound, $"User with id '{unknownUserId}' not found."); - } - - [Fact] - public async Task GetUser_WhenInvalidUserId_ShouldReturnBadRequest() - { - // Arrange - var invalidUserId = Faker.Random.AlphaNumeric(31); - - // Act - var response = await AuthenticatedHttpClient.GetAsync($"/api/account-management/users/{invalidUserId}"); - - // Assert - await response.ShouldHaveErrorStatusCode(HttpStatusCode.BadRequest, $"""Failed to bind parameter "UserId Id" from "{invalidUserId}"."""); - } } diff --git a/application/account-management/Tests/Users/UpdateUserTests.cs b/application/account-management/Tests/Users/UpdateCurrentUserTests.cs similarity index 78% rename from application/account-management/Tests/Users/UpdateUserTests.cs rename to application/account-management/Tests/Users/UpdateCurrentUserTests.cs index 54ac53b9f8..dda5daf7b6 100644 --- a/application/account-management/Tests/Users/UpdateUserTests.cs +++ b/application/account-management/Tests/Users/UpdateCurrentUserTests.cs @@ -8,13 +8,13 @@ namespace PlatformPlatform.AccountManagement.Tests.Users; -public sealed class UpdateUserTests : EndpointBaseTest +public sealed class UpdateCurrentUserTests : EndpointBaseTest { [Fact] - public async Task UpdateUser_WhenValid_ShouldUpdateUser() + public async Task UpdateCurrentUser_WhenValid_ShouldUpdateUser() { // Arrange - var command = new UpdateUserCommand + var command = new UpdateCurrentUserCommand { Email = Faker.Internet.Email(), FirstName = Faker.Name.FirstName(), @@ -23,17 +23,17 @@ public async Task UpdateUser_WhenValid_ShouldUpdateUser() }; // Act - var response = await AuthenticatedHttpClient.PutAsJsonAsync("/api/account-management/users", command); + var response = await AuthenticatedHttpClient.PutAsJsonAsync("/api/account-management/users/me", command); // Assert response.ShouldHaveEmptyHeaderAndLocationOnSuccess(); } [Fact] - public async Task UpdateUser_WhenInvalid_ShouldReturnBadRequest() + public async Task UpdateCurrentUser_WhenInvalid_ShouldReturnBadRequest() { // Arrange - var command = new UpdateUserCommand + var command = new UpdateCurrentUserCommand { Email = Faker.InvalidEmail(), FirstName = Faker.Random.String(31), @@ -42,7 +42,7 @@ public async Task UpdateUser_WhenInvalid_ShouldReturnBadRequest() }; // Act - var response = await AuthenticatedHttpClient.PutAsJsonAsync("/api/account-management/users", command); + var response = await AuthenticatedHttpClient.PutAsJsonAsync("/api/account-management/users/me", command); // Assert var expectedErrors = new[] diff --git a/application/account-management/WebApp/routes/(index)/-components/FeatureSection4.tsx b/application/account-management/WebApp/routes/(index)/-components/FeatureSection4.tsx index ccf3afb155..81764280c2 100644 --- a/application/account-management/WebApp/routes/(index)/-components/FeatureSection4.tsx +++ b/application/account-management/WebApp/routes/(index)/-components/FeatureSection4.tsx @@ -27,7 +27,7 @@ export function FeatureSection4() { /> setData(response)) .catch((error) => setError(error)) .finally(() => setLoading(false)); } - }, [isOpen, userId]); + }, [isOpen]); // Close dialog and cleanup const closeDialog = useCallback(() => { @@ -68,7 +68,7 @@ export default function UserProfileModal({ isOpen, onOpenChange, userId }: Reado // Handle form submission const [{ success, errors, title, message }, action, isPending] = useActionState( - api.actionPut("/api/account-management/users"), + api.actionPut("/api/account-management/users/me"), { success: null } ); @@ -78,9 +78,9 @@ export default function UserProfileModal({ isOpen, onOpenChange, userId }: Reado try { if (selectedAvatarFile) { - await api.uploadFile("/api/account-management/users/update-avatar", selectedAvatarFile); + await api.uploadFile("/api/account-management/users/me/update-avatar", selectedAvatarFile); } else if (removeAvatarFlag) { - await api.delete("/api/account-management/users/remove-avatar"); + await api.delete("/api/account-management/users/me/remove-avatar"); setRemoveAvatarFlag(false); } @@ -97,7 +97,7 @@ export default function UserProfileModal({ isOpen, onOpenChange, userId }: Reado // Add a small delay to ensure all requests have completed setTimeout(async () => { try { - const response = await api.get("/api/account-management/users/{id}", { params: { path: { id: userId } } }); + const response = await api.get("/api/account-management/users/me"); updateUserInfo({ firstName: response.firstName, lastName: response.lastName, @@ -112,7 +112,7 @@ export default function UserProfileModal({ isOpen, onOpenChange, userId }: Reado } }, 100); } - }, [success, closeDialog, userId, updateUserInfo, isSaving]); + }, [success, closeDialog, updateUserInfo, isSaving]); // Handle file selection const onFileSelect = (files: FileList | null) => { diff --git a/application/account-management/WebApp/shared/lib/api/AccountManagement.Api.json b/application/account-management/WebApp/shared/lib/api/AccountManagement.Api.json index 33367a7543..fd39a370bc 100644 --- a/application/account-management/WebApp/shared/lib/api/AccountManagement.Api.json +++ b/application/account-management/WebApp/shared/lib/api/AccountManagement.Api.json @@ -456,29 +456,6 @@ "description": "" } } - }, - "put": { - "tags": [ - "Users" - ], - "operationId": "PutApiAccountManagementUsers", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateUserCommand" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - } - } } }, "/api/account-management/users/summary": { @@ -502,35 +479,6 @@ } }, "/api/account-management/users/{id}": { - "get": { - "tags": [ - "Users" - ], - "operationId": "GetApiAccountManagementUsers2", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "$ref": "#/components/schemas/UserId" - }, - "x-position": 1 - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserResponse" - } - } - } - } - } - }, "delete": { "tags": [ "Users" @@ -615,12 +563,55 @@ } } }, - "/api/account-management/users/update-avatar": { + "/api/account-management/users/me": { + "get": { + "tags": [ + "Users" + ], + "operationId": "GetApiAccountManagementUsersMe", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "Users" + ], + "operationId": "PutApiAccountManagementUsersMe", + "requestBody": { + "x-name": "command", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCurrentUserCommand" + } + } + }, + "required": true, + "x-position": 1 + }, + "responses": { + "200": { + "description": "" + } + } + } + }, + "/api/account-management/users/me/update-avatar": { "post": { "tags": [ "Users" ], - "operationId": "PostApiAccountManagementUsersUpdateAvatar", + "operationId": "PostApiAccountManagementUsersMeUpdateAvatar", "requestBody": { "content": { "multipart/form-data": { @@ -644,12 +635,12 @@ } } }, - "/api/account-management/users/remove-avatar": { + "/api/account-management/users/me/remove-avatar": { "delete": { "tags": [ "Users" ], - "operationId": "DeleteApiAccountManagementUsersRemoveAvatar", + "operationId": "DeleteApiAccountManagementUsersMeRemoveAvatar", "responses": { "200": { "description": "" @@ -657,12 +648,12 @@ } } }, - "/api/account-management/users/change-locale": { + "/api/account-management/users/me/change-locale": { "put": { "tags": [ "Users" ], - "operationId": "PutApiAccountManagementUsersChangeLocale", + "operationId": "PutApiAccountManagementUsersMeChangeLocale", "requestBody": { "x-name": "command", "content": { @@ -1058,43 +1049,6 @@ } } }, - "UserResponse": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "$ref": "#/components/schemas/UserId" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "modifiedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "email": { - "type": "string" - }, - "role": { - "$ref": "#/components/schemas/UserRole" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "title": { - "type": "string" - }, - "avatarUrl": { - "type": "string", - "nullable": true - } - } - }, "CreateUserCommand": { "type": "object", "additionalProperties": false, @@ -1135,7 +1089,44 @@ } } }, - "UpdateUserCommand": { + "UserResponse": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/components/schemas/UserId" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "modifiedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "email": { + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/UserRole" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "title": { + "type": "string" + }, + "avatarUrl": { + "type": "string", + "nullable": true + } + } + }, + "UpdateCurrentUserCommand": { "type": "object", "additionalProperties": false, "properties": { diff --git a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs index 37d89916b3..277eae3b47 100644 --- a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs +++ b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs @@ -44,7 +44,7 @@ public static IServiceCollection AddSharedServices(this IServiceCollection se .AddPersistenceHelpers() .AddDefaultHealthChecks() .AddEmailClient() - .AddMediatRPipelineBehaviours() + .AddMediatRPipelineBehaviors() .RegisterMediatRRequest(assemblies) .RegisterRepositories(assemblies); } @@ -128,15 +128,17 @@ private static IServiceCollection AddEmailClient(this IServiceCollection service return services; } - private static IServiceCollection AddMediatRPipelineBehaviours(this IServiceCollection services) + private static IServiceCollection AddMediatRPipelineBehaviors(this IServiceCollection services) { - // Order is important! First all Pre-behaviors run, then the command is handled, and finally all Post behaviors run. + // Order is important! First all Pre behaviors run, then the command is handled, and finally all Post behaviors run. // So Validation → Command → PublishDomainEvents → UnitOfWork → PublishTelemetryEvents. - return services + services .AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationPipelineBehavior<,>)) // Pre .AddTransient(typeof(IPipelineBehavior<,>), typeof(PublishTelemetryEventsPipelineBehavior<,>)) // Post .AddTransient(typeof(IPipelineBehavior<,>), typeof(UnitOfWorkPipelineBehavior<,>)) // Post - .AddTransient(typeof(IPipelineBehavior<,>), typeof(PublishDomainEventsPipelineBehavior<,>)) // Post + .AddTransient(typeof(IPipelineBehavior<,>), typeof(PublishDomainEventsPipelineBehavior<,>)); // Post + + return services .AddScoped() .AddScoped(); } diff --git a/application/shared-kernel/SharedKernel/DomainEvents/IDomainEvent.cs b/application/shared-kernel/SharedKernel/DomainEvents/IDomainEvent.cs index a86cf3f251..9415801bb0 100644 --- a/application/shared-kernel/SharedKernel/DomainEvents/IDomainEvent.cs +++ b/application/shared-kernel/SharedKernel/DomainEvents/IDomainEvent.cs @@ -3,15 +3,14 @@ namespace PlatformPlatform.SharedKernel.DomainEvents; /// /// The DomainEvent interface represents a domain event that occurred in the domain. The DomainEvent implements the /// interface from MediatR. To configure an event handler, you need to create a class -/// that implements the INotificationHandler interface (from the MediatR library). This should be done in the -/// Application Layer. Any event that occurs in the domain can be handled by one or more domain event handlers. -/// Domain events are happening in the context of an aggregate. Events are published by the -/// PublishDomainEventsPipelineBehavior in the Application Layer, before the UnitOfWork is committed and the parent -/// aggregate that raises the event is saved to the database. That means that the EventHandler also runs before -/// changes are saved to the database. It's crucial to understand that domain event handlers are not supposed to -/// produce any external side effects outside the current transaction/UnitOfWork. For instance, they can be utilized -/// to create, update, or delete aggregates, or they could be used to update read models. However, they should not -/// be used to invoke other services (e.g., send emails) that are not part of the same database transaction. For -/// such tasks, use Integration Events instead. +/// that implements the INotificationHandler interface (from the MediatR library). Any event that occurs in the +/// domain can be handled by one or more domain event handlers. Domain events are happening in the context of an +/// aggregate. Events are published by the PublishDomainEventsPipelineBehavior, before the UnitOfWork is committed, +/// and before the parent aggregate that raises the event is saved to the database. That means that the EventHandler +/// also runs before changes are saved to the database. It's crucial to understand that domain event handlers are +/// not supposed to produce any external side effects outside the current transaction/UnitOfWork. For instance, they +/// can be utilized to create, update, or delete aggregates, or they could be used to update read models. However, +/// they should not be used to invoke other services (e.g., send emails) that are not part of the same database +/// transaction. For such tasks, use Integration Events instead. /// public interface IDomainEvent : INotification;