Implemented partnerui

This commit is contained in:
2026-10-23 00:58:47 +02:00
parent 9ed210613e
commit 8f58e15f0f
27 changed files with 11416 additions and 983 deletions
@@ -0,0 +1,565 @@
import axios, { AxiosError } from 'axios';
import { ProblemDetails } from '../types';
export type ErrorCategory =
| 'validation'
| 'creation'
| 'update'
| 'security'
| 'notFound'
| 'conflict'
| 'server'
| 'general';
export interface FriendlyError {
title: string;
message: string;
statusCode?: number;
hasReturnObject: boolean;
isProblemDetails: boolean;
category: ErrorCategory;
categoryLabel?: string;
errorsList?: string[];
fieldErrors?: Array<{ field: string; message: string }>;
technicalDetails?: string;
}
export interface ProblemDetailsResponse {
type?: string | null;
title?: string | null;
status?: number | null;
detail?: string | null;
details?: string | null;
Details?: string | null;
instance?: string | null;
errors?: Record<string, string[] | string> | any;
[key: string]: any;
}
/**
* Checks if a string contains raw HTTP request/response headers or dump.
*/
export function isRawHttpOutput(text: string): boolean {
if (!text || typeof text !== 'string') return false;
const headerPatterns = [
/^(?:accept|authorization|content-type|content-length|origin|referer|user-agent|sec-ch-ua|sec-fetch|x-forwarded|x-real-ip|host):/im,
/(?:authorization:\s*bearer|content-type:\s*application\/)/i,
/HTTP\/\d(?:\.\d)?\s+\d{3}/i,
];
let matches = 0;
const lines = text.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (/^[A-Za-z0-9-]+:\s*.+$/.test(trimmed)) {
if (headerPatterns.some((p) => p.test(trimmed))) {
matches++;
}
}
}
return matches >= 2 || (matches >= 1 && /(?:authorization:\s*bearer|content-type:\s*application)/i.test(text));
}
/**
* Redacts sensitive tokens and credentials from technical traces.
*/
export function sanitizeTechnicalDetails(text: string): string {
if (!text) return '';
return text
.replace(/(authorization:\s*bearer\s+)[a-zA-Z0-9._-]+/gi, '$1[REDACTED_TOKEN]')
.replace(/(bearer\s+)[a-zA-Z0-9._-]+/gi, '$1[REDACTED_TOKEN]')
.replace(/("?(?:token|access_token|password|secret|apiKey)"?\s*[:=]\s*)"[^"]+"/gi, '$1"[REDACTED]"');
}
/**
* Cleans technical jargon, stack traces, and framework namespaces from text strings.
*/
export function cleanTechnicalJargon(text: string): string {
if (!text) return '';
if (isRawHttpOutput(text)) {
return '';
}
const cleaned = text
.replace(/^AxiosError:\s*/i, '')
.replace(/^Error:\s*/i, '')
.replace(/Request failed with status code \d+/i, '')
.replace(/https?:\/\/[^\s]+/g, '')
.replace(/System\.[a-zA-Z0-9_.]+/g, '')
.replace(/Microsoft\.[a-zA-Z0-9_.]+/g, '')
.replace(/at\s+[a-zA-Z0-9_.]+\s*\(.*?\)/g, '')
.trim();
if (cleaned.length < 2 || cleaned === '[object Object]' || cleaned.toLowerCase() === 'error') {
return '';
}
return cleaned;
}
/**
* Safely inspects whether an error contains a valid response body object.
*/
export function inspectErrorResponseObject(error: unknown): ProblemDetailsResponse | null {
if (!error) return null;
const anyErr = error as any;
const data = anyErr?.response?.data;
if (data && typeof data === 'object') {
return data as ProblemDetailsResponse;
}
if (typeof data === 'string' && data.trim().startsWith('{')) {
try {
const parsed = JSON.parse(data.trim());
if (parsed && typeof parsed === 'object') {
return parsed as ProblemDetailsResponse;
}
} catch {
// Not a JSON object
}
}
if (typeof error === 'object' && error !== null) {
if (
'details' in anyErr ||
'Details' in anyErr ||
'detail' in anyErr ||
'Detail' in anyErr ||
'status' in anyErr ||
'title' in anyErr ||
'type' in anyErr ||
'errors' in anyErr ||
'Errors' in anyErr
) {
return anyErr as ProblemDetailsResponse;
}
}
return null;
}
/**
* Checks if a response object conforms to RFC 7807 / RFC 9110 ProblemDetails.
*/
export function isProblemDetailsObject(data: any): data is ProblemDetails {
if (!data || typeof data !== 'object') return false;
return (
'details' in data ||
'Details' in data ||
'title' in data ||
'detail' in data ||
'Detail' in data ||
'status' in data ||
'type' in data ||
'errors' in data ||
'Errors' in data
);
}
/**
* Parses a comma-delimited error list from the `detail` property.
*/
export function parseCommaDelimitedErrors(detail?: string | null): string[] {
if (!detail || typeof detail !== 'string') return [];
const trimmed = detail.trim();
if (!trimmed) return [];
const rawParts = trimmed.split(',');
const errors: string[] = [];
for (const part of rawParts) {
const pTrim = part.trim();
if (!pTrim) continue;
const cleaned = cleanTechnicalJargon(pTrim);
if (cleaned && cleaned.length > 0) {
errors.push(cleaned);
} else if (pTrim.length > 0 && pTrim.toLowerCase() !== 'error') {
errors.push(pTrim);
}
}
return errors.length > 0 ? errors : [trimmed];
}
function detectOperationIntent(
method?: string,
actionTitle?: string,
url?: string
): 'creation' | 'update' | 'deletion' | 'general' {
const normMethod = (method || '').toUpperCase();
const text = `${actionTitle || ''} ${url || ''}`.toLowerCase();
if (
normMethod === 'POST' ||
text.includes('create') ||
text.includes('registration') ||
text.includes('register') ||
text.includes('dispatch') ||
text.includes('add ') ||
text.includes('new ') ||
text.includes('upload') ||
text.includes('record actual') ||
text.includes('log risk')
) {
return 'creation';
}
if (
normMethod === 'PUT' ||
normMethod === 'PATCH' ||
text.includes('update') ||
text.includes('edit') ||
text.includes('save') ||
text.includes('status') ||
text.includes('approve') ||
text.includes('execute') ||
text.includes('modify')
) {
return 'update';
}
if (normMethod === 'DELETE' || text.includes('delete') || text.includes('remove')) {
return 'deletion';
}
return 'general';
}
/**
* Comprehensive common error handling function across the application.
* Handles RFC 9110 / RFC 7807 ProblemDetails and non-technical language.
*/
export function parseFriendlyError(
error: unknown,
fallbackActionTitle = 'Action Failed'
): FriendlyError {
let statusCode: number | undefined;
let technicalSummary = '';
let httpMethod = '';
let requestUrl = '';
const anyErr = error as any;
if (axios.isAxiosError(error)) {
const axiosErr = error as AxiosError<any>;
statusCode = axiosErr.response?.status;
httpMethod = axiosErr.config?.method?.toUpperCase() || '';
requestUrl = axiosErr.config?.url || '';
technicalSummary = `${httpMethod || 'HTTP'} ${requestUrl} -> ${statusCode || axiosErr.code || 'ERR'}`;
if (!axiosErr.response) {
if (axiosErr.code === 'ECONNABORTED' || axiosErr.message.includes('timeout')) {
return {
title: 'Request Timed Out',
message: 'The server took too long to respond. Please check your network connection and try again.',
statusCode: 408,
hasReturnObject: false,
isProblemDetails: false,
category: 'server',
categoryLabel: 'Timeout',
technicalDetails: technicalSummary || axiosErr.message,
};
}
return {
title: 'Connection Unavailable',
message: 'Unable to reach the system service. Please verify your internet connection or try again in a few moments.',
statusCode: 0,
hasReturnObject: false,
isProblemDetails: false,
category: 'server',
categoryLabel: 'Network',
technicalDetails: technicalSummary || axiosErr.message,
};
}
} else if (anyErr && typeof anyErr === 'object') {
statusCode = anyErr.response?.status ?? anyErr.status;
httpMethod = anyErr.config?.method?.toUpperCase() || '';
requestUrl = anyErr.config?.url || '';
if (statusCode) {
technicalSummary = `HTTP ${statusCode}`;
}
}
const returnObject = inspectErrorResponseObject(error);
const hasReturnObject = returnObject !== null;
if (returnObject) {
if (returnObject.status && typeof returnObject.status === 'number') {
statusCode = returnObject.status;
}
}
const operationIntent = detectOperationIntent(httpMethod, fallbackActionTitle, requestUrl);
if (statusCode === 401) {
const rawDetail =
returnObject?.details ??
returnObject?.Details ??
returnObject?.detail ??
returnObject?.Detail ??
anyErr?.response?.data?.details ??
anyErr?.response?.data?.Details ??
anyErr?.response?.data?.detail ??
anyErr?.details ??
anyErr?.message;
const cleanDetail = cleanTechnicalJargon(rawDetail || '');
return {
title: 'Authentication Required',
message: cleanDetail || 'Your authentication session has expired or is unauthorized (HTTP 401). Please sign in again.',
statusCode: 401,
hasReturnObject,
isProblemDetails: hasReturnObject,
category: 'security',
categoryLabel: 'Unauthorized',
technicalDetails: technicalSummary ? sanitizeTechnicalDetails(technicalSummary) : 'HTTP 401 Unauthorized',
};
}
if (statusCode === 403) {
const rawDetail =
returnObject?.details ??
returnObject?.Details ??
returnObject?.detail ??
returnObject?.Detail ??
anyErr?.response?.data?.details ??
anyErr?.response?.data?.Details ??
anyErr?.response?.data?.detail ??
anyErr?.details ??
anyErr?.message;
const detailErrors = parseCommaDelimitedErrors(rawDetail);
const friendlyMessage =
detailErrors.length > 0
? detailErrors.length === 1
? detailErrors[0]
: detailErrors.map((err) => `• ${err}`).join('\n')
: 'You do not have permission to access this resource or perform this action (HTTP 403).';
return {
title: fallbackActionTitle !== 'Action Failed' ? fallbackActionTitle : 'Access Forbidden',
message: friendlyMessage,
statusCode: 403,
hasReturnObject,
isProblemDetails: hasReturnObject,
category: 'security',
categoryLabel: 'Forbidden',
errorsList: detailErrors.length > 0 ? detailErrors : undefined,
technicalDetails: technicalSummary ? sanitizeTechnicalDetails(technicalSummary) : 'HTTP 403 Forbidden',
};
}
if (hasReturnObject && returnObject) {
const rawDetail =
returnObject.detail ??
returnObject.Detail ??
returnObject.details ??
returnObject.Details ??
anyErr?.response?.data?.detail ??
anyErr?.response?.data?.Detail ??
anyErr?.response?.data?.details ??
anyErr?.response?.data?.Details ??
anyErr?.detail ??
anyErr?.Detail ??
anyErr?.details ??
anyErr?.Details ??
returnObject.message ??
returnObject.Message ??
returnObject.error ??
returnObject.Error;
const errorsList: string[] = [];
if (Array.isArray(rawDetail)) {
rawDetail.forEach((e: any) => {
const cleaned = cleanTechnicalJargon(typeof e === 'string' ? e : e?.message || JSON.stringify(e));
if (cleaned) errorsList.push(cleaned);
});
} else if (typeof rawDetail === 'string' && rawDetail.trim()) {
errorsList.push(...parseCommaDelimitedErrors(rawDetail));
}
if (errorsList.length === 0 && returnObject.errors) {
if (Array.isArray(returnObject.errors)) {
returnObject.errors.forEach((e: any) => {
const cleaned = cleanTechnicalJargon(typeof e === 'string' ? e : e?.message || JSON.stringify(e));
if (cleaned) errorsList.push(cleaned);
});
} else if (typeof returnObject.errors === 'object') {
for (const [key, val] of Object.entries(returnObject.errors)) {
if (Array.isArray(val)) {
val.forEach((m) => {
const cleaned = cleanTechnicalJargon(String(m));
if (cleaned) errorsList.push(`${key}: ${cleaned}`);
});
} else if (typeof val === 'string' && val.trim()) {
const cleaned = cleanTechnicalJargon(val);
if (cleaned) errorsList.push(`${key}: ${cleaned}`);
}
}
}
}
let category: ErrorCategory = 'general';
let categoryLabel: string = 'Error';
if (statusCode === 400) {
if (operationIntent === 'creation') {
category = 'creation';
categoryLabel = 'Creation Error';
} else if (operationIntent === 'update') {
category = 'update';
categoryLabel = 'Update Error';
} else {
category = 'validation';
categoryLabel = 'Validation Error';
}
} else if (statusCode === 404) {
category = 'notFound';
categoryLabel = 'Not Found';
} else if (statusCode === 409) {
category = 'conflict';
categoryLabel = 'Conflict';
} else if (statusCode === 422) {
category = 'validation';
categoryLabel = 'Validation Error';
} else if (statusCode && statusCode >= 500) {
category = 'server';
categoryLabel = 'Server Error';
}
let friendlyMessage = '';
if (errorsList.length > 1) {
friendlyMessage = errorsList.map((err) => `• ${err}`).join('\n');
} else if (errorsList.length === 1) {
friendlyMessage = errorsList[0];
} else {
friendlyMessage =
operationIntent === 'creation'
? 'Failed to create record. Please verify the submitted parameters.'
: operationIntent === 'update'
? 'Failed to update record. Please verify the submitted parameters.'
: 'Request could not be processed due to invalid parameters.';
}
let displayTitle = fallbackActionTitle;
if (displayTitle === 'Action Failed' && returnObject.title && returnObject.title.toLowerCase() !== 'error') {
displayTitle = returnObject.title;
}
return {
title: displayTitle,
message: friendlyMessage,
statusCode,
hasReturnObject: true,
isProblemDetails: true,
category,
categoryLabel,
errorsList: errorsList.length > 0 ? errorsList : undefined,
technicalDetails: technicalSummary ? sanitizeTechnicalDetails(technicalSummary) : (statusCode ? `HTTP Status ${statusCode}` : undefined),
};
}
let title = fallbackActionTitle || 'Action Failed';
let message = '';
let category: ErrorCategory = 'general';
let categoryLabel: string = 'Error';
const directDetail =
anyErr?.response?.data?.detail ??
anyErr?.response?.data?.Detail ??
anyErr?.response?.data?.details ??
anyErr?.response?.data?.Details ??
anyErr?.detail ??
anyErr?.Detail ??
anyErr?.details ??
anyErr?.Details;
const fallbackErrorsList: string[] = [];
if (directDetail) {
if (typeof directDetail === 'string' && directDetail.trim()) {
fallbackErrorsList.push(...parseCommaDelimitedErrors(directDetail.trim()));
message = fallbackErrorsList.join('\n');
} else if (Array.isArray(directDetail)) {
fallbackErrorsList.push(...directDetail.map(String));
message = fallbackErrorsList.join('\n');
}
}
if (!message) {
switch (statusCode) {
case 400:
category = operationIntent === 'creation' ? 'creation' : operationIntent === 'update' ? 'update' : 'validation';
categoryLabel = 'Bad Request';
message =
operationIntent === 'creation'
? 'The new record could not be created. Please review the submitted parameters.'
: operationIntent === 'update'
? 'The record could not be updated. Please verify the submitted parameters.'
: 'The request could not be processed due to invalid parameters.';
break;
case 404:
title = 'Record Not Found';
category = 'notFound';
categoryLabel = 'Not Found';
message = 'The requested resource could not be found (HTTP 404). It may have been archived, removed, or the link is outdated.';
break;
case 409:
title = 'Resource Conflict';
category = 'conflict';
categoryLabel = 'Conflict';
message = 'This operation conflicts with the current status of the resource (HTTP 409). Please refresh the page and try again.';
break;
case 422:
category = 'validation';
categoryLabel = 'Incomplete Data';
message = 'The submitted information could not be processed by the server (HTTP 422). Please verify all required fields.';
break;
case 429:
title = 'Rate Limit Exceeded';
category = 'server';
categoryLabel = 'Rate Limit';
message = 'The system received too many requests in a short time (HTTP 429). Please pause for a moment before trying again.';
break;
case 500:
case 502:
case 503:
case 504:
title = 'Service Temporarily Unavailable';
category = 'server';
categoryLabel = 'Service Issue';
message = `The server encountered an error while processing your request (HTTP ${statusCode}). Please try again in a few moments.`;
break;
default:
if (typeof error === 'string' && error.trim()) {
message = cleanTechnicalJargon(error);
} else if (error instanceof Error && error.message) {
message = cleanTechnicalJargon(error.message);
}
if (!message) {
message = statusCode
? `An unexpected server issue occurred (HTTP ${statusCode}). Please try again or contact support.`
: 'An unexpected issue occurred while processing this request. Please try again.';
}
break;
}
}
return {
title,
message,
statusCode,
hasReturnObject: fallbackErrorsList.length > 0,
isProblemDetails: false,
category,
categoryLabel,
errorsList: fallbackErrorsList.length > 0 ? fallbackErrorsList : undefined,
technicalDetails: technicalSummary ? sanitizeTechnicalDetails(technicalSummary) : (statusCode ? `HTTP Status ${statusCode}` : undefined),
};
}