Removed setUserLockout from PartnerUI
This commit is contained in:
@@ -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,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,
|
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();
|
||||||
|
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(),
|
{ IsSuccess: true } => Results.Ok(),
|
||||||
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
|
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
|
||||||
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
|
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
|
||||||
var r => Results.InternalServerError(r.Errors.ToProblems())
|
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.WithDescription("Lock user session")
|
.WithDescription("Lock user session")
|
||||||
.RequireAuthorization(Constants.MainScope)
|
.RequireAuthorization(Constants.MainScope)
|
||||||
@@ -26,6 +32,7 @@ public sealed class SetUserLockoutEndpoint : IEndpoint
|
|||||||
.MapToApiVersion(new ApiVersion(1))
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
.Produces(StatusCodes.Status200OK)
|
.Produces(StatusCodes.Status200OK)
|
||||||
.Produces(StatusCodes.Status401Unauthorized)
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status403Forbidden)
|
||||||
.Produces<Problems>(StatusCodes.Status404NotFound)
|
.Produces<Problems>(StatusCodes.Status404NotFound)
|
||||||
.Produces<Problems>(StatusCodes.Status400BadRequest)
|
.Produces<Problems>(StatusCodes.Status400BadRequest)
|
||||||
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,7 +683,6 @@ 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,7 +775,6 @@ 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({});
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
for (const endpoint of USER_SYNC_CANDIDATE_ENDPOINTS) {
|
|
||||||
try {
|
try {
|
||||||
let res;
|
const res = await apiClient.post(endpoint, payload);
|
||||||
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 };
|
return { endpoint, data: res?.data };
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
lastError = err;
|
|
||||||
const status = err?.response?.status;
|
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 err;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw lastError || new Error('Failed to reach user sync endpoint');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncUserAccount(
|
export async function syncUserAccount(
|
||||||
|
|||||||
Reference in New Issue
Block a user