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,82 @@
1
+ 'use client';
2
+
3
+ import { useSession } from '@/lib/auth-client';
4
+ import { useEffect, useState } from 'react';
5
+ import { useViewAs } from '@/contexts/view-as-context';
6
+
7
+ /**
8
+ * Hook personnalisé pour récupérer les permissions de l'utilisateur via son profil
9
+ * Les droits sont déterminés par le profil assigné, pas par un rôle
10
+ * Supporte le mode "vue en tant que" pour les admins
11
+ */
12
+ export function useUserRole() {
13
+ const { data: session, isPending } = useSession();
14
+ const { viewAsUser, isViewingAsOther } = useViewAs();
15
+ const [permissions, setPermissions] = useState<string[]>([]);
16
+ const [realUserPermissions, setRealUserPermissions] = useState<string[]>([]); // Permissions de l'utilisateur réellement connecté
17
+ const [loading, setLoading] = useState(true);
18
+
19
+ useEffect(() => {
20
+ if (isPending) return;
21
+
22
+ if (!session?.user) {
23
+ setPermissions([]);
24
+ setRealUserPermissions([]);
25
+ setLoading(false);
26
+ return;
27
+ }
28
+
29
+ // Récupérer les permissions de l'utilisateur depuis l'API
30
+ const fetchUserPermissions = async () => {
31
+ try {
32
+ const response = await fetch('/api/users/me');
33
+ if (response.ok) {
34
+ const userData = await response.json();
35
+ const userPerms = (userData.customRole?.permissions as string[]) || [];
36
+
37
+ // Toujours stocker les permissions réelles de l'utilisateur connecté
38
+ setRealUserPermissions(userPerms);
39
+
40
+ // Si on est en mode "vue en tant que", utiliser les permissions de l'utilisateur visualisé
41
+ if (isViewingAsOther && viewAsUser?.permissions) {
42
+ setPermissions(viewAsUser.permissions);
43
+ } else {
44
+ setPermissions(userPerms);
45
+ }
46
+ } else {
47
+ setPermissions([]);
48
+ setRealUserPermissions([]);
49
+ }
50
+ } catch (error) {
51
+ console.error('Erreur lors de la récupération des permissions:', error);
52
+ setPermissions([]);
53
+ setRealUserPermissions([]);
54
+ } finally {
55
+ setLoading(false);
56
+ }
57
+ };
58
+
59
+ fetchUserPermissions();
60
+ }, [session, isPending, isViewingAsOther, viewAsUser]);
61
+
62
+ // Helper pour vérifier une permission
63
+ const hasPermission = (permission: string) => permissions.includes(permission);
64
+ const hasRealPermission = (permission: string) => realUserPermissions.includes(permission);
65
+
66
+ return {
67
+ permissions,
68
+ realUserPermissions,
69
+ hasPermission,
70
+ hasRealPermission,
71
+ // Helpers de compatibilité basés sur les permissions
72
+ isAdmin: hasPermission('users.manage_roles'), // Admin = peut gérer les rôles
73
+ isRealAdmin: hasRealPermission('users.manage_roles'), // True seulement si l'utilisateur connecté peut gérer les rôles
74
+ isManager: hasPermission('users.view'), // Manager = peut voir les utilisateurs
75
+ isCommercial: hasPermission('contacts.view_own'), // Commercial = peut voir ses contacts
76
+ isTelepro: hasPermission('contacts.view_own'), // Télépro = peut voir ses contacts
77
+ isLoading: loading || isPending,
78
+ currentUserId: isViewingAsOther ? viewAsUser?.id : session?.user?.id,
79
+ // Rôle par défaut pour compatibilité
80
+ role: 'USER',
81
+ };
82
+ }
@@ -0,0 +1,45 @@
1
+ import { prisma } from '@/lib/prisma';
2
+
3
+ export type AuditEntityType = 'USER' | 'ROLE';
4
+
5
+ export type AuditAction =
6
+ | 'USER_CREATED'
7
+ | 'USER_UPDATED'
8
+ | 'ROLE_CREATED'
9
+ | 'ROLE_UPDATED'
10
+ | 'ROLE_DELETED';
11
+
12
+ interface LogAuditParams {
13
+ actorId?: string | null;
14
+ targetUserId?: string | null;
15
+ action: AuditAction;
16
+ entityType: AuditEntityType;
17
+ entityId?: string | null;
18
+ metadata?: Record<string, any>;
19
+ }
20
+
21
+ export async function logAudit({
22
+ actorId,
23
+ targetUserId,
24
+ action,
25
+ entityType,
26
+ entityId,
27
+ metadata,
28
+ }: LogAuditParams) {
29
+ try {
30
+ await prisma.auditLog.create({
31
+ data: {
32
+ actorId: actorId || null,
33
+ targetUserId: targetUserId || null,
34
+ action,
35
+ entityType,
36
+ entityId: entityId || null,
37
+ metadata: metadata || undefined,
38
+ },
39
+ });
40
+ } catch (error) {
41
+ console.error('Erreur lors de la création du log d’audit:', error);
42
+ }
43
+ }
44
+
45
+
@@ -0,0 +1,16 @@
1
+ import { createAuthClient } from 'better-auth/react';
2
+
3
+ // Utiliser NEXT_PUBLIC_APP_URL si défini, sinon utiliser l'origine du navigateur
4
+ // Cela permet de fonctionner automatiquement en production sans configuration supplémentaire
5
+ const getBaseURL = () => {
6
+ if (typeof window !== 'undefined') {
7
+ return process.env.NEXT_PUBLIC_APP_URL || window.location.origin;
8
+ }
9
+ return process.env.NEXT_PUBLIC_APP_URL || process.env.BETTER_AUTH_URL || 'http://localhost:3000';
10
+ };
11
+
12
+ export const authClient = createAuthClient({
13
+ baseURL: getBaseURL(),
14
+ });
15
+
16
+ export const { signIn, signUp, signOut, useSession } = authClient;
@@ -0,0 +1,35 @@
1
+ import { betterAuth } from 'better-auth';
2
+ import { prismaAdapter } from 'better-auth/adapters/prisma';
3
+ import { prisma } from './prisma';
4
+ import { hash, compare } from 'bcryptjs';
5
+
6
+ export const hashPassword = (password: string) => hash(password, 10);
7
+
8
+ export const auth = betterAuth({
9
+ baseURL:
10
+ process.env.BETTER_AUTH_URL || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
11
+ basePath: '/api/auth',
12
+ database: prismaAdapter(prisma, {
13
+ provider: 'postgresql',
14
+ }),
15
+ emailAndPassword: {
16
+ enabled: true,
17
+ minPasswordLength: 6,
18
+ password: {
19
+ hash(password) {
20
+ return hashPassword(password);
21
+ },
22
+ verify(data) {
23
+ return compare(data.password, data.hash);
24
+ },
25
+ },
26
+ },
27
+ user: {
28
+ additionalFields: {
29
+ role: {
30
+ type: 'string',
31
+ required: false,
32
+ },
33
+ },
34
+ },
35
+ });
@@ -0,0 +1,193 @@
1
+ import { auth } from '@/lib/auth';
2
+ import { headers } from 'next/headers';
3
+ import { prisma } from '@/lib/prisma';
4
+
5
+ /**
6
+ * Vérifie si l'utilisateur actuel a une permission spécifique
7
+ * @param requiredPermission - Code de la permission à vérifier
8
+ * @returns true si l'utilisateur a la permission, false sinon
9
+ */
10
+ export async function checkPermission(requiredPermission: string): Promise<boolean> {
11
+ try {
12
+ const session = await auth.api.getSession({
13
+ headers: await headers(),
14
+ });
15
+
16
+ if (!session) {
17
+ return false;
18
+ }
19
+
20
+ const userId = session.user.id;
21
+
22
+ // Récupérer l'utilisateur avec son profil personnalisé si applicable
23
+ const user = await prisma.user.findUnique({
24
+ where: { id: userId },
25
+ include: {
26
+ customRole: true,
27
+ },
28
+ });
29
+
30
+ if (!user) {
31
+ return false;
32
+ }
33
+
34
+ // Les permissions viennent uniquement du profil assigné
35
+ if (!user.customRole) {
36
+ // Aucun profil assigné = aucune permission
37
+ return false;
38
+ }
39
+
40
+ const userPermissions = user.customRole.permissions as string[];
41
+
42
+ // Vérifier si la permission est dans la liste
43
+ return userPermissions.includes(requiredPermission);
44
+ } catch (error) {
45
+ console.error('Erreur lors de la vérification des permissions:', error);
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Vérifie si l'utilisateur actuel a plusieurs permissions
52
+ * @param requiredPermissions - Tableau des codes de permissions à vérifier
53
+ * @param requireAll - Si true, toutes les permissions sont requises. Si false, au moins une est requise
54
+ * @returns true si l'utilisateur a les permissions, false sinon
55
+ */
56
+ export async function checkPermissions(
57
+ requiredPermissions: string[],
58
+ requireAll: boolean = true,
59
+ ): Promise<boolean> {
60
+ try {
61
+ const session = await auth.api.getSession({
62
+ headers: await headers(),
63
+ });
64
+
65
+ if (!session) {
66
+ return false;
67
+ }
68
+
69
+ const userId = session.user.id;
70
+
71
+ // Récupérer l'utilisateur avec son profil personnalisé si applicable
72
+ const user = await prisma.user.findUnique({
73
+ where: { id: userId },
74
+ include: {
75
+ customRole: true,
76
+ },
77
+ });
78
+
79
+ if (!user) {
80
+ return false;
81
+ }
82
+
83
+ // Les permissions viennent uniquement du profil assigné
84
+ if (!user.customRole) {
85
+ // Aucun profil assigné = aucune permission
86
+ return false;
87
+ }
88
+
89
+ const userPermissions = user.customRole.permissions as string[];
90
+
91
+ // Vérifier les permissions
92
+ if (requireAll) {
93
+ return requiredPermissions.every((perm) => userPermissions.includes(perm));
94
+ } else {
95
+ return requiredPermissions.some((perm) => userPermissions.includes(perm));
96
+ }
97
+ } catch (error) {
98
+ console.error('Erreur lors de la vérification des permissions:', error);
99
+ return false;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Récupère toutes les permissions de l'utilisateur actuel
105
+ * @returns Tableau des codes de permissions de l'utilisateur
106
+ */
107
+ export async function getUserPermissions(): Promise<string[]> {
108
+ try {
109
+ const session = await auth.api.getSession({
110
+ headers: await headers(),
111
+ });
112
+
113
+ if (!session) {
114
+ return [];
115
+ }
116
+
117
+ const userId = session.user.id;
118
+
119
+ // Récupérer l'utilisateur avec son profil personnalisé si applicable
120
+ const user = await prisma.user.findUnique({
121
+ where: { id: userId },
122
+ include: {
123
+ customRole: true,
124
+ },
125
+ });
126
+
127
+ if (!user) {
128
+ return [];
129
+ }
130
+
131
+ // Les permissions viennent uniquement du profil assigné
132
+ if (!user.customRole) {
133
+ // Aucun profil assigné = aucune permission
134
+ return [];
135
+ }
136
+
137
+ return user.customRole.permissions as string[];
138
+ } catch (error) {
139
+ console.error('Erreur lors de la récupération des permissions:', error);
140
+ return [];
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Middleware pour protéger une route API avec des permissions
146
+ * Exemple d'utilisation :
147
+ *
148
+ * export async function GET(req: NextRequest) {
149
+ * const hasPermission = await requirePermission('contacts.view_all');
150
+ * if (!hasPermission) {
151
+ * return NextResponse.json({ error: 'Non autorisé' }, { status: 403 });
152
+ * }
153
+ * // ... reste du code
154
+ * }
155
+ */
156
+ export async function requirePermission(requiredPermission: string): Promise<boolean> {
157
+ return checkPermission(requiredPermission);
158
+ }
159
+
160
+ /**
161
+ * Helper pour vérifier si un utilisateur est admin
162
+ * Un admin est un utilisateur avec un profil ayant toutes les permissions
163
+ */
164
+ export async function isAdmin(): Promise<boolean> {
165
+ try {
166
+ const session = await auth.api.getSession({
167
+ headers: await headers(),
168
+ });
169
+
170
+ if (!session) {
171
+ return false;
172
+ }
173
+
174
+ const userId = session.user.id;
175
+
176
+ const user = await prisma.user.findUnique({
177
+ where: { id: userId },
178
+ include: {
179
+ customRole: true,
180
+ },
181
+ });
182
+
183
+ if (!user || !user.customRole) {
184
+ return false;
185
+ }
186
+
187
+ // Vérifier si le profil a la permission de gestion des utilisateurs
188
+ const permissions = user.customRole.permissions as string[];
189
+ return permissions.includes('users.manage_roles');
190
+ } catch (error) {
191
+ return false;
192
+ }
193
+ }
@@ -0,0 +1,112 @@
1
+ import { prisma } from '@/lib/prisma';
2
+
3
+ /**
4
+ * Détecte et gère les doublons de contacts basés sur nom, prénom ET email
5
+ * Si un doublon est trouvé :
6
+ * - Change le statut en "Doublon"
7
+ * - Met à jour updatedAt pour remonter le contact en haut
8
+ * - Ajoute une note indiquant que le contact a été enregistré une énième fois
9
+ *
10
+ * @param firstName - Prénom du contact
11
+ * @param lastName - Nom du contact
12
+ * @param email - Email du contact
13
+ * @param origin - Origine du contact (pour la note)
14
+ * @param userId - ID de l'utilisateur qui crée le contact
15
+ * @returns L'ID du contact existant (doublon) ou null si aucun doublon
16
+ */
17
+ export async function handleContactDuplicate(
18
+ firstName: string | null | undefined,
19
+ lastName: string | null | undefined,
20
+ email: string | null | undefined,
21
+ origin: string | null | undefined,
22
+ userId: string,
23
+ ): Promise<string | null> {
24
+ // Normaliser les valeurs pour la comparaison
25
+ const normalizedFirstName = firstName?.trim().toLowerCase() || null;
26
+ const normalizedLastName = lastName?.trim().toLowerCase() || null;
27
+ const normalizedEmail = email?.trim().toLowerCase() || null;
28
+
29
+ // Si on n'a pas au moins nom, prénom ET email, on ne peut pas détecter de doublon
30
+ if (!normalizedFirstName || !normalizedLastName || !normalizedEmail) {
31
+ return null;
32
+ }
33
+
34
+ // Chercher un contact existant avec le même nom, prénom ET email
35
+ const existingContact = await prisma.contact.findFirst({
36
+ where: {
37
+ AND: [
38
+ { firstName: { equals: normalizedFirstName, mode: 'insensitive' } },
39
+ { lastName: { equals: normalizedLastName, mode: 'insensitive' } },
40
+ { email: { equals: normalizedEmail, mode: 'insensitive' } },
41
+ ],
42
+ },
43
+ });
44
+
45
+ if (!existingContact) {
46
+ return null;
47
+ }
48
+
49
+ // Récupérer ou créer le statut "Doublon"
50
+ let duplicateStatus = await prisma.status.findUnique({
51
+ where: { name: 'Doublon' },
52
+ });
53
+
54
+ if (!duplicateStatus) {
55
+ // Créer le statut Doublon s'il n'existe pas
56
+ const lastStatus = await prisma.status.findFirst({
57
+ orderBy: { order: 'desc' },
58
+ });
59
+ const newOrder = lastStatus ? lastStatus.order + 1 : 100;
60
+
61
+ duplicateStatus = await prisma.status.create({
62
+ data: {
63
+ name: 'Doublon',
64
+ color: '#EF4444', // Rouge pour indiquer un problème
65
+ order: newOrder,
66
+ },
67
+ });
68
+ }
69
+
70
+ // Compter combien de fois ce contact a été enregistré (en comptant les notes "Contact enregistré à nouveau")
71
+ const duplicateCount = await prisma.interaction.count({
72
+ where: {
73
+ contactId: existingContact.id,
74
+ type: 'NOTE',
75
+ title: 'Contact enregistré à nouveau',
76
+ },
77
+ });
78
+
79
+ const occurrenceNumber = duplicateCount + 2; // +2 car c'est la 2ème fois minimum (1ère création + cette fois)
80
+
81
+ // Mettre à jour le contact : changer le statut en Doublon et mettre à jour updatedAt
82
+ await prisma.contact.update({
83
+ where: { id: existingContact.id },
84
+ data: {
85
+ statusId: duplicateStatus.id,
86
+ updatedAt: new Date(), // Pour remonter le contact en haut du tableau
87
+ },
88
+ });
89
+
90
+ // Ajouter une note indiquant que le contact a été enregistré une énième fois
91
+ await prisma.interaction.create({
92
+ data: {
93
+ contactId: existingContact.id,
94
+ type: 'NOTE',
95
+ title: 'Contact enregistré à nouveau',
96
+ content: `Ce contact a été enregistré une ${occurrenceNumber}${occurrenceNumber === 1 ? 'ère' : 'ème'} fois${origin ? ` depuis ${origin}` : ''} le ${new Date().toLocaleDateString(
97
+ 'fr-FR',
98
+ {
99
+ day: 'numeric',
100
+ month: 'long',
101
+ year: 'numeric',
102
+ hour: '2-digit',
103
+ minute: '2-digit',
104
+ },
105
+ )}.`,
106
+ userId: userId,
107
+ date: new Date(),
108
+ },
109
+ });
110
+
111
+ return existingContact.id;
112
+ }