@tantainnovative/ndpr-toolkit 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.mjs +16 -0
- package/next.config.js +15 -0
- package/next.config.ts +62 -0
- package/package.json +1 -1
- package/packages/ndpr-toolkit/README.md +467 -0
- package/packages/ndpr-toolkit/dist/components/breach/BreachNotificationManager.d.ts +62 -0
- package/packages/ndpr-toolkit/dist/components/breach/BreachReportForm.d.ts +66 -0
- package/packages/ndpr-toolkit/dist/components/breach/BreachRiskAssessment.d.ts +50 -0
- package/packages/ndpr-toolkit/dist/components/breach/RegulatoryReportGenerator.d.ts +94 -0
- package/packages/ndpr-toolkit/dist/components/consent/ConsentBanner.d.ts +79 -0
- package/packages/ndpr-toolkit/dist/components/consent/ConsentManager.d.ts +73 -0
- package/packages/ndpr-toolkit/dist/components/consent/ConsentStorage.d.ts +41 -0
- package/packages/ndpr-toolkit/dist/components/dpia/DPIAQuestionnaire.d.ts +70 -0
- package/packages/ndpr-toolkit/dist/components/dpia/DPIAReport.d.ts +40 -0
- package/packages/ndpr-toolkit/dist/components/dpia/StepIndicator.d.ts +64 -0
- package/packages/ndpr-toolkit/dist/components/dsr/DSRDashboard.d.ts +58 -0
- package/packages/ndpr-toolkit/dist/components/dsr/DSRRequestForm.d.ts +74 -0
- package/packages/ndpr-toolkit/dist/components/dsr/DSRTracker.d.ts +56 -0
- package/packages/ndpr-toolkit/dist/components/policy/PolicyExporter.d.ts +65 -0
- package/packages/ndpr-toolkit/dist/components/policy/PolicyGenerator.d.ts +54 -0
- package/packages/ndpr-toolkit/dist/components/policy/PolicyPreview.d.ts +71 -0
- package/packages/ndpr-toolkit/dist/hooks/useBreach.d.ts +97 -0
- package/packages/ndpr-toolkit/dist/hooks/useConsent.d.ts +63 -0
- package/packages/ndpr-toolkit/dist/hooks/useDPIA.d.ts +92 -0
- package/packages/ndpr-toolkit/dist/hooks/useDSR.d.ts +72 -0
- package/packages/ndpr-toolkit/dist/hooks/usePrivacyPolicy.d.ts +87 -0
- package/packages/ndpr-toolkit/dist/index.d.ts +31 -0
- package/packages/ndpr-toolkit/dist/index.esm.js +2 -0
- package/packages/ndpr-toolkit/dist/index.esm.js.map +1 -0
- package/packages/ndpr-toolkit/dist/index.js +2 -0
- package/packages/ndpr-toolkit/dist/index.js.map +1 -0
- package/packages/ndpr-toolkit/dist/setupTests.d.ts +2 -0
- package/packages/ndpr-toolkit/dist/types/breach.d.ts +239 -0
- package/packages/ndpr-toolkit/dist/types/consent.d.ts +95 -0
- package/packages/ndpr-toolkit/dist/types/dpia.d.ts +196 -0
- package/packages/ndpr-toolkit/dist/types/dsr.d.ts +162 -0
- package/packages/ndpr-toolkit/dist/types/privacy.d.ts +204 -0
- package/packages/ndpr-toolkit/dist/utils/breach.d.ts +14 -0
- package/packages/ndpr-toolkit/dist/utils/consent.d.ts +10 -0
- package/packages/ndpr-toolkit/dist/utils/dpia.d.ts +12 -0
- package/packages/ndpr-toolkit/dist/utils/dsr.d.ts +11 -0
- package/packages/ndpr-toolkit/dist/utils/privacy.d.ts +12 -0
- package/packages/ndpr-toolkit/package-lock.json +8197 -0
- package/packages/ndpr-toolkit/package.json +71 -0
- package/packages/ndpr-toolkit/rollup.config.js +34 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachNotificationManager.tsx +701 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachReportForm.tsx +631 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachRiskAssessment.tsx +569 -0
- package/packages/ndpr-toolkit/src/components/breach/RegulatoryReportGenerator.tsx +496 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentBanner.tsx +270 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentManager.tsx +217 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentStorage.tsx +206 -0
- package/packages/ndpr-toolkit/src/components/dpia/DPIAQuestionnaire.tsx +342 -0
- package/packages/ndpr-toolkit/src/components/dpia/DPIAReport.tsx +373 -0
- package/packages/ndpr-toolkit/src/components/dpia/StepIndicator.tsx +174 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRDashboard.tsx +717 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRRequestForm.tsx +476 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRTracker.tsx +620 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyExporter.tsx +541 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyGenerator.tsx +454 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyPreview.tsx +333 -0
- package/packages/ndpr-toolkit/src/hooks/useBreach.ts +409 -0
- package/packages/ndpr-toolkit/src/hooks/useConsent.ts +263 -0
- package/packages/ndpr-toolkit/src/hooks/useDPIA.ts +457 -0
- package/packages/ndpr-toolkit/src/hooks/useDSR.ts +236 -0
- package/packages/ndpr-toolkit/src/hooks/usePrivacyPolicy.ts +428 -0
- package/packages/ndpr-toolkit/src/index.ts +44 -0
- package/packages/ndpr-toolkit/src/setupTests.ts +5 -0
- package/packages/ndpr-toolkit/src/types/breach.ts +283 -0
- package/packages/ndpr-toolkit/src/types/consent.ts +111 -0
- package/packages/ndpr-toolkit/src/types/dpia.ts +236 -0
- package/packages/ndpr-toolkit/src/types/dsr.ts +192 -0
- package/packages/ndpr-toolkit/src/types/index.ts +42 -0
- package/packages/ndpr-toolkit/src/types/privacy.ts +246 -0
- package/packages/ndpr-toolkit/src/utils/breach.ts +122 -0
- package/packages/ndpr-toolkit/src/utils/consent.ts +51 -0
- package/packages/ndpr-toolkit/src/utils/dpia.ts +104 -0
- package/packages/ndpr-toolkit/src/utils/dsr.ts +77 -0
- package/packages/ndpr-toolkit/src/utils/privacy.ts +100 -0
- package/packages/ndpr-toolkit/tsconfig.json +23 -0
- package/postcss.config.mjs +5 -0
- package/src/app/accessibility.css +70 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +123 -0
- package/src/app/layout.tsx +37 -0
- package/src/app/ndpr-demos/breach/page.tsx +354 -0
- package/src/app/ndpr-demos/consent/page.tsx +366 -0
- package/src/app/ndpr-demos/dpia/page.tsx +495 -0
- package/src/app/ndpr-demos/dsr/page.tsx +280 -0
- package/src/app/ndpr-demos/page.tsx +73 -0
- package/src/app/ndpr-demos/policy/page.tsx +771 -0
- package/src/app/page.tsx +452 -0
- package/src/components/ErrorBoundary.tsx +90 -0
- package/src/components/breach-notification/BreachNotificationForm.tsx +479 -0
- package/src/components/consent/ConsentBanner.tsx +193 -0
- package/src/components/data-subject-rights/DataSubjectRequestForm.tsx +530 -0
- package/src/components/dpia/DPIAQuestionnaire.tsx +523 -0
- package/src/components/privacy-policy/PolicyGenerator.tsx +1062 -0
- package/src/components/privacy-policy/data.ts +98 -0
- package/src/components/privacy-policy/shared/CheckboxField.tsx +38 -0
- package/src/components/privacy-policy/shared/CheckboxGroup.tsx +85 -0
- package/src/components/privacy-policy/shared/FormField.tsx +79 -0
- package/src/components/privacy-policy/shared/StepIndicator.tsx +86 -0
- package/src/components/privacy-policy/steps/CustomSectionsStep.tsx +361 -0
- package/src/components/privacy-policy/steps/DataCollectionStep.tsx +231 -0
- package/src/components/privacy-policy/steps/DataSharingStep.tsx +418 -0
- package/src/components/privacy-policy/steps/OrganizationInfoStep.tsx +202 -0
- package/src/components/privacy-policy/steps/PolicyPreviewStep.tsx +226 -0
- package/src/components/ui/Badge.tsx +46 -0
- package/src/components/ui/Button.tsx +59 -0
- package/src/components/ui/Card.tsx +92 -0
- package/src/components/ui/Checkbox.tsx +57 -0
- package/src/components/ui/FormField.tsx +50 -0
- package/src/components/ui/Input.tsx +38 -0
- package/src/components/ui/Loading.tsx +201 -0
- package/src/components/ui/Select.tsx +42 -0
- package/src/components/ui/TextArea.tsx +38 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/hooks/useConsent.ts +70 -0
- package/src/hooks/useLoadingState.ts +85 -0
- package/src/lib/consentService.ts +144 -0
- package/src/lib/dpiaQuestions.ts +188 -0
- package/src/lib/requestService.ts +79 -0
- package/src/lib/sanitize.ts +108 -0
- package/src/lib/storage.ts +222 -0
- package/src/lib/utils.ts +6 -0
- package/src/types/html-to-docx.d.ts +30 -0
- package/src/types/index.ts +77 -0
- package/tailwind.config.ts +65 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { BreachReport, BreachCategory, RiskAssessment, NotificationRequirement, RegulatoryNotification } from '../types/breach';
|
|
3
|
+
import { calculateBreachSeverity } from '../utils/breach';
|
|
4
|
+
|
|
5
|
+
interface UseBreachOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Available breach categories
|
|
8
|
+
*/
|
|
9
|
+
categories: BreachCategory[];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initial breach reports
|
|
13
|
+
*/
|
|
14
|
+
initialReports?: BreachReport[];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Storage key for breach data
|
|
18
|
+
* @default "ndpr_breach_data"
|
|
19
|
+
*/
|
|
20
|
+
storageKey?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether to use local storage to persist breach data
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
useLocalStorage?: boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Callback function called when a breach is reported
|
|
30
|
+
*/
|
|
31
|
+
onReport?: (report: BreachReport) => void;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Callback function called when a risk assessment is completed
|
|
35
|
+
*/
|
|
36
|
+
onAssessment?: (assessment: RiskAssessment) => void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Callback function called when a notification is sent
|
|
40
|
+
*/
|
|
41
|
+
onNotification?: (notification: RegulatoryNotification) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface UseBreachReturn {
|
|
45
|
+
/**
|
|
46
|
+
* All breach reports
|
|
47
|
+
*/
|
|
48
|
+
reports: BreachReport[];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* All risk assessments
|
|
52
|
+
*/
|
|
53
|
+
assessments: RiskAssessment[];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* All regulatory notifications
|
|
57
|
+
*/
|
|
58
|
+
notifications: RegulatoryNotification[];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Submit a new breach report
|
|
62
|
+
*/
|
|
63
|
+
reportBreach: (reportData: Omit<BreachReport, 'id' | 'reportedAt'>) => BreachReport;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Update an existing breach report
|
|
67
|
+
*/
|
|
68
|
+
updateReport: (id: string, updates: Partial<BreachReport>) => BreachReport | null;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get a breach report by ID
|
|
72
|
+
*/
|
|
73
|
+
getReport: (id: string) => BreachReport | null;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Conduct a risk assessment for a breach
|
|
77
|
+
*/
|
|
78
|
+
assessRisk: (breachId: string, assessmentData: Omit<RiskAssessment, 'id' | 'breachId' | 'assessedAt'>) => RiskAssessment;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get a risk assessment for a breach
|
|
82
|
+
*/
|
|
83
|
+
getAssessment: (breachId: string) => RiskAssessment | null;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Calculate notification requirements based on a risk assessment
|
|
87
|
+
*/
|
|
88
|
+
calculateNotificationRequirements: (breachId: string) => NotificationRequirement | null;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Send a regulatory notification
|
|
92
|
+
*/
|
|
93
|
+
sendNotification: (breachId: string, notificationData: Omit<RegulatoryNotification, 'id' | 'breachId' | 'sentAt'>) => RegulatoryNotification;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get a regulatory notification for a breach
|
|
97
|
+
*/
|
|
98
|
+
getNotification: (breachId: string) => RegulatoryNotification | null;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get breaches that require notification within the next X hours
|
|
102
|
+
*/
|
|
103
|
+
getBreachesRequiringNotification: (hoursThreshold?: number) => Array<{
|
|
104
|
+
report: BreachReport;
|
|
105
|
+
assessment: RiskAssessment;
|
|
106
|
+
requirements: NotificationRequirement;
|
|
107
|
+
hoursRemaining: number;
|
|
108
|
+
}>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Clear all breach data
|
|
112
|
+
*/
|
|
113
|
+
clearBreachData: () => void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Hook for managing data breach notifications in compliance with NDPR
|
|
118
|
+
*/
|
|
119
|
+
export function useBreach({
|
|
120
|
+
categories,
|
|
121
|
+
initialReports = [],
|
|
122
|
+
storageKey = "ndpr_breach_data",
|
|
123
|
+
useLocalStorage = true,
|
|
124
|
+
onReport,
|
|
125
|
+
onAssessment,
|
|
126
|
+
onNotification
|
|
127
|
+
}: UseBreachOptions): UseBreachReturn {
|
|
128
|
+
const [reports, setReports] = useState<BreachReport[]>(initialReports);
|
|
129
|
+
const [assessments, setAssessments] = useState<RiskAssessment[]>([]);
|
|
130
|
+
const [notifications, setNotifications] = useState<RegulatoryNotification[]>([]);
|
|
131
|
+
|
|
132
|
+
// Load breach data from storage on mount
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (useLocalStorage && typeof window !== 'undefined') {
|
|
135
|
+
try {
|
|
136
|
+
const savedData = localStorage.getItem(storageKey);
|
|
137
|
+
if (savedData) {
|
|
138
|
+
const { reports, assessments, notifications } = JSON.parse(savedData);
|
|
139
|
+
setReports(reports || []);
|
|
140
|
+
setAssessments(assessments || []);
|
|
141
|
+
setNotifications(notifications || []);
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Error loading breach data:', error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}, [storageKey, useLocalStorage]);
|
|
148
|
+
|
|
149
|
+
// Save breach data to storage when it changes
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (useLocalStorage && typeof window !== 'undefined') {
|
|
152
|
+
try {
|
|
153
|
+
localStorage.setItem(storageKey, JSON.stringify({
|
|
154
|
+
reports,
|
|
155
|
+
assessments,
|
|
156
|
+
notifications
|
|
157
|
+
}));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error('Error saving breach data:', error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}, [reports, assessments, notifications, storageKey, useLocalStorage]);
|
|
163
|
+
|
|
164
|
+
// Generate a unique ID
|
|
165
|
+
const generateId = (prefix: string): string => {
|
|
166
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Submit a new breach report
|
|
170
|
+
const reportBreach = (reportData: Omit<BreachReport, 'id' | 'reportedAt'>): BreachReport => {
|
|
171
|
+
const newReport: BreachReport = {
|
|
172
|
+
id: generateId('breach'),
|
|
173
|
+
reportedAt: Date.now(),
|
|
174
|
+
...reportData
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
setReports(prevReports => [...prevReports, newReport]);
|
|
178
|
+
|
|
179
|
+
if (onReport) {
|
|
180
|
+
onReport(newReport);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return newReport;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Update an existing breach report
|
|
187
|
+
const updateReport = (id: string, updates: Partial<BreachReport>): BreachReport | null => {
|
|
188
|
+
let updatedReport: BreachReport | null = null;
|
|
189
|
+
|
|
190
|
+
setReports(prevReports => {
|
|
191
|
+
const index = prevReports.findIndex(report => report.id === id);
|
|
192
|
+
|
|
193
|
+
if (index === -1) {
|
|
194
|
+
return prevReports;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const report = prevReports[index];
|
|
198
|
+
updatedReport = {
|
|
199
|
+
...report,
|
|
200
|
+
...updates
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const newReports = [...prevReports];
|
|
204
|
+
newReports[index] = updatedReport as BreachReport;
|
|
205
|
+
|
|
206
|
+
return newReports;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return updatedReport;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Get a breach report by ID
|
|
213
|
+
const getReport = (id: string): BreachReport | null => {
|
|
214
|
+
return reports.find(report => report.id === id) || null;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Conduct a risk assessment for a breach
|
|
218
|
+
const assessRisk = (breachId: string, assessmentData: Omit<RiskAssessment, 'id' | 'breachId' | 'assessedAt'>): RiskAssessment => {
|
|
219
|
+
// Check if an assessment already exists for this breach
|
|
220
|
+
const existingAssessment = assessments.find(assessment => assessment.breachId === breachId);
|
|
221
|
+
|
|
222
|
+
if (existingAssessment) {
|
|
223
|
+
// Update the existing assessment
|
|
224
|
+
const updatedAssessment: RiskAssessment = {
|
|
225
|
+
...existingAssessment,
|
|
226
|
+
...assessmentData,
|
|
227
|
+
assessedAt: Date.now()
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
setAssessments(prevAssessments =>
|
|
231
|
+
prevAssessments.map(assessment =>
|
|
232
|
+
assessment.id === existingAssessment.id ? updatedAssessment : assessment
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (onAssessment) {
|
|
237
|
+
onAssessment(updatedAssessment);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return updatedAssessment;
|
|
241
|
+
} else {
|
|
242
|
+
// Create a new assessment
|
|
243
|
+
const newAssessment: RiskAssessment = {
|
|
244
|
+
id: generateId('assessment'),
|
|
245
|
+
breachId,
|
|
246
|
+
assessedAt: Date.now(),
|
|
247
|
+
...assessmentData
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
setAssessments(prevAssessments => [...prevAssessments, newAssessment]);
|
|
251
|
+
|
|
252
|
+
if (onAssessment) {
|
|
253
|
+
onAssessment(newAssessment);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return newAssessment;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Get a risk assessment for a breach
|
|
261
|
+
const getAssessment = (breachId: string): RiskAssessment | null => {
|
|
262
|
+
return assessments.find(assessment => assessment.breachId === breachId) || null;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Calculate notification requirements based on a risk assessment
|
|
266
|
+
const calculateNotificationRequirements = (breachId: string): NotificationRequirement | null => {
|
|
267
|
+
const report = getReport(breachId);
|
|
268
|
+
const assessment = getAssessment(breachId);
|
|
269
|
+
|
|
270
|
+
if (!report) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const { severityLevel, notificationRequired, timeframeHours, justification } = calculateBreachSeverity(report, assessment || undefined);
|
|
275
|
+
|
|
276
|
+
// Calculate the deadline (72 hours from discovery under NDPR)
|
|
277
|
+
const deadline = report.discoveredAt + (timeframeHours * 60 * 60 * 1000);
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
nitdaNotificationRequired: notificationRequired,
|
|
281
|
+
nitdaNotificationDeadline: deadline,
|
|
282
|
+
dataSubjectNotificationRequired: severityLevel === 'high' || severityLevel === 'critical',
|
|
283
|
+
justification
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Send a regulatory notification
|
|
288
|
+
const sendNotification = (breachId: string, notificationData: Omit<RegulatoryNotification, 'id' | 'breachId' | 'sentAt'>): RegulatoryNotification => {
|
|
289
|
+
// Check if a notification already exists for this breach
|
|
290
|
+
const existingNotification = notifications.find(notification => notification.breachId === breachId);
|
|
291
|
+
|
|
292
|
+
if (existingNotification) {
|
|
293
|
+
// Update the existing notification
|
|
294
|
+
const updatedNotification: RegulatoryNotification = {
|
|
295
|
+
...existingNotification,
|
|
296
|
+
...notificationData,
|
|
297
|
+
sentAt: Date.now()
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
setNotifications(prevNotifications =>
|
|
301
|
+
prevNotifications.map(notification =>
|
|
302
|
+
notification.id === existingNotification.id ? updatedNotification : notification
|
|
303
|
+
)
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
if (onNotification) {
|
|
307
|
+
onNotification(updatedNotification);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return updatedNotification;
|
|
311
|
+
} else {
|
|
312
|
+
// Create a new notification
|
|
313
|
+
const newNotification: RegulatoryNotification = {
|
|
314
|
+
id: generateId('notification'),
|
|
315
|
+
breachId,
|
|
316
|
+
sentAt: Date.now(),
|
|
317
|
+
...notificationData
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
setNotifications(prevNotifications => [...prevNotifications, newNotification]);
|
|
321
|
+
|
|
322
|
+
if (onNotification) {
|
|
323
|
+
onNotification(newNotification);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return newNotification;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// Get a regulatory notification for a breach
|
|
331
|
+
const getNotification = (breachId: string): RegulatoryNotification | null => {
|
|
332
|
+
return notifications.find(notification => notification.breachId === breachId) || null;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// Get breaches that require notification within the next X hours
|
|
336
|
+
const getBreachesRequiringNotification = (hoursThreshold = 24): Array<{
|
|
337
|
+
report: BreachReport;
|
|
338
|
+
assessment: RiskAssessment;
|
|
339
|
+
requirements: NotificationRequirement;
|
|
340
|
+
hoursRemaining: number;
|
|
341
|
+
}> => {
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
const result: Array<{
|
|
344
|
+
report: BreachReport;
|
|
345
|
+
assessment: RiskAssessment;
|
|
346
|
+
requirements: NotificationRequirement;
|
|
347
|
+
hoursRemaining: number;
|
|
348
|
+
}> = [];
|
|
349
|
+
|
|
350
|
+
reports.forEach(report => {
|
|
351
|
+
// Skip if a notification has already been sent
|
|
352
|
+
if (notifications.some(notification => notification.breachId === report.id)) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const assessment = getAssessment(report.id);
|
|
357
|
+
if (!assessment) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const requirements = calculateNotificationRequirements(report.id);
|
|
362
|
+
if (!requirements || !requirements.nitdaNotificationRequired) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const timeRemaining = requirements.nitdaNotificationDeadline - now;
|
|
367
|
+
const hoursRemaining = timeRemaining / (60 * 60 * 1000);
|
|
368
|
+
|
|
369
|
+
if (hoursRemaining <= hoursThreshold) {
|
|
370
|
+
result.push({
|
|
371
|
+
report,
|
|
372
|
+
assessment,
|
|
373
|
+
requirements,
|
|
374
|
+
hoursRemaining
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Sort by hours remaining (ascending)
|
|
380
|
+
return result.sort((a, b) => a.hoursRemaining - b.hoursRemaining);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// Clear all breach data
|
|
384
|
+
const clearBreachData = () => {
|
|
385
|
+
setReports([]);
|
|
386
|
+
setAssessments([]);
|
|
387
|
+
setNotifications([]);
|
|
388
|
+
|
|
389
|
+
if (useLocalStorage && typeof window !== 'undefined') {
|
|
390
|
+
localStorage.removeItem(storageKey);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
reports,
|
|
396
|
+
assessments,
|
|
397
|
+
notifications,
|
|
398
|
+
reportBreach,
|
|
399
|
+
updateReport,
|
|
400
|
+
getReport,
|
|
401
|
+
assessRisk,
|
|
402
|
+
getAssessment,
|
|
403
|
+
calculateNotificationRequirements,
|
|
404
|
+
sendNotification,
|
|
405
|
+
getNotification,
|
|
406
|
+
getBreachesRequiringNotification,
|
|
407
|
+
clearBreachData
|
|
408
|
+
};
|
|
409
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { ConsentOption, ConsentSettings, ConsentStorageOptions } from '../types/consent';
|
|
3
|
+
import { validateConsent } from '../utils/consent';
|
|
4
|
+
|
|
5
|
+
interface UseConsentOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Consent options to present to the user
|
|
8
|
+
*/
|
|
9
|
+
options: ConsentOption[];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Storage options for consent settings
|
|
13
|
+
*/
|
|
14
|
+
storageOptions?: ConsentStorageOptions;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Version of the consent form
|
|
18
|
+
* @default "1.0"
|
|
19
|
+
*/
|
|
20
|
+
version?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Callback function called when consent settings change
|
|
24
|
+
*/
|
|
25
|
+
onChange?: (settings: ConsentSettings) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface UseConsentReturn {
|
|
29
|
+
/**
|
|
30
|
+
* Current consent settings
|
|
31
|
+
*/
|
|
32
|
+
settings: ConsentSettings | null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether consent has been given for a specific option
|
|
36
|
+
*/
|
|
37
|
+
hasConsent: (optionId: string) => boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Update consent settings
|
|
41
|
+
*/
|
|
42
|
+
updateConsent: (consents: Record<string, boolean>) => void;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Accept all consent options
|
|
46
|
+
*/
|
|
47
|
+
acceptAll: () => void;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reject all non-required consent options
|
|
51
|
+
*/
|
|
52
|
+
rejectAll: () => void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether the consent banner should be shown
|
|
56
|
+
*/
|
|
57
|
+
shouldShowBanner: boolean;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether consent settings are valid
|
|
61
|
+
*/
|
|
62
|
+
isValid: boolean;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validation errors (if any)
|
|
66
|
+
*/
|
|
67
|
+
validationErrors: string[];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reset consent settings (clear from storage)
|
|
71
|
+
*/
|
|
72
|
+
resetConsent: () => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Hook for managing user consent in compliance with NDPR
|
|
77
|
+
*/
|
|
78
|
+
export function useConsent({
|
|
79
|
+
options,
|
|
80
|
+
storageOptions = {},
|
|
81
|
+
version = "1.0",
|
|
82
|
+
onChange
|
|
83
|
+
}: UseConsentOptions): UseConsentReturn {
|
|
84
|
+
const {
|
|
85
|
+
storageKey = "ndpr_consent",
|
|
86
|
+
storageType = "localStorage"
|
|
87
|
+
} = storageOptions;
|
|
88
|
+
|
|
89
|
+
const [settings, setSettings] = useState<ConsentSettings | null>(null);
|
|
90
|
+
const [shouldShowBanner, setShouldShowBanner] = useState<boolean>(true);
|
|
91
|
+
const [isValid, setIsValid] = useState<boolean>(false);
|
|
92
|
+
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
|
93
|
+
|
|
94
|
+
// Load consent settings from storage on mount
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
let savedSettings: ConsentSettings | null = null;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
if (storageType === 'localStorage' && typeof window !== 'undefined') {
|
|
100
|
+
const savedData = localStorage.getItem(storageKey);
|
|
101
|
+
if (savedData) {
|
|
102
|
+
savedSettings = JSON.parse(savedData);
|
|
103
|
+
}
|
|
104
|
+
} else if (storageType === 'sessionStorage' && typeof window !== 'undefined') {
|
|
105
|
+
const savedData = sessionStorage.getItem(storageKey);
|
|
106
|
+
if (savedData) {
|
|
107
|
+
savedSettings = JSON.parse(savedData);
|
|
108
|
+
}
|
|
109
|
+
} else if (storageType === 'cookie' && typeof document !== 'undefined') {
|
|
110
|
+
const cookies = document.cookie.split(';');
|
|
111
|
+
const consentCookie = cookies.find(cookie => cookie.trim().startsWith(`${storageKey}=`));
|
|
112
|
+
if (consentCookie) {
|
|
113
|
+
const cookieValue = consentCookie.split('=')[1];
|
|
114
|
+
savedSettings = JSON.parse(decodeURIComponent(cookieValue));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('Error loading consent settings:', error);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (savedSettings) {
|
|
122
|
+
setSettings(savedSettings);
|
|
123
|
+
|
|
124
|
+
// Validate the saved settings
|
|
125
|
+
const { valid, errors } = validateConsent(savedSettings);
|
|
126
|
+
setIsValid(valid);
|
|
127
|
+
setValidationErrors(errors);
|
|
128
|
+
|
|
129
|
+
// Only hide banner if settings are valid and for the current version
|
|
130
|
+
setShouldShowBanner(!(valid && savedSettings.version === version));
|
|
131
|
+
} else {
|
|
132
|
+
setShouldShowBanner(true);
|
|
133
|
+
}
|
|
134
|
+
}, [storageKey, storageType, version]);
|
|
135
|
+
|
|
136
|
+
// Save settings to storage
|
|
137
|
+
const saveSettings = (newSettings: ConsentSettings) => {
|
|
138
|
+
try {
|
|
139
|
+
const settingsString = JSON.stringify(newSettings);
|
|
140
|
+
|
|
141
|
+
if (storageType === 'localStorage' && typeof window !== 'undefined') {
|
|
142
|
+
localStorage.setItem(storageKey, settingsString);
|
|
143
|
+
} else if (storageType === 'sessionStorage' && typeof window !== 'undefined') {
|
|
144
|
+
sessionStorage.setItem(storageKey, settingsString);
|
|
145
|
+
} else if (storageType === 'cookie' && typeof document !== 'undefined') {
|
|
146
|
+
const { cookieOptions = {} } = storageOptions;
|
|
147
|
+
const {
|
|
148
|
+
domain,
|
|
149
|
+
path = '/',
|
|
150
|
+
expires = 365,
|
|
151
|
+
secure = true,
|
|
152
|
+
sameSite = 'Lax'
|
|
153
|
+
} = cookieOptions;
|
|
154
|
+
|
|
155
|
+
const expiryDate = new Date();
|
|
156
|
+
expiryDate.setDate(expiryDate.getDate() + expires);
|
|
157
|
+
|
|
158
|
+
let cookieString = `${storageKey}=${encodeURIComponent(settingsString)}; path=${path}; expires=${expiryDate.toUTCString()}`;
|
|
159
|
+
|
|
160
|
+
if (domain) {
|
|
161
|
+
cookieString += `; domain=${domain}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (secure) {
|
|
165
|
+
cookieString += '; secure';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
cookieString += `; samesite=${sameSite}`;
|
|
169
|
+
|
|
170
|
+
document.cookie = cookieString;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Validate the new settings
|
|
174
|
+
const { valid, errors } = validateConsent(newSettings);
|
|
175
|
+
setIsValid(valid);
|
|
176
|
+
setValidationErrors(errors);
|
|
177
|
+
|
|
178
|
+
// Call onChange callback if provided
|
|
179
|
+
if (onChange) {
|
|
180
|
+
onChange(newSettings);
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error('Error saving consent settings:', error);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Update consent settings
|
|
188
|
+
const updateConsent = (consents: Record<string, boolean>) => {
|
|
189
|
+
const newSettings: ConsentSettings = {
|
|
190
|
+
consents,
|
|
191
|
+
timestamp: Date.now(),
|
|
192
|
+
version,
|
|
193
|
+
method: 'explicit',
|
|
194
|
+
hasInteracted: true
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
setSettings(newSettings);
|
|
198
|
+
saveSettings(newSettings);
|
|
199
|
+
setShouldShowBanner(false);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Accept all consent options
|
|
203
|
+
const acceptAll = () => {
|
|
204
|
+
const allConsents: Record<string, boolean> = {};
|
|
205
|
+
options.forEach(option => {
|
|
206
|
+
allConsents[option.id] = true;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
updateConsent(allConsents);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Reject all non-required consent options
|
|
213
|
+
const rejectAll = () => {
|
|
214
|
+
const rejectedConsents: Record<string, boolean> = {};
|
|
215
|
+
options.forEach(option => {
|
|
216
|
+
rejectedConsents[option.id] = option.required || false;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
updateConsent(rejectedConsents);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Check if consent has been given for a specific option
|
|
223
|
+
const hasConsent = (optionId: string): boolean => {
|
|
224
|
+
return !!settings?.consents[optionId];
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Reset consent settings
|
|
228
|
+
const resetConsent = () => {
|
|
229
|
+
if (storageType === 'localStorage' && typeof window !== 'undefined') {
|
|
230
|
+
localStorage.removeItem(storageKey);
|
|
231
|
+
} else if (storageType === 'sessionStorage' && typeof window !== 'undefined') {
|
|
232
|
+
sessionStorage.removeItem(storageKey);
|
|
233
|
+
} else if (storageType === 'cookie' && typeof document !== 'undefined') {
|
|
234
|
+
const { cookieOptions = {} } = storageOptions;
|
|
235
|
+
const { domain, path = '/' } = cookieOptions;
|
|
236
|
+
|
|
237
|
+
let cookieString = `${storageKey}=; path=${path}; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
238
|
+
|
|
239
|
+
if (domain) {
|
|
240
|
+
cookieString += `; domain=${domain}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
document.cookie = cookieString;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
setSettings(null);
|
|
247
|
+
setShouldShowBanner(true);
|
|
248
|
+
setIsValid(false);
|
|
249
|
+
setValidationErrors([]);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
settings,
|
|
254
|
+
hasConsent,
|
|
255
|
+
updateConsent,
|
|
256
|
+
acceptAll,
|
|
257
|
+
rejectAll,
|
|
258
|
+
shouldShowBanner,
|
|
259
|
+
isValid,
|
|
260
|
+
validationErrors,
|
|
261
|
+
resetConsent
|
|
262
|
+
};
|
|
263
|
+
}
|