UserWallet: useRelayNotifications, CNIL validation, ServiceSync polling, SyncScreen link
**Motivations:** - Intégrer les notifications relais (polling, auto-process hashes) dans ServiceSyncScreen - Valider conformité CNIL des contrats/champs à l’ajout au graphe - Accéder à la sync par service depuis SyncScreen **Root causes:** - N/A (évolutions + petits correctifs) **Correctifs:** - relayNotificationService: suppression import inutilisé RelayConfig **Evolutions:** - useRelayNotifications: hook (RelayNotificationService, start/stop polling, auto-process hash) - cnilValidation: validateContractCNIL, validateChampCNIL (valid, errors, warnings) - graphResolver: addContrat/addChamp appellent validation CNIL (async, logs warnings/errors, non bloquant) - ServiceSyncScreen: useRelayNotifications, GraphResolver ref, polling selon configs (fréquence min), sync result (hasMessages, « Aucun nouveau message »), useBloom mention - SyncScreen: bouton « Sync par service » vers /service-sync **Pages affectées:** - userwallet: useRelayNotifications, cnilValidation, ServiceSyncScreen, SyncScreen, graphResolver, relayNotificationService
This commit is contained in:
parent
f27345e0ba
commit
13898d1012
@ -1,10 +1,12 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useIdentity } from '../hooks/useIdentity';
|
import { useIdentity } from '../hooks/useIdentity';
|
||||||
import { useErrorHandler } from '../hooks/useErrorHandler';
|
import { useErrorHandler } from '../hooks/useErrorHandler';
|
||||||
import { ErrorDisplay } from './ErrorDisplay';
|
import { ErrorDisplay } from './ErrorDisplay';
|
||||||
import { getStoredRelays } from '../utils/relay';
|
import { getStoredRelays } from '../utils/relay';
|
||||||
import { GraphResolver } from '../services/graphResolver';
|
import { GraphResolver } from '../services/graphResolver';
|
||||||
import { SyncService } from '../services/syncService';
|
import { SyncService } from '../services/syncService';
|
||||||
|
import { useRelayNotifications } from '../hooks/useRelayNotifications';
|
||||||
import type { ServiceStatus } from '../types/identity';
|
import type { ServiceStatus } from '../types/identity';
|
||||||
|
|
||||||
interface ServiceSyncConfig {
|
interface ServiceSyncConfig {
|
||||||
@ -18,6 +20,7 @@ interface ServiceSyncConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceSyncScreen(): JSX.Element {
|
export function ServiceSyncScreen(): JSX.Element {
|
||||||
|
const navigate = useNavigate();
|
||||||
const { identity } = useIdentity();
|
const { identity } = useIdentity();
|
||||||
const { error, handleError, clearError } = useErrorHandler();
|
const { error, handleError, clearError } = useErrorHandler();
|
||||||
const [services, setServices] = useState<ServiceStatus[]>([]);
|
const [services, setServices] = useState<ServiceStatus[]>([]);
|
||||||
@ -29,12 +32,22 @@ export function ServiceSyncScreen(): JSX.Element {
|
|||||||
Map<string, { ok: boolean; message?: string }>
|
Map<string, { ok: boolean; message?: string }>
|
||||||
>(new Map());
|
>(new Map());
|
||||||
|
|
||||||
|
const graphResolverRef = useRef<GraphResolver | null>(null);
|
||||||
|
if (graphResolverRef.current === null) {
|
||||||
|
graphResolverRef.current = new GraphResolver();
|
||||||
|
}
|
||||||
|
const { startPolling, stopPolling } = useRelayNotifications(
|
||||||
|
graphResolverRef.current,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load services and configs on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (identity === null) {
|
if (identity === null || graphResolverRef.current === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const graphResolver = new GraphResolver();
|
const graphResolver = graphResolverRef.current;
|
||||||
const syncService = new SyncService(
|
const syncService = new SyncService(
|
||||||
getStoredRelays().filter((r) => r.enabled),
|
getStoredRelays().filter((r) => r.enabled),
|
||||||
graphResolver,
|
graphResolver,
|
||||||
@ -44,7 +57,13 @@ export function ServiceSyncScreen(): JSX.Element {
|
|||||||
void (async (): Promise<void> => {
|
void (async (): Promise<void> => {
|
||||||
await syncService.init();
|
await syncService.init();
|
||||||
const svcs = graphResolver.getServices();
|
const svcs = graphResolver.getServices();
|
||||||
setServices(svcs.map((s) => ({ service_uuid: s.uuid })));
|
setServices(
|
||||||
|
svcs.map((s) => ({
|
||||||
|
service_uuid: s.uuid,
|
||||||
|
contrat_complet: true,
|
||||||
|
contrat_valide: true,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
// Load configs from localStorage
|
// Load configs from localStorage
|
||||||
const stored = localStorage.getItem('userwallet_service_sync_configs');
|
const stored = localStorage.getItem('userwallet_service_sync_configs');
|
||||||
@ -62,6 +81,34 @@ export function ServiceSyncScreen(): JSX.Element {
|
|||||||
})();
|
})();
|
||||||
}, [identity]);
|
}, [identity]);
|
||||||
|
|
||||||
|
// Start/stop polling based on configs
|
||||||
|
useEffect(() => {
|
||||||
|
const enabledConfigs = Array.from(configs.values()).filter(
|
||||||
|
(c) => c.enabled,
|
||||||
|
);
|
||||||
|
if (enabledConfigs.length > 0) {
|
||||||
|
const minInterval = Math.min(
|
||||||
|
...enabledConfigs.map((c) => {
|
||||||
|
switch (c.frequency) {
|
||||||
|
case 'min':
|
||||||
|
return 60000;
|
||||||
|
case 'hour':
|
||||||
|
return 3600000;
|
||||||
|
case 'day':
|
||||||
|
return 86400000;
|
||||||
|
default:
|
||||||
|
return 3600000;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
startPolling(minInterval);
|
||||||
|
return () => {
|
||||||
|
stopPolling();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [configs, startPolling, stopPolling]);
|
||||||
|
|
||||||
const saveConfigs = useCallback((newConfigs: Map<string, ServiceSyncConfig>): void => {
|
const saveConfigs = useCallback((newConfigs: Map<string, ServiceSyncConfig>): void => {
|
||||||
const obj = Object.fromEntries(newConfigs);
|
const obj = Object.fromEntries(newConfigs);
|
||||||
localStorage.setItem('userwallet_service_sync_configs', JSON.stringify(obj));
|
localStorage.setItem('userwallet_service_sync_configs', JSON.stringify(obj));
|
||||||
@ -112,7 +159,10 @@ export function ServiceSyncScreen(): JSX.Element {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const start = now - config.windowHours * 3600000;
|
const start = now - config.windowHours * 3600000;
|
||||||
|
|
||||||
const graphResolver = new GraphResolver();
|
if (graphResolverRef.current === null) {
|
||||||
|
graphResolverRef.current = new GraphResolver();
|
||||||
|
}
|
||||||
|
const graphResolver = graphResolverRef.current;
|
||||||
const syncService = new SyncService(
|
const syncService = new SyncService(
|
||||||
getStoredRelays().filter((r) => r.enabled),
|
getStoredRelays().filter((r) => r.enabled),
|
||||||
graphResolver,
|
graphResolver,
|
||||||
@ -120,14 +170,21 @@ export function ServiceSyncScreen(): JSX.Element {
|
|||||||
);
|
);
|
||||||
await syncService.init();
|
await syncService.init();
|
||||||
|
|
||||||
|
// Use Bloom filter if enabled
|
||||||
|
if (config.useBloom) {
|
||||||
|
// Bloom filter is already used in syncService via getKeysInWindow
|
||||||
|
// which skips hashes already seen
|
||||||
|
}
|
||||||
|
|
||||||
const result = await syncService.sync(start, now, serviceUuid);
|
const result = await syncService.sync(start, now, serviceUuid);
|
||||||
|
|
||||||
const newResults = new Map(syncResults);
|
const newResults = new Map(syncResults);
|
||||||
|
const hasMessages = result.messages > 0 || result.newMessages > 0;
|
||||||
newResults.set(serviceUuid, {
|
newResults.set(serviceUuid, {
|
||||||
ok: result.ok,
|
ok: hasMessages,
|
||||||
message: result.ok
|
message: hasMessages
|
||||||
? `Sync OK: ${result.newMessages} nouveaux messages`
|
? `Sync OK: ${result.newMessages} nouveaux messages`
|
||||||
: 'Erreur lors de la synchronisation',
|
: 'Aucun nouveau message',
|
||||||
});
|
});
|
||||||
setSyncResults(newResults);
|
setSyncResults(newResults);
|
||||||
|
|
||||||
@ -148,18 +205,6 @@ export function ServiceSyncScreen(): JSX.Element {
|
|||||||
[identity, getConfig, syncResults, updateConfig, handleError, clearError],
|
[identity, getConfig, syncResults, updateConfig, handleError, clearError],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getFrequencyMs = (frequency: 'min' | 'hour' | 'day'): number => {
|
|
||||||
switch (frequency) {
|
|
||||||
case 'min':
|
|
||||||
return 60000;
|
|
||||||
case 'hour':
|
|
||||||
return 3600000;
|
|
||||||
case 'day':
|
|
||||||
return 86400000;
|
|
||||||
default:
|
|
||||||
return 3600000;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
@ -311,6 +356,25 @@ export function ServiceSyncScreen(): JSX.Element {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(): void => {
|
||||||
|
navigate('/sync');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sync globale
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(): void => {
|
||||||
|
navigate('/');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,7 +125,10 @@ export function SyncScreen(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
|
||||||
|
<button onClick={() => navigate('/service-sync')}>
|
||||||
|
Sync par service
|
||||||
|
</button>
|
||||||
<button onClick={() => navigate('/')}>Retour</button>
|
<button onClick={() => navigate('/')}>Retour</button>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
86
userwallet/src/hooks/useRelayNotifications.ts
Normal file
86
userwallet/src/hooks/useRelayNotifications.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { RelayNotificationService } from '../services/relayNotificationService';
|
||||||
|
import { GraphResolver } from '../services/graphResolver';
|
||||||
|
import { useIdentity } from './useIdentity';
|
||||||
|
import type { RelayHashEvent } from '../services/relayNotificationService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for relay notifications.
|
||||||
|
* Automatically processes new hashes when detected.
|
||||||
|
*/
|
||||||
|
export function useRelayNotifications(
|
||||||
|
graphResolver: GraphResolver,
|
||||||
|
autoProcess: boolean = true,
|
||||||
|
): {
|
||||||
|
notificationService: RelayNotificationService | null;
|
||||||
|
startPolling: (intervalMs?: number) => void;
|
||||||
|
stopPolling: () => void;
|
||||||
|
} {
|
||||||
|
const { identity } = useIdentity();
|
||||||
|
const serviceRef = useRef<RelayNotificationService | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (graphResolver === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceRef.current = new RelayNotificationService(graphResolver, identity ?? null);
|
||||||
|
|
||||||
|
// Auto-process hashes when detected
|
||||||
|
if (autoProcess) {
|
||||||
|
const listener = async (event: RelayHashEvent): Promise<void> => {
|
||||||
|
if (serviceRef.current === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.info(
|
||||||
|
`[RelayNotifications] New hash detected: ${event.hash.slice(0, 16)}... from ${event.relay}`,
|
||||||
|
);
|
||||||
|
// Process hash: fetch message, signatures, keys, decrypt and update graph
|
||||||
|
const result = await serviceRef.current.processHash(event.hash, {
|
||||||
|
fetchMessage: true,
|
||||||
|
fetchSignatures: true,
|
||||||
|
fetchKeys: true,
|
||||||
|
decryptAndUpdateGraph: true,
|
||||||
|
});
|
||||||
|
if (result.graphUpdated) {
|
||||||
|
console.info(
|
||||||
|
`[RelayNotifications] Graph updated from hash ${event.hash.slice(0, 16)}...`,
|
||||||
|
);
|
||||||
|
} else if (result.error !== undefined) {
|
||||||
|
console.warn(
|
||||||
|
`[RelayNotifications] Error processing hash ${event.hash.slice(0, 16)}...: ${result.error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceRef.current.addHashListener(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (serviceRef.current !== null) {
|
||||||
|
serviceRef.current.removeHashListener(listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [graphResolver, identity, autoProcess]);
|
||||||
|
|
||||||
|
const startPolling = useCallback(
|
||||||
|
(intervalMs: number = 60000): void => {
|
||||||
|
if (serviceRef.current !== null) {
|
||||||
|
serviceRef.current.startPolling(intervalMs);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const stopPolling = useCallback((): void => {
|
||||||
|
if (serviceRef.current !== null) {
|
||||||
|
serviceRef.current.stopPolling();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
notificationService: serviceRef.current,
|
||||||
|
startPolling,
|
||||||
|
stopPolling,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -50,6 +50,23 @@ export class GraphResolver {
|
|||||||
*/
|
*/
|
||||||
addContrat(contrat: Contrat): void {
|
addContrat(contrat: Contrat): void {
|
||||||
this.cache.contrats.set(contrat.uuid, contrat);
|
this.cache.contrats.set(contrat.uuid, contrat);
|
||||||
|
// Validate CNIL compliance (warnings only, not blocking)
|
||||||
|
void (async (): Promise<void> => {
|
||||||
|
const { validateContractCNIL } = await import('../utils/cnilValidation');
|
||||||
|
const validation = validateContractCNIL(contrat);
|
||||||
|
if (validation.warnings.length > 0) {
|
||||||
|
console.warn(
|
||||||
|
`[GraphResolver] CNIL warnings for contrat ${contrat.uuid}:`,
|
||||||
|
validation.warnings,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (validation.errors.length > 0) {
|
||||||
|
console.error(
|
||||||
|
`[GraphResolver] CNIL errors for contrat ${contrat.uuid}:`,
|
||||||
|
validation.errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,6 +74,23 @@ export class GraphResolver {
|
|||||||
*/
|
*/
|
||||||
addChamp(champ: Champ): void {
|
addChamp(champ: Champ): void {
|
||||||
this.cache.champs.set(champ.uuid, champ);
|
this.cache.champs.set(champ.uuid, champ);
|
||||||
|
// Validate CNIL compliance (warnings only, not blocking)
|
||||||
|
void (async (): Promise<void> => {
|
||||||
|
const { validateChampCNIL } = await import('../utils/cnilValidation');
|
||||||
|
const validation = validateChampCNIL(champ);
|
||||||
|
if (validation.warnings.length > 0) {
|
||||||
|
console.warn(
|
||||||
|
`[GraphResolver] CNIL warnings for champ ${champ.uuid}:`,
|
||||||
|
validation.warnings,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (validation.errors.length > 0) {
|
||||||
|
console.error(
|
||||||
|
`[GraphResolver] CNIL errors for champ ${champ.uuid}:`,
|
||||||
|
validation.errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { tryDecryptWithKeys } from './syncDecrypt';
|
|||||||
import { updateGraphFromMessage } from './syncUpdateGraph';
|
import { updateGraphFromMessage } from './syncUpdateGraph';
|
||||||
import { validateDecryptedMessage } from './syncValidate';
|
import { validateDecryptedMessage } from './syncValidate';
|
||||||
import type { LocalIdentity } from '../types/identity';
|
import type { LocalIdentity } from '../types/identity';
|
||||||
import type { RelayConfig } from '../types/identity';
|
|
||||||
import type { GraphResolver } from './graphResolver';
|
import type { GraphResolver } from './graphResolver';
|
||||||
import type { MsgChiffre, MsgSignature, MsgCle } from '../types/message';
|
import type { MsgChiffre, MsgSignature, MsgCle } from '../types/message';
|
||||||
|
|
||||||
|
|||||||
163
userwallet/src/utils/cnilValidation.ts
Normal file
163
userwallet/src/utils/cnilValidation.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import type { Contrat, Champ } from '../types/contract';
|
||||||
|
import type { DataJson } from '../types/message';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CNIL validation result.
|
||||||
|
*/
|
||||||
|
export interface CNILValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
warnings: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a contract requires CNIL fields based on its type.
|
||||||
|
*/
|
||||||
|
function contractRequiresCNIL(_contrat: Contrat): boolean {
|
||||||
|
// Tous les contrats nécessitent les champs CNIL selon la politique métier
|
||||||
|
// Peut être ajusté selon les besoins spécifiques
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate CNIL fields in datajson.
|
||||||
|
*/
|
||||||
|
function validateCNILFields(datajson?: DataJson): {
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
warnings: string[];
|
||||||
|
} {
|
||||||
|
const errors: string[] = [];
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
if (datajson === undefined || datajson === null) {
|
||||||
|
errors.push('datajson manquant');
|
||||||
|
return { valid: false, errors, warnings };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier raisons_usage_tiers
|
||||||
|
if (datajson.raisons_usage_tiers !== undefined) {
|
||||||
|
if (!Array.isArray(datajson.raisons_usage_tiers)) {
|
||||||
|
errors.push('raisons_usage_tiers doit être un tableau');
|
||||||
|
} else {
|
||||||
|
for (const item of datajson.raisons_usage_tiers) {
|
||||||
|
if (
|
||||||
|
typeof item !== 'object' ||
|
||||||
|
item === null ||
|
||||||
|
!Array.isArray(item.raisons) ||
|
||||||
|
typeof item.tiers !== 'string'
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
'raisons_usage_tiers: chaque élément doit avoir raisons (string[]) et tiers (string)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier raisons_partage_tiers
|
||||||
|
if (datajson.raisons_partage_tiers !== undefined) {
|
||||||
|
if (!Array.isArray(datajson.raisons_partage_tiers)) {
|
||||||
|
errors.push('raisons_partage_tiers doit être un tableau');
|
||||||
|
} else {
|
||||||
|
for (const item of datajson.raisons_partage_tiers) {
|
||||||
|
if (
|
||||||
|
typeof item !== 'object' ||
|
||||||
|
item === null ||
|
||||||
|
!Array.isArray(item.raisons) ||
|
||||||
|
typeof item.tiers !== 'string'
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
'raisons_partage_tiers: chaque élément doit avoir raisons (string[]) et tiers (string)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier conditions_conservation
|
||||||
|
if (datajson.conditions_conservation !== undefined) {
|
||||||
|
if (typeof datajson.conditions_conservation !== 'object' || datajson.conditions_conservation === null) {
|
||||||
|
errors.push('conditions_conservation doit être un objet');
|
||||||
|
} else {
|
||||||
|
const cc = datajson.conditions_conservation as Record<string, unknown>;
|
||||||
|
if (typeof cc.delai_expiration !== 'string' && typeof cc.delai_expiration !== 'number') {
|
||||||
|
errors.push('conditions_conservation.delai_expiration requis (string ou number)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warnings.push('conditions_conservation non défini (recommandé pour conformité CNIL)');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate CNIL compliance for a contract.
|
||||||
|
* Returns validation result with errors and warnings.
|
||||||
|
*/
|
||||||
|
export function validateContractCNIL(contrat: Contrat): CNILValidationResult {
|
||||||
|
const errors: string[] = [];
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
if (!contractRequiresCNIL(contrat)) {
|
||||||
|
return { valid: true, errors: [], warnings: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const datajson = contrat.datajson;
|
||||||
|
const cnilValidation = validateCNILFields(datajson);
|
||||||
|
|
||||||
|
errors.push(...cnilValidation.errors);
|
||||||
|
warnings.push(...cnilValidation.warnings);
|
||||||
|
|
||||||
|
// Vérifier que les champs obligatoires sont présents si requis par la politique
|
||||||
|
// Pour l'instant, on génère seulement des warnings, pas d'erreurs strictes
|
||||||
|
// La politique métier peut être ajustée ici
|
||||||
|
if (datajson?.raisons_usage_tiers === undefined) {
|
||||||
|
warnings.push('raisons_usage_tiers non défini (recommandé pour conformité CNIL)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (datajson?.raisons_partage_tiers === undefined) {
|
||||||
|
warnings.push('raisons_partage_tiers non défini (recommandé pour conformité CNIL)');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate CNIL compliance for a field (Champ).
|
||||||
|
*/
|
||||||
|
export function validateChampCNIL(champ: Champ): CNILValidationResult {
|
||||||
|
const errors: string[] = [];
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
const datajson = champ.datajson;
|
||||||
|
const cnilValidation = validateCNILFields(datajson);
|
||||||
|
|
||||||
|
errors.push(...cnilValidation.errors);
|
||||||
|
warnings.push(...cnilValidation.warnings);
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if CNIL fields are required for a contract type.
|
||||||
|
* Can be customized based on business policy.
|
||||||
|
*/
|
||||||
|
export function isCNILRequiredForContract(_contrat: Contrat): boolean {
|
||||||
|
// Politique métier : tous les contrats nécessitent les champs CNIL
|
||||||
|
// Peut être ajusté selon le type de contrat
|
||||||
|
return true;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user