Implemented session and account lockout management
Enabled account sync control and session expiration management Applied some fixes to the govername page and project page
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
UpdateProjectStatusRequest,
|
||||
Activity,
|
||||
CreateActivityRequest,
|
||||
UpdateActivityRequest,
|
||||
Milestone,
|
||||
CreateMilestoneRequest,
|
||||
UpdateMilestoneRequest,
|
||||
@@ -51,26 +52,29 @@ import {
|
||||
ActivityStatus,
|
||||
} from '../types';
|
||||
|
||||
// Helper to extract ProblemDetails from Axios error
|
||||
import { parseFriendlyError, FriendlyError } from '../utils/apiErrorParser';
|
||||
import { registerUserEmail, registerUserEmails } from '../lib/formatters';
|
||||
|
||||
// Helper to extract ProblemDetails from Axios error or unknown error
|
||||
export function parseApiError(error: unknown): ProblemDetails {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const axiosErr = error as AxiosError<ProblemDetails>;
|
||||
if (axiosErr.response?.data) {
|
||||
if (axiosErr.response?.data && typeof axiosErr.response.data === 'object') {
|
||||
return axiosErr.response.data;
|
||||
}
|
||||
return {
|
||||
title: 'API Request Error',
|
||||
detail: axiosErr.message || 'Unable to communicate with the remote PFM service.',
|
||||
status: axiosErr.response?.status || 500,
|
||||
};
|
||||
}
|
||||
|
||||
const friendly = parseFriendlyError(error);
|
||||
return {
|
||||
title: 'Unexpected Application Error',
|
||||
detail: String(error),
|
||||
status: 500,
|
||||
title: friendly.title,
|
||||
detail: friendly.message,
|
||||
status: friendly.statusCode || 500,
|
||||
};
|
||||
}
|
||||
|
||||
export { parseFriendlyError };
|
||||
export type { FriendlyError };
|
||||
|
||||
// ==========================================
|
||||
// 1. Executive Dashboard APIs
|
||||
// ==========================================
|
||||
@@ -82,17 +86,53 @@ export const getDashboard = async (): Promise<Dashboard> => {
|
||||
// ==========================================
|
||||
// 2. Governance & Structure APIs
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Normalizes an Organisation object from backend responses.
|
||||
* Robustly maps status across variations (status, Status, activityStatus, approvalStatus, statusId, isActive).
|
||||
*/
|
||||
export const normalizeOrganisation = (raw: any): Organisation => {
|
||||
if (!raw) return raw;
|
||||
const status =
|
||||
raw.status ??
|
||||
raw.Status ??
|
||||
raw.activityStatus ??
|
||||
raw.ActivityStatus ??
|
||||
raw.approvalStatus ??
|
||||
raw.ApprovalStatus ??
|
||||
raw.statusId ??
|
||||
raw.StatusId ??
|
||||
raw.state ??
|
||||
raw.State ??
|
||||
(raw.isActive !== undefined ? (raw.isActive ? 1 : 2) : undefined) ??
|
||||
(raw.IsActive !== undefined ? (raw.IsActive ? 1 : 2) : undefined) ??
|
||||
ActivityStatus.Active;
|
||||
|
||||
return {
|
||||
...raw,
|
||||
id: raw.id ?? raw.Id ?? raw.organisationId ?? raw.OrganisationId,
|
||||
name: raw.name ?? raw.Name ?? null,
|
||||
registrationNo: raw.registrationNo ?? raw.RegistrationNo ?? raw.regNo ?? raw.RegNo ?? null,
|
||||
email: raw.email ?? raw.Email ?? null,
|
||||
type: raw.type ?? raw.Type ?? raw.organisationType ?? raw.OrganisationType ?? 5,
|
||||
status,
|
||||
createdAt: raw.createdAt ?? raw.CreatedAt ?? new Date().toISOString(),
|
||||
createdBy: raw.createdBy ?? raw.CreatedBy ?? '',
|
||||
};
|
||||
};
|
||||
|
||||
export const getOrganisations = async (page = 1, pageSize = 100): Promise<Organisation[]> => {
|
||||
const res = await apiClient.get<Organisation[]>('/v1/api/governance/organisations', {
|
||||
const res = await apiClient.get<any[]>('/v1/api/governance/organisations', {
|
||||
params: { page, pageSize },
|
||||
});
|
||||
return res.data || [];
|
||||
const list = res.data || [];
|
||||
return list.map(normalizeOrganisation);
|
||||
};
|
||||
|
||||
export const getOrganisation = async (organisationId: string | number): Promise<Organisation> => {
|
||||
const hashedOrgId = await toolsService.resolveOrganisationId(organisationId);
|
||||
const res = await apiClient.get<Organisation>(`/v1/api/governance/organisations/${hashedOrgId}`);
|
||||
return res.data;
|
||||
const res = await apiClient.get<any>(`/v1/api/governance/organisations/${hashedOrgId}`);
|
||||
return normalizeOrganisation(res.data);
|
||||
};
|
||||
|
||||
export const createOrganisation = async (payload: CreateOrganisationRequest): Promise<Organisation> => {
|
||||
@@ -275,11 +315,46 @@ export const getActivity = async (activityId: string | number): Promise<Activity
|
||||
|
||||
export const createActivity = async (payload: CreateActivityRequest): Promise<Activity> => {
|
||||
const userSub = getCurrentUserSub();
|
||||
const res = await apiClient.post('/v1/api/projects/activities', {
|
||||
const body: any = {
|
||||
...payload,
|
||||
milestoneId:
|
||||
payload.milestoneId !== undefined && payload.milestoneId !== null && payload.milestoneId !== ''
|
||||
? Number(payload.milestoneId)
|
||||
: null,
|
||||
createdBy: userSub || undefined,
|
||||
});
|
||||
return res.data;
|
||||
};
|
||||
try {
|
||||
const res = await apiClient.post('/v1/api/projects/activities', body);
|
||||
return res.data;
|
||||
} catch (err: any) {
|
||||
if (err?.response?.status === 405) {
|
||||
const res = await apiClient.put('/v1/api/projects/activities', body);
|
||||
return res.data;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateActivity = async (payload: UpdateActivityRequest): Promise<Activity> => {
|
||||
const userSub = getCurrentUserSub();
|
||||
const body: any = {
|
||||
...payload,
|
||||
milestoneId:
|
||||
payload.milestoneId !== undefined && payload.milestoneId !== null && payload.milestoneId !== ''
|
||||
? Number(payload.milestoneId)
|
||||
: null,
|
||||
updatedBy: userSub || undefined,
|
||||
};
|
||||
try {
|
||||
const res = await apiClient.put('/v1/api/projects/activities', body);
|
||||
return res.data;
|
||||
} catch (err: any) {
|
||||
if (err?.response?.status === 404 || err?.response?.status === 405) {
|
||||
const res = await apiClient.put('/v1/api/projects/activity', body);
|
||||
return res.data;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const getMilestones = async (projectId: string | number): Promise<Milestone[]> => {
|
||||
@@ -324,8 +399,9 @@ export const getProjectIndicators = async (projectId: string | number): Promise<
|
||||
|
||||
export const getProjectRisks = async (projectId: string | number): Promise<Risk[]> => {
|
||||
const hashedProjectId = await toolsService.resolveProjectId(projectId);
|
||||
const res = await apiClient.get<Risk[]>(`/v1/api/projects/${hashedProjectId}/risks`);
|
||||
return res.data || [];
|
||||
const res = await apiClient.get<any[]>(`/v1/api/projects/${hashedProjectId}/risks`);
|
||||
const list = res.data || [];
|
||||
return list.map(normalizeRisk);
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
@@ -470,32 +546,85 @@ export const approveDisbursement = async (disbursementId: string | number): Prom
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes a Risk record from backend responses.
|
||||
* Robustly parses Likelihood (1-5) and Impact (1-5), supporting strings, numbers, and case variations.
|
||||
*/
|
||||
export const normalizeRisk = (raw: any): Risk => {
|
||||
if (!raw) return raw;
|
||||
|
||||
const rawLik = raw.likelihood ?? raw.Likelihood ?? raw.riskLikelihood ?? raw.RiskLikelihood;
|
||||
const rawImp = raw.impact ?? raw.Impact ?? raw.riskImpact ?? raw.RiskImpact;
|
||||
|
||||
const parseScore = (val: any, defaultVal: number): number => {
|
||||
if (typeof val === 'number' && !isNaN(val)) {
|
||||
return Math.min(5, Math.max(1, Math.round(val)));
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
const parsed = parseInt(val, 10);
|
||||
if (!isNaN(parsed) && parsed >= 1 && parsed <= 5) {
|
||||
return parsed;
|
||||
}
|
||||
const lower = val.toLowerCase().trim();
|
||||
if (lower.includes('rare') || lower.includes('insignificant') || lower.includes('trivial') || lower === '1') return 1;
|
||||
if (lower.includes('unlikely') || lower.includes('minor') || lower === '2') return 2;
|
||||
if (lower.includes('possible') || lower.includes('moderate') || lower.includes('medium') || lower === '3') return 3;
|
||||
if (lower.includes('likely') || lower.includes('major') || lower.includes('high') || lower === '4') return 4;
|
||||
if (lower.includes('almost') || lower.includes('certain') || lower.includes('severe') || lower.includes('critical') || lower.includes('catastrophic') || lower === '5') return 5;
|
||||
}
|
||||
return defaultVal;
|
||||
};
|
||||
|
||||
const likelihood = parseScore(rawLik, 3);
|
||||
const impact = parseScore(rawImp, 2);
|
||||
|
||||
return {
|
||||
...raw,
|
||||
id: raw.id ?? raw.Id ?? raw.riskId ?? raw.RiskId,
|
||||
projectId: raw.projectId ?? raw.ProjectId,
|
||||
name: raw.name ?? raw.Name ?? null,
|
||||
description: raw.description ?? raw.Description ?? null,
|
||||
likelihood,
|
||||
impact,
|
||||
createdBy: raw.createdBy ?? raw.CreatedBy ?? '',
|
||||
createdAt: raw.createdAt ?? raw.CreatedAt ?? new Date().toISOString(),
|
||||
owner: raw.owner ?? raw.Owner ?? raw.riskOwner ?? raw.RiskOwner ?? raw.createdBy ?? raw.CreatedBy ?? '',
|
||||
mitigationAction: raw.mitigationAction ?? raw.MitigationAction ?? raw.mitigation ?? raw.Mitigation ?? '',
|
||||
status: raw.status ?? raw.Status,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAllRisks = async (page = 1, pageSize = 100): Promise<Risk[]> => {
|
||||
const res = await apiClient.get<Risk[]>('/v1/api/execution/risks', {
|
||||
const res = await apiClient.get<any[]>('/v1/api/execution/risks', {
|
||||
params: { page, pageSize },
|
||||
});
|
||||
return res.data || [];
|
||||
const list = res.data || [];
|
||||
return list.map(normalizeRisk);
|
||||
};
|
||||
|
||||
export const getRisk = async (riskId: string | number): Promise<Risk> => {
|
||||
const hashedRiskId = await toolsService.encodeId(riskId);
|
||||
const res = await apiClient.get<Risk>(`/v1/api/execution/risks/${hashedRiskId}`);
|
||||
return res.data;
|
||||
const res = await apiClient.get<any>(`/v1/api/execution/risks/${hashedRiskId}`);
|
||||
return normalizeRisk(res.data);
|
||||
};
|
||||
|
||||
export const createRisk = async (payload: CreateRiskRequest): Promise<Risk> => {
|
||||
const userSub = getCurrentUserSub();
|
||||
const res = await apiClient.post('/v1/api/execution/risks', {
|
||||
...payload,
|
||||
likelihood: Number(payload.likelihood),
|
||||
impact: Number(payload.impact),
|
||||
createdBy: userSub || undefined,
|
||||
});
|
||||
return res.data;
|
||||
return normalizeRisk(res.data);
|
||||
};
|
||||
|
||||
export const updateRisk = async (payload: UpdateRiskRequest): Promise<void> => {
|
||||
const userSub = getCurrentUserSub();
|
||||
await apiClient.put('/v1/api/execution/risks', {
|
||||
...payload,
|
||||
likelihood: Number(payload.likelihood),
|
||||
impact: Number(payload.impact),
|
||||
updatedBy: userSub || undefined,
|
||||
});
|
||||
};
|
||||
@@ -592,11 +721,16 @@ export const getUsers = async (
|
||||
filter: UserFilterRequest = {}
|
||||
): Promise<User[]> => {
|
||||
const res = await apiClient.post<User[]>(`/v1/api/users/${page}/${pageSize}`, filter);
|
||||
return res.data || [];
|
||||
const list = res.data || [];
|
||||
registerUserEmails(list);
|
||||
return list;
|
||||
};
|
||||
|
||||
export const getUser = async (userId: string): Promise<User> => {
|
||||
const res = await apiClient.get<User>(`/v1/api/users/${userId}`);
|
||||
if (res.data?.id && res.data?.email) {
|
||||
registerUserEmail(res.data.id, res.data.email);
|
||||
}
|
||||
return res.data;
|
||||
};
|
||||
|
||||
@@ -604,7 +738,11 @@ export const getCurrentUser = async (): Promise<User> => {
|
||||
const userSub = getCurrentUserSub();
|
||||
if (userSub) {
|
||||
try {
|
||||
return await getUser(userSub);
|
||||
const u = await getUser(userSub);
|
||||
if (u?.id && u?.email) {
|
||||
registerUserEmail(u.id, u.email);
|
||||
}
|
||||
return u;
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
@@ -621,8 +759,40 @@ export const updateUserStatus = async (userId: string, status: ActivityStatus |
|
||||
await apiClient.get(`/v1/api/users/${userId}/status/${status}`);
|
||||
};
|
||||
|
||||
export const syncIntegration = async (): Promise<void> => {
|
||||
await apiClient.post('/v1/api/integration/sync');
|
||||
export {
|
||||
syncUserAccount,
|
||||
ensureUserSynced,
|
||||
isUserSyncedInBrowser,
|
||||
saveUserSyncInBrowser,
|
||||
clearUserSyncInBrowser,
|
||||
getUserSyncRecord,
|
||||
isActivationPending,
|
||||
setActivationPending,
|
||||
getActivationPendingReason,
|
||||
} from './userSyncService';
|
||||
|
||||
/**
|
||||
* Synchronizes the authenticated user with the backend to enable the user in the system.
|
||||
* By default, sync is saved in the browser to prevent hotspotting the sync endpoint.
|
||||
*/
|
||||
export const syncUser = async (
|
||||
userData?: {
|
||||
id?: string;
|
||||
sub?: string;
|
||||
email?: string | null;
|
||||
name?: string | null;
|
||||
role?: string | null;
|
||||
tenant?: string | null;
|
||||
},
|
||||
force = false
|
||||
): Promise<boolean> => {
|
||||
const { ensureUserSynced } = await import('./userSyncService');
|
||||
return ensureUserSynced(userData?.sub || userData?.id, undefined, userData, force);
|
||||
};
|
||||
|
||||
export const syncIntegration = async (force = true): Promise<void> => {
|
||||
const { ensureUserSynced } = await import('./userSyncService');
|
||||
await ensureUserSynced(undefined, undefined, undefined, force);
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
|
||||
Reference in New Issue
Block a user