Skip to content

Commit 6949fb3

Browse files
authored
Finalize Account Settings and switch to a dedicated page (#681)
### Summary & Motivation Convert the Account Settings from a modal dialog accessed through the `AvatarButton` to a standalone page, now accessible via the **Account** menu in the **Side Menu**. This change improves usability and provides a more structured experience for managing account details. A new endpoint has been implemented to update the **tenant name** (account name) when saving changes. Previously, all tenant-related API endpoints used `/tenants/{id}`, but these have been updated to `/tenants/current` for improved clarity and consistency. Both `GET` and `PUT` endpoints now use this new convention. To support this change, a new `GetCurrentTenantAsync` method has been added to the `TenantRepository`, and the `GetTenant` and `GetCurrentTenant` commands have been updated to use this method. All related frontend components, commands, and tests have been updated accordingly. ### Checklist - [x] I have added tests, or done manual regression tests - [x] I have updated the documentation, if necessary
2 parents a231365 + e3a5d3e commit 6949fb3

File tree

17 files changed

+253
-300
lines changed

17 files changed

+253
-300
lines changed

application/account-management/Api/Endpoints/TenantEndpoints.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ public void MapEndpoints(IEndpointRouteBuilder routes)
1414
{
1515
var group = routes.MapGroup(RoutesPrefix).WithTags("Tenants").RequireAuthorization();
1616

17-
group.MapGet("/{id}", async Task<ApiResult<TenantResponse>> ([AsParameters] GetTenantQuery query, IMediator mediator)
18-
=> await mediator.Send(query)
17+
group.MapGet("/current", async Task<ApiResult<TenantResponse>> (IMediator mediator)
18+
=> await mediator.Send(new GetTenantQuery())
1919
).Produces<TenantResponse>();
2020

21-
group.MapPut("/{id}", async Task<ApiResult> (TenantId id, UpdateTenantCommand command, IMediator mediator)
22-
=> await mediator.Send(command with { Id = id })
21+
group.MapPut("/current", async Task<ApiResult> (UpdateCurrentTenantCommand command, IMediator mediator)
22+
=> await mediator.Send(command)
2323
);
2424

25-
group.MapDelete("/{id}", async Task<ApiResult> (TenantId id, IMediator mediator)
25+
routes.MapDelete("/internal-api/account-management/tenants/{id}", async Task<ApiResult> (TenantId id, IMediator mediator)
2626
=> await mediator.Send(new DeleteTenantCommand(id))
2727
);
2828
}

application/account-management/Core/Features/Tenants/Commands/UpdateTenant.cs renamed to application/account-management/Core/Features/Tenants/Commands/UpdateCurrentTenant.cs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,19 @@
22
using JetBrains.Annotations;
33
using PlatformPlatform.AccountManagement.Features.Tenants.Domain;
44
using PlatformPlatform.SharedKernel.Cqrs;
5-
using PlatformPlatform.SharedKernel.Domain;
65
using PlatformPlatform.SharedKernel.Telemetry;
76

87
namespace PlatformPlatform.AccountManagement.Features.Tenants.Commands;
98

109
[PublicAPI]
11-
public sealed record UpdateTenantCommand : ICommand, IRequest<Result>
10+
public sealed record UpdateCurrentTenantCommand : ICommand, IRequest<Result>
1211
{
13-
[JsonIgnore] // Removes this property from the API contract
14-
public TenantId Id { get; init; } = null!;
15-
1612
public required string Name { get; init; }
1713
}
1814

19-
public sealed class UpdateTenantValidator : AbstractValidator<UpdateTenantCommand>
15+
public sealed class UpdateCurrentTenantValidator : AbstractValidator<UpdateCurrentTenantCommand>
2016
{
21-
public UpdateTenantValidator()
17+
public UpdateCurrentTenantValidator()
2218
{
2319
RuleFor(x => x.Name).NotEmpty();
2420
RuleFor(x => x.Name).Length(1, 30)
@@ -28,12 +24,11 @@ public UpdateTenantValidator()
2824
}
2925

3026
public sealed class UpdateTenantHandler(ITenantRepository tenantRepository, ITelemetryEventsCollector events)
31-
: IRequestHandler<UpdateTenantCommand, Result>
27+
: IRequestHandler<UpdateCurrentTenantCommand, Result>
3228
{
33-
public async Task<Result> Handle(UpdateTenantCommand command, CancellationToken cancellationToken)
29+
public async Task<Result> Handle(UpdateCurrentTenantCommand command, CancellationToken cancellationToken)
3430
{
35-
var tenant = await tenantRepository.GetByIdAsync(command.Id, cancellationToken);
36-
if (tenant is null) return Result.NotFound($"Tenant with id '{command.Id}' not found.");
31+
var tenant = await tenantRepository.GetCurrentTenantAsync(cancellationToken);
3732

3833
tenant.Update(command.Name);
3934
tenantRepository.Update(tenant);

application/account-management/Core/Features/Tenants/Domain/TenantRepository.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
using Microsoft.EntityFrameworkCore;
22
using PlatformPlatform.AccountManagement.Database;
33
using PlatformPlatform.SharedKernel.Domain;
4+
using PlatformPlatform.SharedKernel.ExecutionContext;
45
using PlatformPlatform.SharedKernel.Persistence;
56

67
namespace PlatformPlatform.AccountManagement.Features.Tenants.Domain;
78

89
public interface ITenantRepository : ICrudRepository<Tenant, TenantId>
910
{
11+
Task<Tenant> GetCurrentTenantAsync(CancellationToken cancellationToken);
12+
1013
Task<bool> ExistsAsync(TenantId id, CancellationToken cancellationToken);
1114

1215
Task<bool> IsSubdomainFreeAsync(string subdomain, CancellationToken cancellationToken);
1316
}
1417

15-
internal sealed class TenantRepository(AccountManagementDbContext accountManagementDbContext)
18+
internal sealed class TenantRepository(AccountManagementDbContext accountManagementDbContext, IExecutionContext executionContext)
1619
: RepositoryBase<Tenant, TenantId>(accountManagementDbContext), ITenantRepository
1720
{
21+
public async Task<Tenant> GetCurrentTenantAsync(CancellationToken cancellationToken)
22+
{
23+
ArgumentNullException.ThrowIfNull(executionContext.TenantId!);
24+
return await GetByIdAsync(executionContext.TenantId, cancellationToken) ??
25+
throw new InvalidOperationException("Active tenant not found.");
26+
}
27+
1828
public Task<bool> IsSubdomainFreeAsync(string subdomain, CancellationToken cancellationToken)
1929
{
2030
return DbSet.AllAsync(tenant => tenant.Id != subdomain, cancellationToken);

application/account-management/Core/Features/Tenants/Queries/GetTenant.cs renamed to application/account-management/Core/Features/Tenants/Queries/GetCurrentTenant.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
namespace PlatformPlatform.AccountManagement.Features.Tenants.Queries;
88

99
[PublicAPI]
10-
public sealed record GetTenantQuery(TenantId Id) : IRequest<Result<TenantResponse>>;
10+
public sealed record GetTenantQuery : IRequest<Result<TenantResponse>>;
1111

1212
[PublicAPI]
1313
public sealed record TenantResponse(TenantId Id, DateTimeOffset CreatedAt, DateTimeOffset? ModifiedAt, string Name, TenantState State);
@@ -17,7 +17,7 @@ public sealed class GetTenantHandler(ITenantRepository tenantRepository)
1717
{
1818
public async Task<Result<TenantResponse>> Handle(GetTenantQuery query, CancellationToken cancellationToken)
1919
{
20-
var tenant = await tenantRepository.GetByIdAsync(query.Id, cancellationToken);
21-
return tenant?.Adapt<TenantResponse>() ?? Result<TenantResponse>.NotFound($"Tenant with id '{query.Id}' not found.");
20+
var tenant = await tenantRepository.GetCurrentTenantAsync(cancellationToken);
21+
return tenant.Adapt<TenantResponse>();
2222
}
2323
}

application/account-management/Tests/Tenants/DeleteTenantTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public async Task DeleteTenant_WhenTenantDoesNotExists_ShouldReturnNotFound()
2020
var unknownTenantId = Faker.Subdomain();
2121

2222
// Act
23-
var response = await AuthenticatedHttpClient.DeleteAsync($"/api/account-management/tenants/{unknownTenantId}");
23+
var response = await AuthenticatedHttpClient.DeleteAsync($"/internal-api/account-management/tenants/{unknownTenantId}");
2424

2525
//Assert
2626
await response.ShouldHaveErrorStatusCode(HttpStatusCode.NotFound, $"Tenant with id '{unknownTenantId}' not found.");
@@ -50,7 +50,7 @@ public async Task DeleteTenant_WhenTenantHasUsers_ShouldReturnBadRequest()
5050
);
5151

5252
// Act
53-
var response = await AuthenticatedHttpClient.DeleteAsync($"/api/account-management/tenants/{existingTenantId}");
53+
var response = await AuthenticatedHttpClient.DeleteAsync($"/internal-api/account-management/tenants/{existingTenantId}");
5454
TelemetryEventsCollectorSpy.Reset();
5555

5656
// Assert
@@ -70,7 +70,7 @@ public async Task DeleteTenant_WhenTenantHasNoUsers_ShouldDeleteTenant()
7070
var existingTenantId = DatabaseSeeder.Tenant1.Id;
7171

7272
// Act
73-
var response = await AuthenticatedHttpClient.DeleteAsync($"/api/account-management/tenants/{existingTenantId}");
73+
var response = await AuthenticatedHttpClient.DeleteAsync($"/internal-api/account-management/tenants/{existingTenantId}");
7474

7575
// Assert
7676
response.ShouldHaveEmptyHeaderAndLocationOnSuccess();
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using FluentAssertions;
2+
using NJsonSchema;
3+
using PlatformPlatform.AccountManagement.Database;
4+
using PlatformPlatform.SharedKernel.Tests;
5+
using Xunit;
6+
7+
namespace PlatformPlatform.AccountManagement.Tests.Tenants;
8+
9+
public sealed class GetCurrentTenantTests : EndpointBaseTest<AccountManagementDbContext>
10+
{
11+
[Fact]
12+
public async Task GetCurrentTenant_WhenTenantExists_ShouldReturnTenantWithValidContract()
13+
{
14+
// Act
15+
var response = await AuthenticatedHttpClient.GetAsync("/api/account-management/tenants/current");
16+
17+
// Assert
18+
response.ShouldBeSuccessfulGetRequest();
19+
20+
var schema = await JsonSchema.FromJsonAsync(
21+
"""
22+
{
23+
'type': 'object',
24+
'properties': {
25+
'id': {'type': 'string', 'pattern': '^(?=.{3,30}$)(?!-)[a-z0-9-]*(?<!-)$'},
26+
'createdAt': {'type': 'string', 'format': 'date-time'},
27+
'modifiedAt': {'type': ['null', 'string'], 'format': 'date-time'},
28+
'name': {'type': 'string', 'minLength': 1, 'maxLength': 30},
29+
'state': {'type': 'string', 'minLength': 1, 'maxLength':20}
30+
},
31+
'required': ['id', 'createdAt', 'modifiedAt', 'name', 'state'],
32+
'additionalProperties': false
33+
}
34+
"""
35+
);
36+
37+
var responseBody = await response.Content.ReadAsStringAsync();
38+
schema.Validate(responseBody).Should().BeEmpty();
39+
}
40+
}

application/account-management/Tests/Tenants/GetTenantTests.cs

Lines changed: 0 additions & 70 deletions
This file was deleted.

application/account-management/Tests/Tenants/UpdateTenantTests.cs renamed to application/account-management/Tests/Tenants/UpdateCurrentTenantTests.cs

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,16 @@
99

1010
namespace PlatformPlatform.AccountManagement.Tests.Tenants;
1111

12-
public sealed class UpdateTenantTests : EndpointBaseTest<AccountManagementDbContext>
12+
public sealed class UpdateCurrentTenantTests : EndpointBaseTest<AccountManagementDbContext>
1313
{
1414
[Fact]
15-
public async Task UpdateTenant_WhenValid_ShouldUpdateTenant()
15+
public async Task UpdateCurrentTenant_WhenValid_ShouldUpdateTenant()
1616
{
1717
// Arrange
18-
var existingTenantId = DatabaseSeeder.Tenant1.Id;
19-
var command = new UpdateTenantCommand { Name = Faker.TenantName() };
18+
var command = new UpdateCurrentTenantCommand { Name = Faker.TenantName() };
2019

2120
// Act
22-
var response = await AuthenticatedHttpClient.PutAsJsonAsync($"/api/account-management/tenants/{existingTenantId}", command);
21+
var response = await AuthenticatedHttpClient.PutAsJsonAsync("/api/account-management/tenants/current", command);
2322

2423
// Assert
2524
response.ShouldHaveEmptyHeaderAndLocationOnSuccess();
@@ -30,15 +29,14 @@ public async Task UpdateTenant_WhenValid_ShouldUpdateTenant()
3029
}
3130

3231
[Fact]
33-
public async Task UpdateTenant_WhenInvalid_ShouldReturnBadRequest()
32+
public async Task UpdateCurrentTenant_WhenInvalid_ShouldReturnBadRequest()
3433
{
3534
// Arrange
36-
var existingTenantId = DatabaseSeeder.Tenant1.Id;
3735
var invalidName = Faker.Random.String2(31);
38-
var command = new UpdateTenantCommand { Name = invalidName };
36+
var command = new UpdateCurrentTenantCommand { Name = invalidName };
3937

4038
// Act
41-
var response = await AuthenticatedHttpClient.PutAsJsonAsync($"/api/account-management/tenants/{existingTenantId}", command);
39+
var response = await AuthenticatedHttpClient.PutAsJsonAsync("/api/account-management/tenants/current", command);
4240

4341
// Assert
4442
var expectedErrors = new[]
@@ -49,20 +47,4 @@ public async Task UpdateTenant_WhenInvalid_ShouldReturnBadRequest()
4947

5048
TelemetryEventsCollectorSpy.AreAllEventsDispatched.Should().BeFalse();
5149
}
52-
53-
[Fact]
54-
public async Task UpdateTenant_WhenTenantDoesNotExists_ShouldReturnNotFound()
55-
{
56-
// Arrange
57-
var unknownTenantId = Faker.Subdomain();
58-
var command = new UpdateTenantCommand { Name = Faker.TenantName() };
59-
60-
// Act
61-
var response = await AuthenticatedHttpClient.PutAsJsonAsync($"/api/account-management/tenants/{unknownTenantId}", command);
62-
63-
//Assert
64-
await response.ShouldHaveErrorStatusCode(HttpStatusCode.NotFound, $"Tenant with id '{unknownTenantId}' not found.");
65-
66-
TelemetryEventsCollectorSpy.AreAllEventsDispatched.Should().BeFalse();
67-
}
6850
}

application/account-management/WebApp/shared/components/accountModals/DeleteAccountConfirmation.tsx renamed to application/account-management/WebApp/routes/admin/account/-components/DeleteAccountConfirmation.tsx

File renamed without changes.

0 commit comments

Comments
 (0)