Skip to content

Commit 07ce145

Browse files
committed
feat(auth): Implement UMS authentication with OWASP compliance
Authentication Implementation: - Backend: Login/logout/refresh/session endpoints (AuthEndpoints.cs) - JWT token service with tenant propagation - Refresh token rotation (60min access / 7day refresh) - DevAuthMiddleware gated to development only Frontend: - M3 login screen with tenant-first flow - Auth store with Zustand (refresh token rotation, activity tracking) - ProtectedRoute component for route guards - ConnectedUserDrawer for session visibility - TenantSelect and SearchableSelect components - Enhanced useFormValidation hook with field-level errors Infrastructure: - Prettier configuration with ESLint integration - M3DataView split into smaller components (DataTable, PaginationControls, FilterBar, SortDropdown) - Formatting utilities (date, number, currency, percentage) - Storybook setup with essential addons - E2E tests (auth, navigation, profile-panel) All tasks completed: - Polly retry policies (already configured for SQL) - EF indexes (already in place) - OpenTelemetry (already configured)
1 parent 388b5d3 commit 07ce145

116 files changed

Lines changed: 19630 additions & 13628 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/apps/ums.api/Ums.Application/Authorization/Profile/DTOs/ProfileDto.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,20 @@ namespace Ums.Application.Authorization.Profile.DTOs;
33
public sealed record ProfileDto(
44
Guid ProfileId,
55
Guid TenantId,
6+
string TenantCode,
7+
string TenantName,
68
Guid UserId,
9+
string UserEmail,
710
Guid RoleId,
11+
string RoleCode,
12+
string RoleName,
13+
Guid SystemSuiteId,
14+
string SystemSuiteCode,
15+
string SystemSuiteName,
816
Guid? BranchId,
17+
string? BranchName,
918
string Scope,
1019
bool IsActive,
20+
int PermissionCount,
1121
IReadOnlyList<ProfilePermissionDto> Permissions);
1222

src/apps/ums.api/Ums.Application/Authorization/Profile/Queries/GetAllProfilesQueryHandler.cs

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
using Ums.Application.Authorization.Profile.DTOs;
22
using Ums.Domain.Authorization;
3+
using Ums.Domain.Identity;
4+
using Ums.Domain.Identity.UserAccount;
35
using static Ums.Application.Common.QueryRequestNormalizer;
46

57
namespace Ums.Application.Authorization.Profile.Queries;
68

79
public sealed class GetAllProfilesQueryHandler : IQueryHandler<GetAllProfilesQuery, PagedResult<ProfileDto>>
810
{
911
private readonly IProfileRepository _profileRepository;
12+
private readonly IRoleRepository _roleRepository;
13+
private readonly ISystemSuiteRepository _systemSuiteRepository;
14+
private readonly ITenantRepository _tenantRepository;
15+
private readonly IUserAccountRepository _userAccountRepository;
1016

11-
public GetAllProfilesQueryHandler(IProfileRepository profileRepository)
17+
public GetAllProfilesQueryHandler(
18+
IProfileRepository profileRepository,
19+
IRoleRepository roleRepository,
20+
ISystemSuiteRepository systemSuiteRepository,
21+
ITenantRepository tenantRepository,
22+
IUserAccountRepository userAccountRepository)
1223
{
1324
_profileRepository = profileRepository;
25+
_roleRepository = roleRepository;
26+
_systemSuiteRepository = systemSuiteRepository;
27+
_tenantRepository = tenantRepository;
28+
_userAccountRepository = userAccountRepository;
1429
}
1530

1631
[LoggerAspect(Type = typeof(IUmsLogger), LogDuration = true, LogException = true, LogArguments = [])]
@@ -33,15 +48,57 @@ public async Task<Result<PagedResult<ProfileDto>>> Handle(
3348
? await _profileRepository.GetByTenantIdAsync(request.TenantId.Value, cancellationToken)
3449
: await _profileRepository.GetAllAsync(cancellationToken);
3550

36-
var query = profiles.Select(p => new ProfileDto(
37-
p.Props.Id.GetValue(),
38-
p.Props.TenantId.GetValue(),
39-
p.Props.UserId.GetValue(),
40-
p.Props.RoleId.GetValue(),
41-
p.Props.BranchId?.GetValue(),
42-
p.Props.Scope.ToString(),
43-
p.Props.IsActive,
44-
new List<ProfilePermissionDto>()));
51+
var allTenants = await _tenantRepository.GetAllAsync(cancellationToken);
52+
var profileRoleIds = profiles.Select(p => p.Props.RoleId.GetValue()).Distinct().ToList();
53+
var profileTenantIds = profiles.Select(p => p.Props.TenantId.GetValue()).Distinct().ToList();
54+
55+
var allRoles = new List<Ums.Domain.Authorization.Role.Role>();
56+
foreach (var tenantId in profileTenantIds)
57+
{
58+
var tenantRoles = await _roleRepository.GetByTenantIdAsync(tenantId, cancellationToken);
59+
allRoles.AddRange(tenantRoles);
60+
}
61+
62+
var allUsers = await _userAccountRepository.GetAllAsync(cancellationToken);
63+
64+
var roleSystemSuiteIds = allRoles.Select(r => r.Props.SystemSuiteId.GetValue()).Distinct().ToList();
65+
var allSuites = new List<Ums.Domain.Authorization.SystemSuite.SystemSuite>();
66+
foreach (var suiteId in roleSystemSuiteIds)
67+
{
68+
var suite = await _systemSuiteRepository.GetByIdAsync(suiteId, cancellationToken);
69+
if (suite != null) allSuites.Add(suite);
70+
}
71+
72+
var tenantLookup = allTenants.ToDictionary(t => t.Props.Id.GetValue(), t => (Code: t.Props.Code.GetValue(), Name: t.Props.Name.GetValue()));
73+
var roleLookup = allRoles.ToDictionary(r => r.Props.Id.GetValue(), r => (Code: r.Props.Code.GetValue(), Name: r.Props.Value.GetValue(), SystemSuiteId: r.Props.SystemSuiteId.GetValue()));
74+
var userLookup = allUsers.ToDictionary(u => u.Props.Id.GetValue(), u => u.Props.Email.GetValue());
75+
var suiteLookup = allSuites.ToDictionary(s => s.Props.Id.GetValue(), s => (Code: s.Props.Code.GetValue(), Name: s.Props.Name.GetValue()));
76+
77+
var query = profiles.Select(p =>
78+
{
79+
var roleInfo = roleLookup.GetValueOrDefault(p.Props.RoleId.GetValue());
80+
var suiteInfo = suiteLookup.GetValueOrDefault(roleInfo.SystemSuiteId);
81+
var permCount = p.Permissions.Count;
82+
return new ProfileDto(
83+
p.Props.Id.GetValue(),
84+
p.Props.TenantId.GetValue(),
85+
tenantLookup.GetValueOrDefault(p.Props.TenantId.GetValue()).Code,
86+
tenantLookup.GetValueOrDefault(p.Props.TenantId.GetValue()).Name,
87+
p.Props.UserId.GetValue(),
88+
userLookup.GetValueOrDefault(p.Props.UserId.GetValue()) ?? "—",
89+
p.Props.RoleId.GetValue(),
90+
roleInfo.Code ?? "—",
91+
roleInfo.Name ?? "—",
92+
roleInfo.SystemSuiteId,
93+
suiteInfo.Code ?? "—",
94+
suiteInfo.Name ?? "—",
95+
p.Props.BranchId?.GetValue(),
96+
null,
97+
p.Props.Scope.ToString(),
98+
p.Props.IsActive,
99+
permCount,
100+
new List<ProfilePermissionDto>());
101+
});
45102

46103
if (!string.Equals(status, "all", StringComparison.OrdinalIgnoreCase))
47104
{
@@ -53,19 +110,27 @@ public async Task<Result<PagedResult<ProfileDto>>> Handle(
53110
{
54111
query = criteria switch
55112
{
56-
"id" => query.Where(p => p.ProfileId.ToString().Contains(search, StringComparison.OrdinalIgnoreCase)),
57-
"roleid" => query.Where(p => p.RoleId.ToString().Contains(search, StringComparison.OrdinalIgnoreCase)),
58-
_ => query.Where(p => p.UserId.ToString().Contains(search, StringComparison.OrdinalIgnoreCase)),
113+
"user" => query.Where(p => p.UserEmail.Contains(search, StringComparison.OrdinalIgnoreCase)),
114+
"role" => query.Where(p => p.RoleName.Contains(search, StringComparison.OrdinalIgnoreCase) || p.RoleCode.Contains(search, StringComparison.OrdinalIgnoreCase)),
115+
"tenant" => query.Where(p => p.TenantName.Contains(search, StringComparison.OrdinalIgnoreCase) || p.TenantCode.Contains(search, StringComparison.OrdinalIgnoreCase)),
116+
"system" => query.Where(p => p.SystemSuiteName.Contains(search, StringComparison.OrdinalIgnoreCase) || p.SystemSuiteCode.Contains(search, StringComparison.OrdinalIgnoreCase)),
117+
_ => query.Where(p => p.UserEmail.Contains(search, StringComparison.OrdinalIgnoreCase)),
59118
};
60119
}
61120

62121
query = (sortBy, sortOrder) switch
63122
{
64123
("scope", "desc") => query.OrderByDescending(p => p.Scope),
65124
("scope", _) => query.OrderBy(p => p.Scope),
66-
("roleid", "desc") => query.OrderByDescending(p => p.RoleId),
67-
("roleid", _) => query.OrderBy(p => p.RoleId),
68-
_ => query.OrderBy(p => p.UserId),
125+
("role", "desc") => query.OrderByDescending(p => p.RoleName),
126+
("role", _) => query.OrderBy(p => p.RoleName),
127+
("user", "desc") => query.OrderByDescending(p => p.UserEmail),
128+
("user", _) => query.OrderBy(p => p.UserEmail),
129+
("tenant", "desc") => query.OrderByDescending(p => p.TenantName),
130+
("tenant", _) => query.OrderBy(p => p.TenantName),
131+
("system", "desc") => query.OrderByDescending(p => p.SystemSuiteName),
132+
("system", _) => query.OrderBy(p => p.SystemSuiteName),
133+
_ => query.OrderBy(p => p.UserEmail),
69134
};
70135

71136
var totalItems = query.Count();

src/apps/ums.api/Ums.Application/Authorization/Profile/Queries/GetProfileByIdQueryHandler.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using Ums.Application.Authorization.Profile.DTOs;
22
using Ums.Domain.Authorization;
33
using Ums.Domain.Authorization.Profile;
4+
using Ums.Domain.Identity;
5+
using Ums.Domain.Identity.UserAccount;
46
using SystemSuiteAggregate = Ums.Domain.Authorization.SystemSuite.SystemSuite;
57

68
namespace Ums.Application.Authorization.Profile.Queries;
@@ -10,15 +12,21 @@ public sealed class GetProfileByIdQueryHandler : IQueryHandler<GetProfileByIdQue
1012
private readonly IProfileRepository _profileRepository;
1113
private readonly IRoleRepository _roleRepository;
1214
private readonly ISystemSuiteRepository _systemSuiteRepository;
15+
private readonly ITenantRepository _tenantRepository;
16+
private readonly IUserAccountRepository _userAccountRepository;
1317

1418
public GetProfileByIdQueryHandler(
1519
IProfileRepository profileRepository,
1620
IRoleRepository roleRepository,
17-
ISystemSuiteRepository systemSuiteRepository)
21+
ISystemSuiteRepository systemSuiteRepository,
22+
ITenantRepository tenantRepository,
23+
IUserAccountRepository userAccountRepository)
1824
{
1925
_profileRepository = profileRepository;
2026
_roleRepository = roleRepository;
2127
_systemSuiteRepository = systemSuiteRepository;
28+
_tenantRepository = tenantRepository;
29+
_userAccountRepository = userAccountRepository;
2230
}
2331

2432
[LoggerAspect(Type = typeof(IUmsLogger), LogDuration = true, LogException = true, LogArguments = [])]
@@ -33,18 +41,17 @@ public async Task<Result<ProfileDto>> Handle(
3341
return Result<ProfileDto>.Failure("Profile not found.");
3442
}
3543

36-
// Resolve SystemSuite through Role to fetch target and action names
3744
var role = await _roleRepository.GetByIdAsync(profile.Props.RoleId.GetValue(), cancellationToken);
3845
var suite = role is null ? null : await _systemSuiteRepository.GetByIdAsync(role.Props.SystemSuiteId.GetValue(), cancellationToken);
46+
var tenant = await _tenantRepository.GetByIdAsync(profile.Props.TenantId.GetValue(), cancellationToken);
47+
var user = await _userAccountRepository.GetByIdAsync(profile.Props.UserId.GetValue(), cancellationToken);
3948

40-
// Build flat action lookup: actionId → actionName
4149
var actionLookup = suite is null
4250
? new Dictionary<Guid, string>()
4351
: suite.Actions.ToDictionary(
4452
a => a.GetId().GetValue(),
4553
a => a.Name.GetValue());
4654

47-
// Build flat target lookup: id → name
4855
var targetLookup = BuildTargetNameLookup(suite);
4956

5057
var permissions = profile.Permissions
@@ -66,11 +73,21 @@ public async Task<Result<ProfileDto>> Handle(
6673
return Result<ProfileDto>.Success(new ProfileDto(
6774
profile.Props.Id.GetValue(),
6875
profile.Props.TenantId.GetValue(),
76+
tenant?.Props.Code.GetValue() ?? "—",
77+
tenant?.Props.Name.GetValue() ?? "—",
6978
profile.Props.UserId.GetValue(),
79+
user?.Props.Email.GetValue() ?? "—",
7080
profile.Props.RoleId.GetValue(),
81+
role?.Props.Code.GetValue() ?? "—",
82+
role?.Props.Value.GetValue() ?? "—",
83+
role?.Props.SystemSuiteId.GetValue() ?? Guid.Empty,
84+
suite?.Props.Code.GetValue() ?? "—",
85+
suite?.Props.Name.GetValue() ?? "—",
7186
profile.Props.BranchId?.GetValue(),
87+
null,
7288
profile.Props.Scope.ToString(),
7389
profile.Props.IsActive,
90+
permissions.Count,
7491
permissions));
7592
}
7693

0 commit comments

Comments
 (0)