Removed setUserLockout from PartnerUI

This commit is contained in:
2026-10-24 11:39:47 +02:00
parent 3316fc0275
commit 7220700bac
6 changed files with 67 additions and 177 deletions
@@ -1,4 +1,4 @@
using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Extensions;
using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Errors; 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, builder.MapPut("api/users/{userId:Guid}/session/lockout/{disable:bool}", async (Guid userId, bool disable, HttpContext context,
IUserService service, CancellationToken cancellationToken = default) => IUserService service, CancellationToken cancellationToken = default) =>
await service.SetUserLockoutAsync(userId, context.GetUserId(), disable, cancellationToken) switch {
{ var (_, role) = context.User.GetUserRole();
{ IsSuccess: true } => Results.Ok(), if (role != null && role.Equals("Partner", StringComparison.OrdinalIgnoreCase))
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()), return Results.Forbid();
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems()) return await service.SetUserLockoutAsync(userId, context.GetUserId(), disable, cancellationToken) switch
}) {
.WithDescription("Lock user session") { IsSuccess: true } => Results.Ok(),
.RequireAuthorization(Constants.MainScope) var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
.WithName(typeof(SetUserLockoutEndpoint).ToEndpointName()) var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
.MapToApiVersion(new ApiVersion(1)) var r => Results.InternalServerError(r.Errors.ToProblems())
.Produces(StatusCodes.Status200OK) };
.Produces(StatusCodes.Status401Unauthorized) })
.Produces<Problems>(StatusCodes.Status404NotFound) .WithDescription("Lock user session")
.Produces<Problems>(StatusCodes.Status400BadRequest) .RequireAuthorization(Constants.MainScope)
.Produces<Problems>(StatusCodes.Status500InternalServerError) .WithName(typeof(SetUserLockoutEndpoint).ToEndpointName())
.WithTags(EndpointTags.Users); .MapToApiVersion(new ApiVersion(1))
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces<Problems>(StatusCodes.Status404NotFound)
.Produces<Problems>(StatusCodes.Status400BadRequest)
.Produces<Problems>(StatusCodes.Status500InternalServerError)
.WithTags(EndpointTags.Users);
} }
} }
+2 -2
View File
@@ -29,7 +29,7 @@ builder.Services.AddApiVersioning(options =>
builder.Services.AddScoped<ActiveUserFilter>(); builder.Services.AddScoped<ActiveUserFilter>();
builder.Services.AddScoped<UserRoleFilter>(); builder.Services.AddScoped<UserRoleFilter>();
builder.Services.AddTransient<ITenantContextService, TenantContextService>(); builder.Services.AddScoped<ITenantContextService, TenantContextService>();
builder.Services.AddEndpoints(Assembly.GetExecutingAssembly()); builder.Services.AddEndpoints(Assembly.GetExecutingAssembly());
builder.Services.AddApiServices(builder.Configuration); builder.Services.AddApiServices(builder.Configuration);
@@ -218,4 +218,4 @@ app.MapGet("/", () => Results.Redirect($"/openapi/{latestVersionGroup}"))
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
app.UseExceptionHandler("/Error", createScopeForErrors: true); app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.Run(); app.Run();
@@ -1,4 +1,4 @@
namespace PostFundManagement.Application.Extensions; namespace PostFundManagement.Application.Extensions;
public static class General public static class General
{ {
@@ -16,7 +16,8 @@ public static class General
public static (bool Decoded, Guid userId) GetUserId(this ClaimsPrincipal principal) 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); 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) 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); var decoded = !string.IsNullOrWhiteSpace(emailClaim);
return (decoded, emailClaim!); return (decoded, emailClaim ?? string.Empty);
} }
public static (bool Decoded, string role) GetUserRole(this ClaimsPrincipal principal) 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<IError> errors, int statusCode = StatusCodes.Status400BadRequest) => new() public static Microsoft.AspNetCore.Mvc.ProblemDetails ToProblems(this IReadOnlyList<IError> errors, int statusCode = StatusCodes.Status400BadRequest) => new()
@@ -17,7 +17,6 @@ import { AccountActivationModal } from '../components/auth/AccountActivationModa
import { NonPartnerAccessDeniedModal } from '../components/auth/NonPartnerAccessDeniedModal'; import { NonPartnerAccessDeniedModal } from '../components/auth/NonPartnerAccessDeniedModal';
import { SessionExpiredModal } from '../components/auth/SessionExpiredModal'; import { SessionExpiredModal } from '../components/auth/SessionExpiredModal';
import { encodeHashIdSync } from '../services/toolsService'; import { encodeHashIdSync } from '../services/toolsService';
import { verifyTenantLockout, clearTenantLockoutCache } from '../services/tenantLockoutService';
import { import {
onSessionExpired, onSessionExpired,
triggerSessionExpired, triggerSessionExpired,
@@ -510,35 +509,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
return; 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 }); setUser({ ...profile });
localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(profile)); localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(profile));
@@ -669,39 +639,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
return; 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) { if (resolvedUser) {
setUser({ ...resolvedUser }); setUser({ ...resolvedUser });
localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(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<UserSyncResult> => { const syncUserSession = async (force = true): Promise<UserSyncResult> => {
try { try {
await loadRuntimeConfig(); await loadRuntimeConfig();
clearTenantLockoutCache(); if (sub) {
if (sub) {
clearCachedUserRbac(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)); localStorage.setItem(LOCAL_STORAGE_CLAIMS_KEY, JSON.stringify(resolved.claims));
} catch {} } catch {}
const tenantClaimValues = extractTenantClaimValues(claims, accessToken); // Partner user verified successfully
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);
}
}
setActivationPending(false); setActivationPending(false);
setIsPendingActivation(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_SUB_KEY);
localStorage.removeItem(LOCAL_STORAGE_CLAIMS_KEY); localStorage.removeItem(LOCAL_STORAGE_CLAIMS_KEY);
clearUserSyncInBrowser(); clearUserSyncInBrowser();
clearTenantLockoutCache(); setUser(null);
setUser(null);
setSub(''); setSub('');
setClaims({}); setClaims({});
setTenantId(null); setTenantId(null);
@@ -1585,13 +1585,7 @@ export const deleteUserClaim = async (targetUserId: string, claimId: string | nu
await apiClient.delete(`/v1/api/users/${targetUserId}/claims/${claimId}`); await apiClient.delete(`/v1/api/users/${targetUserId}/claims/${claimId}`);
}; };
export const forceLogoutUser = async (userId: string): Promise<void> => { // Administrative functions (forceLogoutUser, setUserLockout) removed from PartnerUI
await apiClient.put(`/v1/api/users/${userId}/session/logout`);
};
export const setUserLockout = async (userId: string, disable: boolean): Promise<void> => {
await apiClient.put(`/v1/api/users/${userId}/session/lockout/${disable}`);
};
import { import {
syncUserAccount, syncUserAccount,
@@ -189,19 +189,7 @@ async function executeBackendSync(
userSub?: string, userSub?: string,
profile?: UserSyncProfile profile?: UserSyncProfile
): Promise<{ endpoint: string; data?: any }> { ): Promise<{ endpoint: string; data?: any }> {
const rawTenantId = profile?.tenant_id ?? (profile as any)?.tenantId; const endpoint = '/v1/api/integration/sync';
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 payload = { const payload = {
id: userSub || profile?.id || profile?.sub, id: userSub || profile?.id || profile?.sub,
sub: userSub || profile?.sub, sub: userSub || profile?.sub,
@@ -209,40 +197,25 @@ async function executeBackendSync(
name: profile?.name || undefined, name: profile?.name || undefined,
role: profile?.role || undefined, role: profile?.role || undefined,
tenant: profile?.tenant || undefined, tenant: profile?.tenant || undefined,
tenant_id: parsedTenantId, tenant_id: profile?.tenant_id,
organisationId: resolvedOrgId, organisationId: profile?.organisationId,
}; };
let lastError: any = null; try {
const res = await apiClient.post(endpoint, payload);
for (const endpoint of USER_SYNC_CANDIDATE_ENDPOINTS) { return { endpoint, data: res?.data };
try { } catch (err: any) {
let res; const status = err?.response?.status;
if (endpoint === '/v1/api/integration/sync') { if (status === 400 || status === 415 || status === 422) {
try { try {
res = await apiClient.post(endpoint, payload); const resFallback = await apiClient.post(endpoint, {});
} catch (postErr: any) { return { endpoint, data: resFallback?.data };
const errStatus = postErr?.response?.status; } catch (fallbackErr) {
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) {
throw err; throw err;
} }
} }
throw err;
} }
throw lastError || new Error('Failed to reach user sync endpoint');
} }
export async function syncUserAccount( export async function syncUserAccount(