create-crm-tmp 1.0.0

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.
Files changed (187) hide show
  1. package/bin/create-crm-tmp.js +93 -0
  2. package/package.json +25 -0
  3. package/template/.prettierignore +33 -0
  4. package/template/.prettierrc.json +25 -0
  5. package/template/README.md +173 -0
  6. package/template/eslint.config.mjs +18 -0
  7. package/template/exemple-contacts.csv +11 -0
  8. package/template/next.config.ts +8 -0
  9. package/template/package.json +64 -0
  10. package/template/postcss.config.mjs +7 -0
  11. package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
  12. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
  13. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
  14. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
  15. package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
  16. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
  17. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
  18. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
  19. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
  20. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
  21. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
  22. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
  23. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
  24. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
  25. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
  26. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
  27. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
  28. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
  29. package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
  30. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
  31. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
  32. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
  33. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
  34. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
  35. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
  36. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
  37. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
  38. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
  39. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
  40. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
  41. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
  42. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
  43. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
  44. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
  45. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
  46. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
  47. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
  48. package/template/prisma/migrations/migration_lock.toml +3 -0
  49. package/template/prisma/schema.prisma +582 -0
  50. package/template/prisma.config.ts +14 -0
  51. package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
  52. package/template/src/app/(auth)/layout.tsx +3 -0
  53. package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
  54. package/template/src/app/(auth)/reset-password/page.tsx +146 -0
  55. package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
  56. package/template/src/app/(auth)/signin/page.tsx +166 -0
  57. package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
  58. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
  59. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
  60. package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
  61. package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
  62. package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
  63. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
  64. package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
  65. package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
  66. package/template/src/app/(dashboard)/layout.tsx +30 -0
  67. package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
  68. package/template/src/app/(dashboard)/templates/page.tsx +567 -0
  69. package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
  70. package/template/src/app/(dashboard)/users/page.tsx +457 -0
  71. package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
  72. package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
  73. package/template/src/app/api/audit-logs/route.ts +57 -0
  74. package/template/src/app/api/auth/[...all]/route.ts +4 -0
  75. package/template/src/app/api/auth/check-active/route.ts +31 -0
  76. package/template/src/app/api/auth/google/callback/route.ts +94 -0
  77. package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
  78. package/template/src/app/api/auth/google/route.ts +34 -0
  79. package/template/src/app/api/auth/google/status/route.ts +32 -0
  80. package/template/src/app/api/closing-reasons/route.ts +27 -0
  81. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
  82. package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
  83. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
  84. package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
  85. package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
  86. package/template/src/app/api/contacts/[id]/route.ts +322 -0
  87. package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
  88. package/template/src/app/api/contacts/export/route.ts +270 -0
  89. package/template/src/app/api/contacts/import/route.ts +381 -0
  90. package/template/src/app/api/contacts/route.ts +283 -0
  91. package/template/src/app/api/dashboard/stats/route.ts +299 -0
  92. package/template/src/app/api/email/track/[id]/route.ts +68 -0
  93. package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
  94. package/template/src/app/api/invite/complete/route.ts +88 -0
  95. package/template/src/app/api/invite/validate/route.ts +55 -0
  96. package/template/src/app/api/reminders/route.ts +95 -0
  97. package/template/src/app/api/reset-password/complete/route.ts +73 -0
  98. package/template/src/app/api/reset-password/request/route.ts +84 -0
  99. package/template/src/app/api/reset-password/validate/route.ts +49 -0
  100. package/template/src/app/api/reset-password/verify/route.ts +74 -0
  101. package/template/src/app/api/roles/[id]/route.ts +183 -0
  102. package/template/src/app/api/roles/route.ts +140 -0
  103. package/template/src/app/api/send/route.ts +282 -0
  104. package/template/src/app/api/settings/change-password/route.ts +95 -0
  105. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
  106. package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
  107. package/template/src/app/api/settings/company/route.ts +121 -0
  108. package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
  109. package/template/src/app/api/settings/google-ads/route.ts +122 -0
  110. package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
  111. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
  112. package/template/src/app/api/settings/google-sheet/route.ts +254 -0
  113. package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
  114. package/template/src/app/api/settings/meta-leads/route.ts +132 -0
  115. package/template/src/app/api/settings/profile/route.ts +42 -0
  116. package/template/src/app/api/settings/smtp/route.ts +130 -0
  117. package/template/src/app/api/settings/smtp/test/route.ts +121 -0
  118. package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
  119. package/template/src/app/api/settings/statuses/route.ts +83 -0
  120. package/template/src/app/api/statuses/route.ts +25 -0
  121. package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
  122. package/template/src/app/api/tasks/[id]/route.ts +728 -0
  123. package/template/src/app/api/tasks/meet/route.ts +240 -0
  124. package/template/src/app/api/tasks/route.ts +417 -0
  125. package/template/src/app/api/templates/[id]/route.ts +140 -0
  126. package/template/src/app/api/templates/route.ts +91 -0
  127. package/template/src/app/api/users/[id]/route.ts +168 -0
  128. package/template/src/app/api/users/list/route.ts +45 -0
  129. package/template/src/app/api/users/me/route.ts +48 -0
  130. package/template/src/app/api/users/route.ts +250 -0
  131. package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
  132. package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
  133. package/template/src/app/api/workflows/[id]/route.ts +192 -0
  134. package/template/src/app/api/workflows/process/route.ts +293 -0
  135. package/template/src/app/api/workflows/route.ts +124 -0
  136. package/template/src/app/favicon.ico +0 -0
  137. package/template/src/app/globals.css +1416 -0
  138. package/template/src/app/layout.tsx +31 -0
  139. package/template/src/app/page.tsx +32 -0
  140. package/template/src/components/dashboard/activity-chart.tsx +67 -0
  141. package/template/src/components/dashboard/contacts-chart.tsx +63 -0
  142. package/template/src/components/dashboard/recent-activity.tsx +164 -0
  143. package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
  144. package/template/src/components/dashboard/stat-card.tsx +61 -0
  145. package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
  146. package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
  147. package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
  148. package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
  149. package/template/src/components/editor.tsx +856 -0
  150. package/template/src/components/email-template.tsx +35 -0
  151. package/template/src/components/header.tsx +320 -0
  152. package/template/src/components/invitation-email-template.tsx +79 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +120 -0
  154. package/template/src/components/meet-confirmation-email-template.tsx +156 -0
  155. package/template/src/components/meet-update-email-template.tsx +209 -0
  156. package/template/src/components/page-header.tsx +61 -0
  157. package/template/src/components/reset-password-email-template.tsx +79 -0
  158. package/template/src/components/sidebar.tsx +294 -0
  159. package/template/src/components/skeleton.tsx +380 -0
  160. package/template/src/components/ui/commands.tsx +396 -0
  161. package/template/src/components/ui/components.tsx +150 -0
  162. package/template/src/components/ui/theme.tsx +5 -0
  163. package/template/src/components/view-as-banner.tsx +45 -0
  164. package/template/src/components/view-as-modal.tsx +186 -0
  165. package/template/src/contexts/mobile-menu-context.tsx +31 -0
  166. package/template/src/contexts/sidebar-context.tsx +107 -0
  167. package/template/src/contexts/task-reminder-context.tsx +239 -0
  168. package/template/src/contexts/view-as-context.tsx +84 -0
  169. package/template/src/hooks/use-user-role.ts +82 -0
  170. package/template/src/lib/audit-log.ts +45 -0
  171. package/template/src/lib/auth-client.ts +16 -0
  172. package/template/src/lib/auth.ts +35 -0
  173. package/template/src/lib/check-permission.ts +193 -0
  174. package/template/src/lib/contact-duplicate.ts +112 -0
  175. package/template/src/lib/contact-interactions.ts +371 -0
  176. package/template/src/lib/encryption.ts +99 -0
  177. package/template/src/lib/google-calendar.ts +300 -0
  178. package/template/src/lib/google-drive.ts +372 -0
  179. package/template/src/lib/permissions.ts +412 -0
  180. package/template/src/lib/prisma.ts +32 -0
  181. package/template/src/lib/roles.ts +120 -0
  182. package/template/src/lib/template-variables.ts +76 -0
  183. package/template/src/lib/utils.ts +46 -0
  184. package/template/src/lib/workflow-executor.ts +482 -0
  185. package/template/src/proxy.ts +91 -0
  186. package/template/tsconfig.json +34 -0
  187. package/template/vercel.json +8 -0
@@ -0,0 +1,371 @@
1
+ import { prisma } from '@/lib/prisma';
2
+ import { InteractionType } from '../../generated/prisma/client';
3
+
4
+ interface CreateInteractionParams {
5
+ contactId: string;
6
+ type: InteractionType;
7
+ title?: string | null;
8
+ content: string;
9
+ userId: string;
10
+ date?: Date | null;
11
+ metadata?: Record<string, any>;
12
+ }
13
+
14
+ /**
15
+ * Crée une interaction pour un contact
16
+ */
17
+ export async function createInteraction(params: CreateInteractionParams) {
18
+ const { contactId, type, title, content, userId, date, metadata } = params;
19
+
20
+ return await prisma.interaction.create({
21
+ data: {
22
+ contactId,
23
+ type,
24
+ title: title ?? null,
25
+ content,
26
+ userId,
27
+ date: date ?? null,
28
+ metadata: metadata ?? undefined,
29
+ },
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Crée une interaction pour un changement de statut
35
+ */
36
+ export async function logStatusChange(
37
+ contactId: string,
38
+ oldStatusId: string | null,
39
+ newStatusId: string | null,
40
+ userId: string,
41
+ oldStatusName?: string | null,
42
+ newStatusName?: string | null,
43
+ ) {
44
+ const oldStatus = oldStatusName || 'Aucun';
45
+ const newStatus = newStatusName || 'Aucun';
46
+
47
+ return await createInteraction({
48
+ contactId,
49
+ type: 'STATUS_CHANGE',
50
+ title: 'Changement de statut',
51
+ content: `Statut modifié de "${oldStatus}" à "${newStatus}"`,
52
+ userId,
53
+ metadata: {
54
+ oldStatusId,
55
+ newStatusId,
56
+ oldStatusName,
57
+ newStatusName,
58
+ },
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Crée une interaction pour une mise à jour de contact
64
+ */
65
+ export async function logContactUpdate(
66
+ contactId: string,
67
+ changes: Record<string, { old: any; new: any }>,
68
+ userId: string,
69
+ ) {
70
+ const changeDescriptions: string[] = [];
71
+
72
+ for (const [field, { old, new: newValue }] of Object.entries(changes)) {
73
+ const fieldNames: Record<string, string> = {
74
+ firstName: 'Prénom',
75
+ lastName: 'Nom',
76
+ phone: 'Téléphone',
77
+ secondaryPhone: 'Téléphone secondaire',
78
+ email: 'Email',
79
+ address: 'Adresse',
80
+ city: 'Ville',
81
+ postalCode: 'Code postal',
82
+ civility: 'Civilité',
83
+ origin: 'Origine',
84
+ companyName: 'Entreprise',
85
+ closingReason: 'Motif de fermeture',
86
+ };
87
+
88
+ const fieldName = fieldNames[field] || field;
89
+ // Normaliser les valeurs : null, undefined et chaînes vides sont considérés comme équivalents
90
+ const normalize = (value: any) => {
91
+ if (value === null || value === undefined) return null;
92
+ if (typeof value === 'string' && value.trim() === '') return null;
93
+ return value;
94
+ };
95
+
96
+ const oldNorm = normalize(old);
97
+ const newNorm = normalize(newValue);
98
+
99
+ // Si après normalisation il n'y a pas de vraie différence, on ignore ce champ
100
+ if (oldNorm === newNorm) {
101
+ continue;
102
+ }
103
+
104
+ const formatValue = (value: any) => {
105
+ if (value === null || value === undefined) return 'Aucun';
106
+ return String(value);
107
+ };
108
+
109
+ const oldValueStr = formatValue(oldNorm);
110
+ const newValueStr = formatValue(newNorm);
111
+
112
+ if (oldValueStr !== newValueStr) {
113
+ changeDescriptions.push(`${fieldName}: "${oldValueStr}" → "${newValueStr}"`);
114
+ }
115
+ }
116
+
117
+ if (changeDescriptions.length === 0) {
118
+ return null;
119
+ }
120
+
121
+ return await createInteraction({
122
+ contactId,
123
+ type: 'CONTACT_UPDATE',
124
+ title: 'Modification de la fiche contact',
125
+ content: changeDescriptions.join('\n'),
126
+ userId,
127
+ metadata: { changes },
128
+ });
129
+ }
130
+
131
+ /**
132
+ * Crée une interaction pour un changement d'assignation
133
+ */
134
+ export async function logAssignmentChange(
135
+ contactId: string,
136
+ type: 'COMMERCIAL' | 'TELEPRO',
137
+ oldUserId: string | null,
138
+ newUserId: string | null,
139
+ userId: string,
140
+ oldUserName?: string | null,
141
+ newUserName?: string | null,
142
+ ) {
143
+ // Normaliser les valeurs pour la comparaison (null, undefined, '' sont considérés comme équivalents)
144
+ const normalizedOldUserId = oldUserId || null;
145
+ const normalizedNewUserId = newUserId || null;
146
+
147
+ // Ne créer l'interaction que si les valeurs ont réellement changé
148
+ if (normalizedOldUserId === normalizedNewUserId) {
149
+ return null;
150
+ }
151
+
152
+ const roleName = type === 'COMMERCIAL' ? 'Commercial' : 'Télépro';
153
+ const oldName = oldUserName || 'Non attribué';
154
+ const newName = newUserName || 'Non attribué';
155
+
156
+ return await createInteraction({
157
+ contactId,
158
+ type: 'ASSIGNMENT_CHANGE',
159
+ title: `Changement d'assignation ${roleName}`,
160
+ content: `${roleName} modifié de "${oldName}" à "${newName}"`,
161
+ userId,
162
+ metadata: {
163
+ assignmentType: type,
164
+ oldUserId,
165
+ newUserId,
166
+ oldUserName,
167
+ newUserName,
168
+ },
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Crée une interaction pour la création d'un rendez-vous
174
+ */
175
+ export async function logAppointmentCreated(
176
+ contactId: string,
177
+ taskId: string,
178
+ scheduledAt: Date,
179
+ title: string | null,
180
+ userId: string,
181
+ ) {
182
+ const formattedDate = scheduledAt.toLocaleDateString('fr-FR', {
183
+ day: 'numeric',
184
+ month: 'long',
185
+ year: 'numeric',
186
+ hour: '2-digit',
187
+ minute: '2-digit',
188
+ });
189
+
190
+ return await createInteraction({
191
+ contactId,
192
+ type: 'APPOINTMENT_CREATED',
193
+ title: title ?? null, // Enregistrer seulement le titre
194
+ content: `Rendez-vous programmé le ${formattedDate}`,
195
+ userId,
196
+ date: scheduledAt,
197
+ metadata: {
198
+ taskId,
199
+ scheduledAt: scheduledAt.toISOString(),
200
+ },
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Crée une interaction pour l'annulation d'un rendez-vous
206
+ */
207
+ export async function logAppointmentCancelled(
208
+ contactId: string,
209
+ taskId: string,
210
+ scheduledAt: Date,
211
+ title: string | null,
212
+ userId: string,
213
+ isGoogleMeet: boolean = false,
214
+ ) {
215
+ const formattedDate = scheduledAt.toLocaleDateString('fr-FR', {
216
+ day: 'numeric',
217
+ month: 'long',
218
+ year: 'numeric',
219
+ hour: '2-digit',
220
+ minute: '2-digit',
221
+ });
222
+
223
+ const appointmentType = isGoogleMeet ? 'Google Meet' : 'Rendez-vous';
224
+
225
+ return await createInteraction({
226
+ contactId,
227
+ type: 'APPOINTMENT_DELETED',
228
+ title: title ?? null, // Enregistrer seulement le titre
229
+ content: `${appointmentType} prévu le ${formattedDate} a été annulé.`,
230
+ userId,
231
+ date: scheduledAt,
232
+ metadata: {
233
+ taskId,
234
+ scheduledAt: scheduledAt.toISOString(),
235
+ cancelled: true,
236
+ isGoogleMeet,
237
+ },
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Crée une interaction pour la modification d'un rendez-vous
243
+ */
244
+ export async function logAppointmentChanged(
245
+ contactId: string,
246
+ taskId: string,
247
+ scheduledAt: Date,
248
+ title: string | null,
249
+ userId: string,
250
+ isGoogleMeet: boolean = false,
251
+ ) {
252
+ const formattedDate = scheduledAt.toLocaleDateString('fr-FR', {
253
+ day: 'numeric',
254
+ month: 'long',
255
+ year: 'numeric',
256
+ hour: '2-digit',
257
+ minute: '2-digit',
258
+ });
259
+
260
+ const appointmentType = isGoogleMeet ? 'Google Meet' : 'Rendez-vous';
261
+
262
+ return await createInteraction({
263
+ contactId,
264
+ type: 'APPOINTMENT_CHANGED',
265
+ title: title ?? null, // Enregistrer seulement le titre
266
+ content: `${appointmentType} programmé le ${formattedDate} a été modifié.`,
267
+ userId,
268
+ date: scheduledAt,
269
+ metadata: {
270
+ taskId,
271
+ scheduledAt: scheduledAt.toISOString(),
272
+ isGoogleMeet,
273
+ },
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Crée une interaction pour l'upload d'un fichier
279
+ */
280
+ export async function logFileUploaded(
281
+ contactId: string,
282
+ fileId: string,
283
+ fileName: string,
284
+ fileSize: number,
285
+ userId: string,
286
+ ) {
287
+ const formatFileSize = (bytes: number): string => {
288
+ if (bytes === 0) return '0 Bytes';
289
+ const k = 1024;
290
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
291
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
292
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
293
+ };
294
+
295
+ return await createInteraction({
296
+ contactId,
297
+ type: 'FILE_UPLOADED',
298
+ title: null,
299
+ content: `Fichier "${fileName}" (${formatFileSize(fileSize)}) ajouté`,
300
+ userId,
301
+ metadata: {
302
+ fileId,
303
+ fileName,
304
+ fileSize,
305
+ },
306
+ });
307
+ }
308
+
309
+ /**
310
+ * Crée une interaction pour le remplacement d'un fichier (doublon)
311
+ */
312
+ export async function logFileReplaced(
313
+ contactId: string,
314
+ fileId: string,
315
+ fileName: string,
316
+ fileSize: number,
317
+ userId: string,
318
+ ) {
319
+ const formatFileSize = (bytes: number): string => {
320
+ if (bytes === 0) return '0 Bytes';
321
+ const k = 1024;
322
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
323
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
324
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
325
+ };
326
+
327
+ return await createInteraction({
328
+ contactId,
329
+ type: 'FILE_REPLACED',
330
+ title: null,
331
+ content: `Fichier "${fileName}" (${formatFileSize(fileSize)}) remplacé`,
332
+ userId,
333
+ metadata: {
334
+ fileId,
335
+ fileName,
336
+ fileSize,
337
+ replaced: true,
338
+ },
339
+ });
340
+ }
341
+
342
+ /**
343
+ * Crée une interaction pour la suppression d'un fichier
344
+ */
345
+ export async function logFileDeleted(
346
+ contactId: string,
347
+ fileName: string,
348
+ fileSize: number,
349
+ userId: string,
350
+ ) {
351
+ const formatFileSize = (bytes: number): string => {
352
+ if (bytes === 0) return '0 Bytes';
353
+ const k = 1024;
354
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
355
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
356
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
357
+ };
358
+
359
+ return await createInteraction({
360
+ contactId,
361
+ type: 'FILE_DELETED',
362
+ title: null,
363
+ content: `Fichier "${fileName}" (${formatFileSize(fileSize)}) supprimé`,
364
+ userId,
365
+ metadata: {
366
+ fileName,
367
+ fileSize,
368
+ deleted: true,
369
+ },
370
+ });
371
+ }
@@ -0,0 +1,99 @@
1
+ import crypto from 'crypto';
2
+
3
+ const ALGORITHM = 'aes-256-gcm';
4
+ const IV_LENGTH = 16; // 128 bits
5
+
6
+ // Récupérer la clé depuis les variables d'environnement
7
+ function getEncryptionKey(): Buffer | null {
8
+ const key = process.env.ENCRYPTION_KEY;
9
+ if (!key) {
10
+ console.warn("⚠️ ENCRYPTION_KEY n'est pas définie. Le chiffrement ne sera pas utilisé.");
11
+ return null;
12
+ }
13
+
14
+ // Si la clé est en hexadécimal, la convertir
15
+ if (key.length === 64) {
16
+ return Buffer.from(key, 'hex');
17
+ }
18
+
19
+ // Sinon, utiliser directement (mais ce n'est pas recommandé)
20
+ // On génère un hash pour avoir exactement 32 bytes
21
+ return crypto.createHash('sha256').update(key).digest();
22
+ }
23
+
24
+ /**
25
+ * Chiffre un texte avec AES-256-GCM
26
+ * @param text - Le texte à chiffrer
27
+ * @returns Le texte chiffré au format: iv:tag:encrypted (ou texte en clair si ENCRYPTION_KEY n'est pas définie)
28
+ */
29
+ export function encrypt(text: string): string {
30
+ if (!text) {
31
+ return text;
32
+ }
33
+
34
+ const key = getEncryptionKey();
35
+
36
+ // Si la clé n'est pas définie, retourner le texte en clair (avec un avertissement)
37
+ if (!key) {
38
+ console.warn(
39
+ '⚠️ Chiffrement désactivé : ENCRYPTION_KEY non définie. Le mot de passe sera stocké en clair.',
40
+ );
41
+ return text;
42
+ }
43
+
44
+ const iv = crypto.randomBytes(IV_LENGTH);
45
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
46
+
47
+ let encrypted = cipher.update(text, 'utf8', 'hex');
48
+ encrypted += cipher.final('hex');
49
+
50
+ const tag = cipher.getAuthTag();
51
+
52
+ // Retourner: iv:tag:encrypted
53
+ return iv.toString('hex') + ':' + tag.toString('hex') + ':' + encrypted;
54
+ }
55
+
56
+ /**
57
+ * Déchiffre un texte chiffré avec AES-256-GCM
58
+ * @param encryptedData - Le texte chiffré au format: iv:tag:encrypted
59
+ * @returns Le texte déchiffré
60
+ */
61
+ export function decrypt(encryptedData: string): string {
62
+ if (!encryptedData) {
63
+ return encryptedData;
64
+ }
65
+
66
+ // Si le format ne correspond pas (ancien format non chiffré), retourner tel quel
67
+ const parts = encryptedData.split(':');
68
+ if (parts.length !== 3) {
69
+ // Probablement un ancien mot de passe non chiffré, retourner tel quel
70
+ // (pour la migration des données existantes)
71
+ return encryptedData;
72
+ }
73
+
74
+ try {
75
+ const key = getEncryptionKey();
76
+
77
+ // Si la clé n'est pas définie et que le texte est chiffré, on ne peut pas le déchiffrer
78
+ if (!key) {
79
+ console.warn('⚠️ Impossible de déchiffrer : ENCRYPTION_KEY non définie.');
80
+ return encryptedData;
81
+ }
82
+
83
+ const iv = Buffer.from(parts[0], 'hex');
84
+ const tag = Buffer.from(parts[1], 'hex');
85
+ const encrypted = parts[2];
86
+
87
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
88
+ decipher.setAuthTag(tag);
89
+
90
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
91
+ decrypted += decipher.final('utf8');
92
+
93
+ return decrypted;
94
+ } catch (error) {
95
+ // Si le déchiffrement échoue, retourner tel quel (pour la compatibilité)
96
+ console.error('Erreur lors du déchiffrement:', error);
97
+ return encryptedData;
98
+ }
99
+ }