diff --git a/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs index c58aac9..b9d7521 100644 --- a/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs @@ -1,4 +1,4 @@ -using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Extensions; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Errors; @@ -13,22 +13,29 @@ public sealed class SetUserLockoutEndpoint : IEndpoint { builder.MapPut("api/users/{userId:Guid}/session/lockout/{disable:bool}", async (Guid userId, bool disable, HttpContext context, IUserService service, CancellationToken cancellationToken = default) => - await service.SetUserLockoutAsync(userId, context.GetUserId(), disable, cancellationToken) switch - { - { IsSuccess: true } => Results.Ok(), - var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), - var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), - var r => Results.InternalServerError(r.Errors.ToProblems()) - }) - .WithDescription("Lock user session") - .RequireAuthorization(Constants.MainScope) - .WithName(typeof(SetUserLockoutEndpoint).ToEndpointName()) - .MapToApiVersion(new ApiVersion(1)) - .Produces(StatusCodes.Status200OK) - .Produces(StatusCodes.Status401Unauthorized) - .Produces(StatusCodes.Status404NotFound) - .Produces(StatusCodes.Status400BadRequest) - .Produces(StatusCodes.Status500InternalServerError) - .WithTags(EndpointTags.Users); + { + var (_, role) = context.User.GetUserRole(); + if (role != null && role.Equals("Partner", StringComparison.OrdinalIgnoreCase)) + return Results.Forbid(); + + return await service.SetUserLockoutAsync(userId, context.GetUserId(), disable, cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }; + }) + .WithDescription("Lock user session") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(SetUserLockoutEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Users); } } diff --git a/PostFundManagement.Api/Program.cs b/PostFundManagement.Api/Program.cs index 6bbab29..c44f968 100644 --- a/PostFundManagement.Api/Program.cs +++ b/PostFundManagement.Api/Program.cs @@ -29,7 +29,7 @@ builder.Services.AddApiVersioning(options => builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddTransient(); +builder.Services.AddScoped(); builder.Services.AddEndpoints(Assembly.GetExecutingAssembly()); builder.Services.AddApiServices(builder.Configuration); @@ -218,4 +218,4 @@ app.MapGet("/", () => Results.Redirect($"/openapi/{latestVersionGroup}")) if (!app.Environment.IsDevelopment()) app.UseExceptionHandler("/Error", createScopeForErrors: true); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/PostFundManagement.Application/Extensions/General.cs b/PostFundManagement.Application/Extensions/General.cs index ec14c53..91e7fdb 100644 --- a/PostFundManagement.Application/Extensions/General.cs +++ b/PostFundManagement.Application/Extensions/General.cs @@ -1,4 +1,4 @@ -namespace PostFundManagement.Application.Extensions; +namespace PostFundManagement.Application.Extensions; public static class General { @@ -16,7 +16,8 @@ public static class General public static (bool Decoded, Guid userId) GetUserId(this ClaimsPrincipal principal) { - var subClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value ?? principal.FindFirst(ClaimTypes.NameIdentifier)?.Value; + var subClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value + ?? principal.FindFirst(ClaimTypes.NameIdentifier)?.Value; var decoded = Guid.TryParse(subClaim, out var userId); @@ -25,20 +26,34 @@ public static class General public static (bool Decoded, string email) GetUserEmail(this ClaimsPrincipal principal) { - var emailClaim = principal.FindFirst("email")?.Value ?? principal.FindFirst(ClaimTypes.Email)?.Value; + var emailClaim = principal.FindFirst("email")?.Value + ?? principal.FindFirst(ClaimTypes.Email)?.Value + ?? principal.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value + ?? principal.FindFirst("preferred_username")?.Value + ?? principal.FindFirst("name")?.Value + ?? principal.FindFirst(ClaimTypes.Name)?.Value; var decoded = !string.IsNullOrWhiteSpace(emailClaim); - return (decoded, emailClaim!); + return (decoded, emailClaim ?? string.Empty); } public static (bool Decoded, string role) GetUserRole(this ClaimsPrincipal principal) { - var roleClaim = principal.FindFirst("role")?.Value ?? principal.FindFirst(ClaimTypes.Role)?.Value; + var roleClaims = principal.FindAll("role") + .Concat(principal.FindAll(ClaimTypes.Role)) + .Concat(principal.FindAll("http://schemas.microsoft.com/ws/2008/06/identity/claims/role")) + .Concat(principal.FindAll("roles")) + .Select(c => c.Value) + .Where(v => !string.IsNullOrWhiteSpace(v)) + .ToList(); - var decoded = !string.IsNullOrWhiteSpace(roleClaim); + var partnerRole = roleClaims.FirstOrDefault(r => r.Equals("Partner", StringComparison.OrdinalIgnoreCase)); + var role = partnerRole ?? roleClaims.FirstOrDefault(); - return (decoded, roleClaim!); + var decoded = !string.IsNullOrWhiteSpace(role); + + return (decoded, role ?? string.Empty); } public static Microsoft.AspNetCore.Mvc.ProblemDetails ToProblems(this IReadOnlyList errors, int statusCode = StatusCodes.Status400BadRequest) => new() diff --git a/PostFundManagement.PartnerUI/src/auth/AuthProvider.tsx b/PostFundManagement.PartnerUI/src/auth/AuthProvider.tsx index 6eee598..a933afc 100644 --- a/PostFundManagement.PartnerUI/src/auth/AuthProvider.tsx +++ b/PostFundManagement.PartnerUI/src/auth/AuthProvider.tsx @@ -17,7 +17,6 @@ import { AccountActivationModal } from '../components/auth/AccountActivationModa import { NonPartnerAccessDeniedModal } from '../components/auth/NonPartnerAccessDeniedModal'; import { SessionExpiredModal } from '../components/auth/SessionExpiredModal'; import { encodeHashIdSync } from '../services/toolsService'; -import { verifyTenantLockout, clearTenantLockoutCache } from '../services/tenantLockoutService'; import { onSessionExpired, triggerSessionExpired, @@ -510,35 +509,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => return; } - // Step 3: Verify tenant lockout using the HashId-encoded tenant ID - const effectiveTenantId = - parsedTenantId ?? - syncResult.data?.tenantId ?? - syncResult.data?.organisationId ?? - tenantClaimValues.tenantId; - - const tenantLockoutResult = await verifyTenantLockout(effectiveTenantId, tenantClaim); - - if (!tenantLockoutResult.isActive) { - setIsPendingActivation(true); - setIsSynced(false); - setActivationError(tenantLockoutResult.errorMessage || 'Tenant is inactive or not found'); - setIsLoading(false); - return; - } - - if (tenantLockoutResult.tenant?.name) { - profile.tenant = tenantLockoutResult.tenant.name; - setTenantName(tenantLockoutResult.tenant.name); - } - if (tenantLockoutResult.tenant?.id !== undefined && !profile.tenant_id) { - const numericTid = Number(tenantLockoutResult.tenant.id); - if (!isNaN(numericTid) && numericTid > 0) { - profile.tenant_id = numericTid; - setTenantId(numericTid); - } - } - setUser({ ...profile }); localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(profile)); @@ -669,39 +639,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => return; } - // Step 3: Verify tenant lockout using HashId-encoded tenant ID - const effectiveTenantId = - storedTenantId ?? - syncResult.data?.tenantId ?? - syncResult.data?.organisationId ?? - tenantClaimValues.tenantId; - - const tenantLockoutResult = await verifyTenantLockout( - effectiveTenantId, - storedTenantClaim, - hadPendingActivation - ); - - if (!tenantLockoutResult.isActive) { - setIsPendingActivation(true); - setIsSynced(false); - setActivationError(tenantLockoutResult.errorMessage || 'Tenant is inactive or not found'); - return; - } - - if (tenantLockoutResult.tenant?.name) { - if (resolvedUser) { - resolvedUser.tenant = tenantLockoutResult.tenant.name; - } - setTenantName(tenantLockoutResult.tenant.name); - } - if (tenantLockoutResult.tenant?.id !== undefined && resolvedUser && !resolvedUser.tenant_id) { - const numericTid = Number(tenantLockoutResult.tenant.id); - if (!isNaN(numericTid) && numericTid > 0) { - resolvedUser.tenant_id = numericTid; - setTenantId(numericTid); - } - } if (resolvedUser) { setUser({ ...resolvedUser }); localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(resolvedUser)); @@ -746,8 +683,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => const syncUserSession = async (force = true): Promise => { try { await loadRuntimeConfig(); - clearTenantLockoutCache(); - if (sub) { + if (sub) { clearCachedUserRbac(sub); } @@ -793,41 +729,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(resolved.claims)); } catch {} - const tenantClaimValues = extractTenantClaimValues(claims, accessToken); - const effectiveTenantId = - currentTenantId ?? - result.data?.tenantId ?? - result.data?.organisationId ?? - tenantClaimValues.tenantId; - const effectiveTenantClaim = currentTenantClaim || tenantClaimValues.tenant || undefined; - - // Step 3: Verify tenant lockout using HashId-encoded tenant ID - const tenantLockoutResult = await verifyTenantLockout( - effectiveTenantId, - effectiveTenantClaim, - true - ); - if (!tenantLockoutResult.isActive) { - const msg = tenantLockoutResult.errorMessage || 'Tenant is not Active. Lockout active.'; - setIsPendingActivation(true); - setIsSynced(false); - setActivationError(msg); - return { - success: false, - isActivationPending: true, - errorMessage: msg, - }; - } - - if (tenantLockoutResult.tenant?.name) { - setTenantName(tenantLockoutResult.tenant.name); - } - if (tenantLockoutResult.tenant?.id !== undefined && !tenantId) { - const numericTid = Number(tenantLockoutResult.tenant.id); - if (!isNaN(numericTid) && numericTid > 0) { - setTenantId(numericTid); - } - } + // Partner user verified successfully setActivationPending(false); setIsPendingActivation(false); @@ -873,8 +775,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => localStorage.removeItem(LOCAL_STORAGE_SUB_KEY); localStorage.removeItem(LOCAL_STORAGE_CLAIMS_KEY); clearUserSyncInBrowser(); - clearTenantLockoutCache(); - setUser(null); + setUser(null); setSub(''); setClaims({}); setTenantId(null); diff --git a/PostFundManagement.PartnerUI/src/services/api/index.ts b/PostFundManagement.PartnerUI/src/services/api/index.ts index 7d36d91..1dabe7a 100644 --- a/PostFundManagement.PartnerUI/src/services/api/index.ts +++ b/PostFundManagement.PartnerUI/src/services/api/index.ts @@ -1585,13 +1585,7 @@ export const deleteUserClaim = async (targetUserId: string, claimId: string | nu await apiClient.delete(`/v1/api/users/${targetUserId}/claims/${claimId}`); }; -export const forceLogoutUser = async (userId: string): Promise => { - await apiClient.put(`/v1/api/users/${userId}/session/logout`); -}; - -export const setUserLockout = async (userId: string, disable: boolean): Promise => { - await apiClient.put(`/v1/api/users/${userId}/session/lockout/${disable}`); -}; +// Administrative functions (forceLogoutUser, setUserLockout) removed from PartnerUI import { syncUserAccount, diff --git a/PostFundManagement.PartnerUI/src/services/userSyncService.ts b/PostFundManagement.PartnerUI/src/services/userSyncService.ts index 36617ec..591b332 100644 --- a/PostFundManagement.PartnerUI/src/services/userSyncService.ts +++ b/PostFundManagement.PartnerUI/src/services/userSyncService.ts @@ -189,19 +189,7 @@ async function executeBackendSync( userSub?: string, profile?: UserSyncProfile ): Promise<{ endpoint: string; data?: any }> { - const rawTenantId = profile?.tenant_id ?? (profile as any)?.tenantId; - const parsedTenantId = - rawTenantId !== undefined && rawTenantId !== null && rawTenantId !== '' - ? Number(rawTenantId) - : undefined; - - const resolvedOrgId = - parsedTenantId !== undefined - ? parsedTenantId - : profile?.organisationId !== undefined && profile?.organisationId !== null && profile?.organisationId !== '' - ? Number(profile.organisationId) - : undefined; - + const endpoint = '/v1/api/integration/sync'; const payload = { id: userSub || profile?.id || profile?.sub, sub: userSub || profile?.sub, @@ -209,40 +197,25 @@ async function executeBackendSync( name: profile?.name || undefined, role: profile?.role || undefined, tenant: profile?.tenant || undefined, - tenant_id: parsedTenantId, - organisationId: resolvedOrgId, + tenant_id: profile?.tenant_id, + organisationId: profile?.organisationId, }; - let lastError: any = null; - - for (const endpoint of USER_SYNC_CANDIDATE_ENDPOINTS) { - try { - let res; - if (endpoint === '/v1/api/integration/sync') { - try { - res = await apiClient.post(endpoint, payload); - } catch (postErr: any) { - const errStatus = postErr?.response?.status; - if (errStatus === 400 || errStatus === 415 || errStatus === 422) { - res = await apiClient.post(endpoint, {}); - } else { - throw postErr; - } - } - } else { - res = await apiClient.post(endpoint, payload); - } - return { endpoint, data: res?.data }; - } catch (err: any) { - lastError = err; - const status = err?.response?.status; - if (status === 401 || status === 403 || status === 423) { + try { + const res = await apiClient.post(endpoint, payload); + return { endpoint, data: res?.data }; + } catch (err: any) { + const status = err?.response?.status; + if (status === 400 || status === 415 || status === 422) { + try { + const resFallback = await apiClient.post(endpoint, {}); + return { endpoint, data: resFallback?.data }; + } catch (fallbackErr) { throw err; } } + throw err; } - - throw lastError || new Error('Failed to reach user sync endpoint'); } export async function syncUserAccount(