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.Domain.Abstractions;
using PostFundManagement.Domain.Errors;
@@ -13,12 +13,18 @@ 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
{
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<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
var r => Results.InternalServerError(r.Errors.ToProblems())
};
})
.WithDescription("Lock user session")
.RequireAuthorization(Constants.MainScope)
@@ -26,6 +32,7 @@ public sealed class SetUserLockoutEndpoint : IEndpoint
.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)
+1 -1
View File
@@ -29,7 +29,7 @@ builder.Services.AddApiVersioning(options =>
builder.Services.AddScoped<ActiveUserFilter>();
builder.Services.AddScoped<UserRoleFilter>();
builder.Services.AddTransient<ITenantContextService, TenantContextService>();
builder.Services.AddScoped<ITenantContextService, TenantContextService>();
builder.Services.AddEndpoints(Assembly.GetExecutingAssembly());
builder.Services.AddApiServices(builder.Configuration);
@@ -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<IError> errors, int statusCode = StatusCodes.Status400BadRequest) => new()
@@ -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,7 +683,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const syncUserSession = async (force = true): Promise<UserSyncResult> => {
try {
await loadRuntimeConfig();
clearTenantLockoutCache();
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,7 +775,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
localStorage.removeItem(LOCAL_STORAGE_SUB_KEY);
localStorage.removeItem(LOCAL_STORAGE_CLAIMS_KEY);
clearUserSyncInBrowser();
clearTenantLockoutCache();
setUser(null);
setSub('');
setClaims({});
@@ -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<void> => {
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}`);
};
// Administrative functions (forceLogoutUser, setUserLockout) removed from PartnerUI
import {
syncUserAccount,
@@ -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);
}
const 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) {
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(