Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
cf51e54
Enable submit of user search by pressing Enter
tjementum Jun 8, 2025
902aea7
Rename label of user actions in user row from "Menu“ to "User actions"
tjementum Jun 10, 2025
81188d8
Ensure UI state updates on user management when deleting users
tjementum Jun 15, 2025
286ccc7
Change filter icon to list filter for improved user list filtering
tjementum Jun 22, 2025
fe7b223
Rename Invite users button to Invite user
tjementum Jul 1, 2025
edd1f90
Create initial version of user menu
tjementum Jun 29, 2025
d3d39e2
Fix right positioning and overlap, improve layout on UserProfileSidePane
tjementum Jun 29, 2025
c52c488
Apply additional UserProfileSidePane feedback improvements
tjementum Jun 29, 2025
ed9c51d
Fix table height issue preventing e2e tests from passing
tjementum Jun 29, 2025
45d9688
Improve user profile side pane styling and table interaction
tjementum Jun 29, 2025
34817e2
Make user profile side pane always dock on tablet and desktop screens
tjementum Jun 29, 2025
872ee5f
Fix UserProfileSidePane styling issues
tjementum Jun 29, 2025
c69d031
Fix UserProfileSidePane styling and functionality issues
tjementum Jun 29, 2025
858bf4f
Redesign UserProfileSidePane as full-height fixed element with top ba…
tjementum Jun 29, 2025
224d77c
Push main content left when user profile side pane is open
tjementum Jun 29, 2025
ab6d498
Fix horizontal scrolling issue when side pane is open
tjementum Jun 29, 2025
4d27610
Restore Change role functionality in user table action menu
tjementum Jun 29, 2025
88d37ed
Implement grid-based layout for UserProfileSidePane with proper horiz…
tjementum Jun 29, 2025
9db1d86
Fix side pane scrolling to be independent of main content
tjementum Jun 29, 2025
7a0f642
Fix mobile responsive behavior for user profile side pane
tjementum Jun 29, 2025
d114f4b
Extend full-screen overlay behavior to small screens for side pane
tjementum Jun 29, 2025
1d9e068
Fix z-index layering for user profile side pane
tjementum Jun 29, 2025
9b98817
Improve responsive behavior for user filter toolbar
tjementum Jun 29, 2025
6032519
Add dynamic filter layout based on side menu state
tjementum Jun 29, 2025
313b4ed
Fix dynamic filter layout to properly consider side menu expansion state
tjementum Jun 29, 2025
662e3bf
Make side menu z-index responsive to fix mobile layering
tjementum Jun 29, 2025
35872c5
Fix close button in sidepane and align height with top menu
tjementum Jun 29, 2025
156bb14
Collapse filter buttons when there is no space
tjementum Jun 29, 2025
cdf65ac
Make user filter bar buttons collapse and expand depending on availab…
tjementum Jun 30, 2025
b3adddb
Ensure space for filter buttons is calculated when showing/hiding use…
tjementum Jun 30, 2025
86727f6
Remove logic for showing/hiding filter bar buttons based on screen si…
tjementum Jun 30, 2025
b614392
Make Invite user button small when user filter bar is expanded
tjementum Jun 30, 2025
dd94662
Collapse invite users when filter bar is expanded without filters
tjementum Jun 30, 2025
c73bf7c
Show big invite user button on 2xl screens
tjementum Jun 30, 2025
d5f3118
Remove checkboxes in UserTable to enable easy user switching while re…
tjementum Jun 30, 2025
c20919a
Add tooltips to all buttons when displaying only the icon
tjementum Jun 30, 2025
895722e
Translate missing copy
tjementum Jun 30, 2025
9fcc35a
Improve user sorting to prioritize names with email fallback
tjementum Jun 30, 2025
aa2bf1b
Add Owner permission guard to UpdateCurrentTenant backend command
tjementum Jun 30, 2025
f1e5be2
Fix account settings form permissions for non-owner users
tjementum Jun 30, 2025
8790d55
Hide invite user button from non-Owners in UserToolbar
tjementum Jun 30, 2025
f8f2378
Hide bulk delete button from non-Owners in UserToolbar
tjementum Jun 30, 2025
04276ee
Hide delete account button from non-Owners
tjementum Jun 30, 2025
2c4eb02
Change role badge in user side pane to button
tjementum Jun 30, 2025
3de24cc
Add tenant name to JWT and introduce UserInfoFactory for easy reuse
tjementum Jun 28, 2025
2a0bb84
Make side menu resizable and save preferred size in local storage
tjementum Jun 28, 2025
58a3465
Show tenant name in side menu next to logo mark instead of PlatformPl…
tjementum Jun 28, 2025
e146013
Make active menu item bold
tjementum Jun 28, 2025
5a583d0
Fix location of sidemenu toggle button
tjementum Jun 28, 2025
eb1fbfc
Make selected menu item icon slightly bolder when menu is collapsed
tjementum Jun 29, 2025
2d47ffe
Remove server-side rendering checks for window == undefined
tjementum Jun 28, 2025
ba2339c
Move side menu below top menu bar
tjementum Jul 4, 2025
9271064
Always show menu divider line
tjementum Jul 4, 2025
dba8287
Show horizontal divider line and remove blur transition to top menu
tjementum Jul 4, 2025
68ac182
Fix alignment between sidemenu divider and topbar border
tjementum Jul 4, 2025
efd80d7
Make top menu same height as width of collapsed side menu
tjementum Jul 4, 2025
5878ae3
Center side menu toggle button at the intersection of horizontal and …
tjementum Jul 4, 2025
64a5e14
Center Account page and simplify logic for true centering
tjementum Jul 4, 2025
22a9651
Ensure all border colors are consistent in the application
tjementum Jul 4, 2025
eb41a59
Ensure spacing between side menu and main content aligns with applica…
tjementum Jul 4, 2025
d217253
Add deep link to users
tjementum Jul 4, 2025
85872e5
Add API endpoint to get user by ID for deep linking support
tjementum Jul 4, 2025
748f15a
Fetch up-to-date user details for side pane and show warning when dee…
tjementum Jul 4, 2025
7cb309c
Show data freshness warning when user data differs between side pane …
tjementum Jul 4, 2025
d2d0a7f
Reset pagination when filter is changed on users page
tjementum Jul 4, 2025
175e16f
Add skeleton when loading user profile to prevent displaying state da…
tjementum Jul 5, 2025
82305db
Make borders in user table row rounded and change background color on…
tjementum Jul 5, 2025
55aa734
Make borders in user table row rounded and change background color on…
tjementum Jul 6, 2025
822eb54
Fix complexity warnings in SideMenu
tjementum Jul 13, 2025
9deab10
Reduce complexity in UserProfileSidePane and improve accessibility
tjementum Jul 21, 2025
6a3d8a7
Ensure top menu is always hidden on mobile screens
tjementum Jul 21, 2025
8360891
Fix duplicate placeholder comments in translation files
tjementum Jul 21, 2025
d123e87
Add email fallback to change user role aria-label
tjementum Jul 21, 2025
100120c
Restrict Delete and Change role actions to Owner role only
tjementum Jul 21, 2025
4eb556b
Make disabled menu items visually distinct with reduced opacity and m…
tjementum Jul 21, 2025
bcdaeaf
Disable bulk delete button when owner selects themselves
tjementum Jul 21, 2025
9ffccdd
Simplify invite user dialog text with translations
tjementum Jul 21, 2025
3bb62f3
Add aria labels to user toolbar buttons for accessibility
tjementum Jul 21, 2025
574304f
Add aria labels to pagination control for small icons
tjementum Jul 21, 2025
3758fb8
Improve accessibility with aria-labels and centralize tooltip delays
tjementum Jul 21, 2025
b71a08a
Make User Actions column in User table smaller
tjementum Jul 22, 2025
020c69b
Change ChangeUserRole to a normal dialog with OK and Cancel buttons
tjementum Jul 22, 2025
65036f0
Clear selected users when pagination changes
tjementum Jul 22, 2025
f587760
Make AppLayout more accessible by adding main and secondary navigatio…
tjementum Jul 22, 2025
167a3c6
Add minimum width to SearchField in UserQuerying component
tjementum Jul 24, 2025
6d1a1b1
Show disabled delete button for owners viewing their own profile
tjementum Jul 24, 2025
aabaf47
Increase Actions column width in UserTable
tjementum Jul 24, 2025
8c76dbc
Clear userId from URL when deleting a user to prevent Not Found error
tjementum Jul 29, 2025
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 @@ -19,7 +19,7 @@ public void MapEndpoints(IEndpointRouteBuilder routes)
).Produces<TenantResponse>();

group.MapPut("/current", async Task<ApiResult> (UpdateCurrentTenantCommand command, IMediator mediator)
=> await mediator.Send(command)
=> (await mediator.Send(command)).AddRefreshAuthenticationTokens()
);

routes.MapDelete("/internal-api/account-management/tenants/{id}", async Task<ApiResult> (TenantId id, IMediator mediator)
Expand Down
4 changes: 4 additions & 0 deletions application/account-management/Api/Endpoints/UserEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public void MapEndpoints(IEndpointRouteBuilder routes)
=> await mediator.Send(query)
).Produces<UsersResponse>();

group.MapGet("/{id}", async Task<ApiResult<UserDetails>> (UserId id, IMediator mediator)
=> await mediator.Send(new GetUserByIdQuery(id))
).Produces<UserDetails>();

group.MapGet("/summary", async Task<ApiResult<UserSummaryResponse>> (IMediator mediator)
=> await mediator.Send(new GetUserSummaryQuery())
).Produces<UserSummaryResponse>();
Expand Down
3 changes: 2 additions & 1 deletion application/account-management/Core/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@
{
services.AddHttpClient<GravatarClient>(client =>
{
client.BaseAddress = new Uri("https://gravatar.com/");

Check warning on line 26 in application/account-management/Core/Configuration.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Refactor your code not to use hardcoded absolute paths or URIs. (https://rules.sonarsource.com/csharp/RSPEC-1075)

Check warning on line 26 in application/account-management/Core/Configuration.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Refactor your code not to use hardcoded absolute paths or URIs. (https://rules.sonarsource.com/csharp/RSPEC-1075)

Check warning on line 26 in application/account-management/Core/Configuration.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Refactor your code not to use hardcoded absolute paths or URIs. (https://rules.sonarsource.com/csharp/RSPEC-1075)

Check warning on line 26 in application/account-management/Core/Configuration.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Refactor your code not to use hardcoded absolute paths or URIs. (https://rules.sonarsource.com/csharp/RSPEC-1075)
client.Timeout = TimeSpan.FromSeconds(5);
}
);

return services
.AddSharedServices<AccountManagementDbContext>(Assembly)
.AddScoped<AvatarUpdater>();
.AddScoped<AvatarUpdater>()
.AddScoped<UserInfoFactory>();
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using JetBrains.Annotations;
using Mapster;
using PlatformPlatform.AccountManagement.Features.Authentication.Domain;
using PlatformPlatform.AccountManagement.Features.EmailConfirmations.Commands;
using PlatformPlatform.AccountManagement.Features.Users.Domain;
using PlatformPlatform.AccountManagement.Features.Users.Shared;
using PlatformPlatform.AccountManagement.Integrations.Gravatar;
using PlatformPlatform.SharedKernel.Authentication;
using PlatformPlatform.SharedKernel.Authentication.TokenGeneration;
using PlatformPlatform.SharedKernel.Cqrs;
using PlatformPlatform.SharedKernel.Telemetry;
Expand All @@ -19,9 +17,10 @@
public LoginId Id { get; init; } = null!;
}

public sealed class CompleteLoginHandler(

Check warning on line 20 in application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Constructor has 9 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)

Check warning on line 20 in application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Constructor has 9 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)
IUserRepository userRepository,
ILoginRepository loginRepository,
UserInfoFactory userInfoFactory,
AuthenticationTokenService authenticationTokenService,
IMediator mediator,
AvatarUpdater avatarUpdater,
Expand Down Expand Up @@ -65,7 +64,7 @@
var gravatar = await gravatarClient.GetGravatar(user.Id, user.Email, cancellationToken);
if (gravatar is not null)
{
if (await avatarUpdater.UpdateAvatar(user, true, gravatar.ContentType, gravatar.Stream, cancellationToken))

Check warning on line 67 in application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Merge this if statement with the enclosing one. (https://rules.sonarsource.com/csharp/RSPEC-1066)

Check warning on line 67 in application/account-management/Core/Features/Authentication/Commands/CompleteLogin.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Merge this if statement with the enclosing one. (https://rules.sonarsource.com/csharp/RSPEC-1066)
{
events.CollectEvent(new GravatarUpdated(gravatar.Stream.Length));
}
Expand All @@ -75,7 +74,8 @@
login.MarkAsCompleted();
loginRepository.Update(login);

authenticationTokenService.CreateAndSetAuthenticationTokens(user.Adapt<UserInfo>());
var userInfo = await userInfoFactory.CreateUserInfoAsync(user, cancellationToken);
authenticationTokenService.CreateAndSetAuthenticationTokens(userInfo);

events.CollectEvent(new LoginCompleted(user.Id, completeEmailConfirmationResult.Value!.ConfirmationTimeInSeconds));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using JetBrains.Annotations;
using Mapster;
using Microsoft.AspNetCore.Http;
using PlatformPlatform.AccountManagement.Features.Users.Domain;
using PlatformPlatform.SharedKernel.Authentication;
using PlatformPlatform.AccountManagement.Features.Users.Shared;
using PlatformPlatform.SharedKernel.Authentication.TokenGeneration;
using PlatformPlatform.SharedKernel.Cqrs;
using PlatformPlatform.SharedKernel.Domain;
Expand All @@ -17,6 +16,7 @@

public sealed class RefreshAuthenticationTokensHandler(
IUserRepository userRepository,
UserInfoFactory userInfoFactory,
IHttpContextAccessor httpContextAccessor,
AuthenticationTokenService authenticationTokenService,
ITelemetryEventsCollector events,
Expand All @@ -31,7 +31,7 @@
if (!UserId.TryParse(httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
{
logger.LogWarning("No valid 'sub' claim found in refresh token");
return Result.Unauthorized("Invalid refresh token");

Check warning on line 34 in application/account-management/Core/Features/Authentication/Commands/RefreshAuthenticationTokens.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Define a constant instead of using this literal 'Invalid refresh token' 6 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)

Check warning on line 34 in application/account-management/Core/Features/Authentication/Commands/RefreshAuthenticationTokens.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Define a constant instead of using this literal 'Invalid refresh token' 6 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)

Check warning on line 34 in application/account-management/Core/Features/Authentication/Commands/RefreshAuthenticationTokens.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Define a constant instead of using this literal 'Invalid refresh token' 6 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)

Check warning on line 34 in application/account-management/Core/Features/Authentication/Commands/RefreshAuthenticationTokens.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Define a constant instead of using this literal 'Invalid refresh token' 6 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
}

if (!RefreshTokenId.TryParse(httpContext.User.FindFirstValue("rtid"), out var refreshTokenId))
Expand Down Expand Up @@ -75,9 +75,10 @@
return Result.Unauthorized($"No user found with user id {userId} found.");
}

// TODO: Check if the refreshTokenId exists in the database and if the jwtId and refreshTokenVersion are valid

Check warning on line 78 in application/account-management/Core/Features/Authentication/Commands/RefreshAuthenticationTokens.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)

Check warning on line 78 in application/account-management/Core/Features/Authentication/Commands/RefreshAuthenticationTokens.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)

Check warning on line 78 in application/account-management/Core/Features/Authentication/Commands/RefreshAuthenticationTokens.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)

Check warning on line 78 in application/account-management/Core/Features/Authentication/Commands/RefreshAuthenticationTokens.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)

authenticationTokenService.RefreshAuthenticationTokens(user.Adapt<UserInfo>(), refreshTokenId, refreshTokenVersion, refreshTokenExpires);
var userInfo = await userInfoFactory.CreateUserInfoAsync(user, cancellationToken);
authenticationTokenService.RefreshAuthenticationTokens(userInfo, refreshTokenId, refreshTokenVersion, refreshTokenExpires);
events.CollectEvent(new AuthenticationTokensRefreshed());

return Result.Success();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
using JetBrains.Annotations;
using Mapster;
using PlatformPlatform.AccountManagement.Features.EmailConfirmations.Commands;
using PlatformPlatform.AccountManagement.Features.EmailConfirmations.Domain;
using PlatformPlatform.AccountManagement.Features.Tenants.Commands;
using PlatformPlatform.AccountManagement.Features.Users.Domain;
using PlatformPlatform.SharedKernel.Authentication;
using PlatformPlatform.AccountManagement.Features.Users.Shared;
using PlatformPlatform.SharedKernel.Authentication.TokenGeneration;
using PlatformPlatform.SharedKernel.Cqrs;
using PlatformPlatform.SharedKernel.Telemetry;
Expand All @@ -20,6 +19,7 @@ public sealed record CompleteSignupCommand(string OneTimePassword, string Prefer

public sealed class CompleteSignupHandler(
IUserRepository userRepository,
UserInfoFactory userInfoFactory,
AuthenticationTokenService authenticationTokenService,
IMediator mediator,
ITelemetryEventsCollector events
Expand All @@ -42,7 +42,8 @@ public async Task<Result> Handle(CompleteSignupCommand command, CancellationToke
if (!createTenantResult.IsSuccess) return Result.From(createTenantResult);

var user = await userRepository.GetByIdAsync(createTenantResult.Value!.UserId, cancellationToken);
authenticationTokenService.CreateAndSetAuthenticationTokens(user!.Adapt<UserInfo>());
var userInfo = await userInfoFactory.CreateUserInfoAsync(user!, cancellationToken);
authenticationTokenService.CreateAndSetAuthenticationTokens(userInfo);

events.CollectEvent(
new SignupCompleted(createTenantResult.Value.TenantId, completeEmailConfirmationResult.Value!.ConfirmationTimeInSeconds)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using FluentValidation;
using JetBrains.Annotations;
using PlatformPlatform.AccountManagement.Features.Tenants.Domain;
using PlatformPlatform.AccountManagement.Features.Users.Domain;
using PlatformPlatform.SharedKernel.Cqrs;
using PlatformPlatform.SharedKernel.ExecutionContext;
using PlatformPlatform.SharedKernel.Telemetry;

namespace PlatformPlatform.AccountManagement.Features.Tenants.Commands;
Expand All @@ -20,11 +22,19 @@ public UpdateCurrentTenantValidator()
}
}

public sealed class UpdateTenantHandler(ITenantRepository tenantRepository, ITelemetryEventsCollector events)
: IRequestHandler<UpdateCurrentTenantCommand, Result>
public sealed class UpdateTenantHandler(
ITenantRepository tenantRepository,
IExecutionContext executionContext,
ITelemetryEventsCollector events
) : IRequestHandler<UpdateCurrentTenantCommand, Result>
{
public async Task<Result> Handle(UpdateCurrentTenantCommand command, CancellationToken cancellationToken)
{
if (executionContext.UserInfo.Role != UserRole.Owner.ToString())
{
return Result.Forbidden("Only owners are allowed to update tenant information.");
}

var tenant = await tenantRepository.GetCurrentTenantAsync(cancellationToken);

tenant.Update(command.Name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

Task<User[]> GetByIdsAsync(UserId[] ids, CancellationToken cancellationToken);

Task<(User[] Users, int TotalItems, int TotalPages)> Search(

Check warning on line 25 in application/account-management/Core/Features/Users/Domain/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Method has 10 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)

Check warning on line 25 in application/account-management/Core/Features/Users/Domain/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Method has 10 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)
string? search,
UserRole? userRole,
UserStatus? userStatus,
Expand Down Expand Up @@ -103,7 +103,7 @@
return (summary.TotalUsers, summary.ActiveUsers, summary.PendingUsers);
}

public async Task<(User[] Users, int TotalItems, int TotalPages)> Search(

Check warning on line 106 in application/account-management/Core/Features/Users/Domain/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Refactor this method to reduce its Cognitive Complexity from 41 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 106 in application/account-management/Core/Features/Users/Domain/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Refactor this method to reduce its Cognitive Complexity from 41 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
string? search,
UserRole? userRole,
UserStatus? userStatus,
Expand Down Expand Up @@ -158,15 +158,28 @@
? users.OrderBy(u => u.ModifiedAt)
: users.OrderByDescending(u => u.ModifiedAt),
SortableUserProperties.Name => sortOrder == SortOrder.Ascending
? users.OrderBy(u => u.FirstName).ThenBy(u => u.LastName)
: users.OrderByDescending(u => u.FirstName).ThenByDescending(u => u.LastName),
? users.OrderBy(u => u.FirstName == null ? 1 : 0)
.ThenBy(u => u.FirstName)
.ThenBy(u => u.LastName == null ? 1 : 0)
.ThenBy(u => u.LastName)
.ThenBy(u => u.Email)
: users.OrderBy(u => u.FirstName == null ? 0 : 1)
.ThenByDescending(u => u.FirstName)
.ThenBy(u => u.LastName == null ? 0 : 1)
.ThenByDescending(u => u.LastName)
.ThenBy(u => u.Email),
SortableUserProperties.Email => sortOrder == SortOrder.Ascending
? users.OrderBy(u => u.Email)
: users.OrderByDescending(u => u.Email),
SortableUserProperties.Role => sortOrder == SortOrder.Ascending
? users.OrderBy(u => u.Role)
: users.OrderByDescending(u => u.Role),
_ => users
.OrderBy(u => u.FirstName == null ? 1 : 0)
.ThenBy(u => u.FirstName)
.ThenBy(u => u.LastName == null ? 1 : 0)
.ThenBy(u => u.LastName)
.ThenBy(u => u.Email)
};

pageSize ??= 50;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using JetBrains.Annotations;
using Mapster;
using PlatformPlatform.AccountManagement.Features.Users.Domain;
using PlatformPlatform.SharedKernel.Cqrs;
using PlatformPlatform.SharedKernel.Domain;

namespace PlatformPlatform.AccountManagement.Features.Users.Queries;

[PublicAPI]
public sealed record GetUserByIdQuery(UserId Id) : IRequest<Result<UserDetails>>;

public sealed class GetUserByIdHandler(IUserRepository userRepository)
: IRequestHandler<GetUserByIdQuery, Result<UserDetails>>
{
public async Task<Result<UserDetails>> Handle(GetUserByIdQuery query, CancellationToken cancellationToken)
{
var user = await userRepository.GetByIdAsync(query.Id, cancellationToken);

if (user is null)
{
return Result<UserDetails>.NotFound($"User with ID '{query.Id}' not found.");
}

return user.Adapt<UserDetails>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using PlatformPlatform.AccountManagement.Features.Tenants.Domain;
using PlatformPlatform.AccountManagement.Features.Users.Domain;
using PlatformPlatform.SharedKernel.Authentication;

namespace PlatformPlatform.AccountManagement.Features.Users.Shared;

/// <summary>
/// Factory for creating UserInfo instances with tenant information.
/// Centralizes the logic for creating UserInfo to follow SRP and avoid duplication.
/// </summary>
public sealed class UserInfoFactory(ITenantRepository tenantRepository)
{
/// <summary>
/// Creates a UserInfo instance from a User entity, including tenant name.
/// </summary>
/// <param name="user">The user entity</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>UserInfo with all required properties including tenant name</returns>
public async Task<UserInfo> CreateUserInfoAsync(User user, CancellationToken cancellationToken)
{
var tenant = await tenantRepository.GetByIdAsync(user.TenantId, cancellationToken);

return new UserInfo
{
IsAuthenticated = true,
Id = user.Id,
TenantId = user.TenantId,
Role = user.Role.ToString(),
Email = user.Email,
FirstName = user.FirstName,
LastName = user.LastName,
Title = user.Title,
AvatarUrl = user.Avatar.Url,
TenantName = tenant?.Name,
Locale = user.Locale
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,19 @@ public async Task UpdateCurrentTenant_WhenInvalid_ShouldReturnBadRequest()

TelemetryEventsCollectorSpy.AreAllEventsDispatched.Should().BeFalse();
}

[Fact]
public async Task UpdateCurrentTenant_WhenNonOwner_ShouldReturnForbidden()
{
// Arrange
var command = new UpdateCurrentTenantCommand { Name = Faker.TenantName() };

// Act
var response = await AuthenticatedMemberHttpClient.PutAsJsonAsync("/api/account-management/tenants/current", command);

// Assert
await response.ShouldHaveErrorStatusCode(HttpStatusCode.Forbidden, "Only owners are allowed to update tenant information.");

TelemetryEventsCollectorSpy.AreAllEventsDispatched.Should().BeFalse();
}
}
85 changes: 85 additions & 0 deletions application/account-management/Tests/Users/GetUserByIdTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Net;
using System.Text.Json;
using FluentAssertions;
using PlatformPlatform.AccountManagement.Database;
using PlatformPlatform.AccountManagement.Features.Users.Domain;
using PlatformPlatform.AccountManagement.Features.Users.Queries;
using PlatformPlatform.SharedKernel.Domain;
using PlatformPlatform.SharedKernel.Tests;
using PlatformPlatform.SharedKernel.Tests.Persistence;
using Xunit;

namespace PlatformPlatform.AccountManagement.Tests.Users;

public sealed class GetUserByIdTests : EndpointBaseTest<AccountManagementDbContext>
{
private readonly UserId _userId = UserId.NewId();

public GetUserByIdTests()
{
Connection.Insert("Users", [
("TenantId", DatabaseSeeder.Tenant1.Id.ToString()),
("Id", _userId.ToString()),
("CreatedAt", TimeProvider.System.GetUtcNow().AddMinutes(-10)),
("ModifiedAt", null),
("Email", Faker.Internet.Email()),
("FirstName", Faker.Name.FirstName()),
("LastName", Faker.Name.LastName()),
("Title", Faker.Name.JobTitle()),
("Role", UserRole.Member.ToString()),
("EmailConfirmed", true),
("Avatar", JsonSerializer.Serialize(new Avatar())),
("Locale", "en-US")
]
);
}

[Fact]
public async Task GetUserById_WhenUserExists_ShouldReturnUserDetails()
{
// Act
var response = await AuthenticatedOwnerHttpClient.GetAsync($"/api/account-management/users/{_userId}");

// Assert
response.ShouldBeSuccessfulGetRequest();
var userDetails = await response.DeserializeResponse<UserDetails>();
userDetails.Should().NotBeNull();
userDetails.Id.Should().Be(_userId);
}

[Fact]
public async Task GetUserById_WhenUserDoesNotExist_ShouldReturnNotFound()
{
// Arrange
var nonExistentUserId = UserId.NewId();

// Act
var response = await AuthenticatedOwnerHttpClient.GetAsync($"/api/account-management/users/{nonExistentUserId}");

// Assert
await response.ShouldHaveErrorStatusCode(HttpStatusCode.NotFound, $"User with ID '{nonExistentUserId}' not found.");
}

[Fact]
public async Task GetUserById_WhenMemberTriesToAccessOtherUser_ShouldSucceed()
{
// Act
var response = await AuthenticatedMemberHttpClient.GetAsync($"/api/account-management/users/{_userId}");

// Assert
response.ShouldBeSuccessfulGetRequest();
var userDetails = await response.DeserializeResponse<UserDetails>();
userDetails.Should().NotBeNull();
userDetails.Id.Should().Be(_userId);
}

[Fact]
public async Task GetUserById_WhenNotAuthenticated_ShouldReturnUnauthorized()
{
// Act
var response = await AnonymousHttpClient.GetAsync($"/api/account-management/users/{_userId}");

// Assert
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
}
Loading
Loading