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,434 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import Link from 'next/link';
5
+ import { ArrowLeft, Shield, Users as UsersIcon, Key, Plus, Edit, Trash2, X } from 'lucide-react';
6
+ import { PERMISSIONS, PERMISSIONS_BY_CATEGORY } from '@/lib/permissions';
7
+ import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
8
+
9
+ interface Role {
10
+ id: string;
11
+ name: string;
12
+ description: string | null;
13
+ permissions: string[];
14
+ isSystem: boolean;
15
+ usersCount: number;
16
+ createdAt: string | null;
17
+ updatedAt: string | null;
18
+ }
19
+
20
+ interface RoleModalProps {
21
+ isOpen: boolean;
22
+ onClose: () => void;
23
+ onSave: () => void;
24
+ role?: Role;
25
+ }
26
+
27
+ function RoleModal({ isOpen, onClose, onSave, role }: RoleModalProps) {
28
+ const [formData, setFormData] = useState({
29
+ name: role?.name || '',
30
+ description: role?.description || '',
31
+ permissions: role?.permissions || [],
32
+ });
33
+ const [isSubmitting, setIsSubmitting] = useState(false);
34
+ const [error, setError] = useState('');
35
+
36
+ // Réinitialiser le formulaire quand le rôle change
37
+ useEffect(() => {
38
+ if (role) {
39
+ setFormData({
40
+ name: role.name,
41
+ description: role.description || '',
42
+ permissions: role.permissions,
43
+ });
44
+ } else {
45
+ setFormData({
46
+ name: '',
47
+ description: '',
48
+ permissions: [],
49
+ });
50
+ }
51
+ setError('');
52
+ }, [role, isOpen]);
53
+
54
+ const togglePermission = (permissionCode: string) => {
55
+ setFormData((prev) => ({
56
+ ...prev,
57
+ permissions: prev.permissions.includes(permissionCode)
58
+ ? prev.permissions.filter((p) => p !== permissionCode)
59
+ : [...prev.permissions, permissionCode],
60
+ }));
61
+ };
62
+
63
+ const handleSubmit = async (e: React.FormEvent) => {
64
+ e.preventDefault();
65
+ setError('');
66
+ setIsSubmitting(true);
67
+
68
+ try {
69
+ const url = role ? `/api/roles/${role.id}` : '/api/roles';
70
+ const method = role ? 'PUT' : 'POST';
71
+
72
+ const response = await fetch(url, {
73
+ method,
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify(formData),
76
+ });
77
+
78
+ const data = await response.json();
79
+
80
+ if (!response.ok) {
81
+ throw new Error(data.error || "Erreur lors de l'enregistrement");
82
+ }
83
+
84
+ onSave();
85
+ onClose();
86
+ } catch (err: any) {
87
+ setError(err.message);
88
+ } finally {
89
+ setIsSubmitting(false);
90
+ }
91
+ };
92
+
93
+ if (!isOpen) return null;
94
+
95
+ return (
96
+ <div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-gray-500/20 p-4 backdrop-blur-sm">
97
+ <div className="my-8 w-full max-w-2xl rounded-lg bg-white shadow-xl">
98
+ <form onSubmit={handleSubmit}>
99
+ {/* Header */}
100
+ <div className="flex items-center justify-between border-b border-gray-200 p-6">
101
+ <h2 className="text-xl font-semibold text-gray-900">
102
+ {role ? 'Modifier le profil' : 'Nouveau profil'}
103
+ </h2>
104
+ <button
105
+ type="button"
106
+ onClick={onClose}
107
+ disabled={isSubmitting}
108
+ className="cursor-pointer rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-50"
109
+ >
110
+ <X className="h-5 w-5" />
111
+ </button>
112
+ </div>
113
+
114
+ {/* Content */}
115
+ <div className="max-h-[70vh] overflow-y-auto p-6">
116
+ {error && (
117
+ <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>
118
+ )}
119
+ <div className="space-y-6">
120
+ {/* Nom et description */}
121
+ <div className="grid gap-4 sm:grid-cols-2">
122
+ <div>
123
+ <label htmlFor="name" className="block text-sm font-medium text-gray-700">
124
+ Nom du profil *
125
+ </label>
126
+ <input
127
+ type="text"
128
+ id="name"
129
+ value={formData.name}
130
+ onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
131
+ className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
132
+ required
133
+ />
134
+ </div>
135
+ <div className="sm:col-span-2">
136
+ <label htmlFor="description" className="block text-sm font-medium text-gray-700">
137
+ Description
138
+ </label>
139
+ <textarea
140
+ id="description"
141
+ value={formData.description}
142
+ onChange={(e) =>
143
+ setFormData((prev) => ({
144
+ ...prev,
145
+ description: e.target.value,
146
+ }))
147
+ }
148
+ rows={2}
149
+ className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
150
+ />
151
+ </div>
152
+ </div>
153
+
154
+ {/* Permissions */}
155
+ <div>
156
+ <h3 className="mb-4 text-base font-semibold text-gray-900">Permissions</h3>
157
+
158
+ <div className="space-y-6">
159
+ {Object.entries(PERMISSIONS_BY_CATEGORY).map(([category, permissions]) => (
160
+ <div key={category}>
161
+ <h4 className="mb-3 text-sm font-medium tracking-wide text-gray-500 uppercase">
162
+ {category}
163
+ </h4>
164
+ <div className="space-y-2">
165
+ {permissions.map((permission) => (
166
+ <label
167
+ key={permission.code}
168
+ className="flex cursor-pointer items-start gap-3 rounded-lg border border-gray-200 p-3 transition-colors hover:bg-gray-50"
169
+ >
170
+ <input
171
+ type="checkbox"
172
+ checked={formData.permissions.includes(permission.code)}
173
+ onChange={() => togglePermission(permission.code)}
174
+ className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
175
+ />
176
+ <div className="flex-1">
177
+ <div className="font-medium text-gray-900">{permission.name}</div>
178
+ <div className="text-sm text-gray-500">{permission.description}</div>
179
+ </div>
180
+ </label>
181
+ ))}
182
+ </div>
183
+ </div>
184
+ ))}
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ {/* Footer */}
191
+ <div className="flex items-center justify-end gap-3 border-t border-gray-200 p-6">
192
+ <button
193
+ type="button"
194
+ onClick={onClose}
195
+ disabled={isSubmitting}
196
+ className="cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
197
+ >
198
+ Annuler
199
+ </button>
200
+ <button
201
+ type="submit"
202
+ disabled={isSubmitting}
203
+ className="cursor-pointer rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50"
204
+ >
205
+ {isSubmitting ? 'Enregistrement...' : 'Enregistrer'}
206
+ </button>
207
+ </div>
208
+ </form>
209
+ </div>
210
+ </div>
211
+ );
212
+ }
213
+
214
+ export default function RolesPage() {
215
+ const { toggle: toggleMobileMenu, isOpen: isMobileMenuOpen } = useMobileMenuContext();
216
+ const [showModal, setShowModal] = useState(false);
217
+ const [selectedRole, setSelectedRole] = useState<Role | null>(null);
218
+ const [roles, setRoles] = useState<Role[]>([]);
219
+ const [loading, setLoading] = useState(true);
220
+ const [error, setError] = useState('');
221
+
222
+ const fetchRoles = async () => {
223
+ try {
224
+ setLoading(true);
225
+ const response = await fetch('/api/roles');
226
+ if (!response.ok) {
227
+ throw new Error('Erreur lors du chargement des profils');
228
+ }
229
+ const data = await response.json();
230
+ setRoles(data);
231
+ } catch (err: any) {
232
+ setError(err.message);
233
+ } finally {
234
+ setLoading(false);
235
+ }
236
+ };
237
+
238
+ useEffect(() => {
239
+ fetchRoles();
240
+ }, []);
241
+
242
+ const handleEditRole = (roleId: string) => {
243
+ const role = roles.find((r) => r.id === roleId);
244
+ if (role) {
245
+ setSelectedRole(role);
246
+ setShowModal(true);
247
+ }
248
+ };
249
+
250
+ const handleDeleteRole = async (roleId: string) => {
251
+ const role = roles.find((r) => r.id === roleId);
252
+ if (!role) return;
253
+
254
+ const confirmMessage = role.isSystem
255
+ ? `⚠️ Attention : "${role.name}" est un profil système.\n\nÊtes-vous sûr de vouloir le supprimer ?`
256
+ : `Êtes-vous sûr de vouloir supprimer le profil "${role.name}" ?`;
257
+
258
+ if (!confirm(confirmMessage)) {
259
+ return;
260
+ }
261
+
262
+ try {
263
+ const response = await fetch(`/api/roles/${roleId}`, {
264
+ method: 'DELETE',
265
+ });
266
+
267
+ const data = await response.json();
268
+
269
+ if (!response.ok) {
270
+ throw new Error(data.error || 'Erreur lors de la suppression');
271
+ }
272
+
273
+ await fetchRoles();
274
+ } catch (err: any) {
275
+ setError(err.message);
276
+ setTimeout(() => setError(''), 5000);
277
+ }
278
+ };
279
+
280
+ const handleCloseModal = () => {
281
+ setShowModal(false);
282
+ setSelectedRole(null);
283
+ };
284
+
285
+ const handleSaveRole = async () => {
286
+ await fetchRoles();
287
+ };
288
+
289
+ return (
290
+ <div className="h-full">
291
+ <div className="border-b border-gray-200 bg-white">
292
+ <div className="p-4 sm:p-6">
293
+ <div className="mb-4">
294
+ <Link
295
+ href="/users"
296
+ className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900"
297
+ >
298
+ <ArrowLeft className="h-4 w-4" />
299
+ Retour
300
+ </Link>
301
+ </div>
302
+ <div className="flex items-center justify-between">
303
+ <div>
304
+ <h1 className="text-2xl font-bold text-gray-900">Gestion des profils</h1>
305
+ <p className="mt-1 text-sm text-gray-500">
306
+ Créer et configurer les profils avec leurs permissions
307
+ </p>
308
+ </div>
309
+ <button
310
+ onClick={() => {
311
+ setSelectedRole(null);
312
+ setShowModal(true);
313
+ }}
314
+ className="flex cursor-pointer items-center gap-2 rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700"
315
+ >
316
+ <Plus className="h-4 w-4" />
317
+ Nouveau profil
318
+ </button>
319
+ </div>
320
+ </div>
321
+ </div>
322
+
323
+ <div className="p-4 sm:p-6">
324
+ {error && <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>}
325
+
326
+ {loading ? (
327
+ <div className="grid gap-6 lg:grid-cols-2">
328
+ {[1, 2, 3, 4].map((i) => (
329
+ <div key={i} className="h-64 animate-pulse rounded-lg bg-gray-200" />
330
+ ))}
331
+ </div>
332
+ ) : (
333
+ <div className="grid gap-6 lg:grid-cols-2">
334
+ {roles
335
+ .sort((a, b) => b.permissions.length - a.permissions.length) // Trier par nombre de permissions (DESC)
336
+ .map((role) => {
337
+ const visiblePermissions = role.permissions.slice(0, 4);
338
+ const remainingCount = role.permissions.length - 4;
339
+
340
+ return (
341
+ <div
342
+ key={role.id}
343
+ className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm"
344
+ >
345
+ <div className="flex items-start justify-between">
346
+ <div className="flex items-start gap-3">
347
+ <div className="rounded-lg bg-green-100 p-2">
348
+ <Shield className="h-5 w-5 text-green-600" />
349
+ </div>
350
+ <div>
351
+ <h3 className="font-semibold text-gray-900">
352
+ {role.name}
353
+ {role.isSystem && (
354
+ <span className="ml-2 inline-block rounded bg-indigo-100 px-2 py-0.5 text-xs text-indigo-600">
355
+ Système
356
+ </span>
357
+ )}
358
+ </h3>
359
+ <p className="mt-1 text-sm text-gray-600">{role.description}</p>
360
+ </div>
361
+ </div>
362
+ <div className="flex items-center gap-2">
363
+ <button
364
+ onClick={() => handleEditRole(role.id)}
365
+ className="cursor-pointer rounded-lg p-2 text-orange-600 hover:bg-orange-50"
366
+ title="Modifier"
367
+ >
368
+ <Edit className="h-4 w-4" />
369
+ </button>
370
+ <button
371
+ onClick={() => handleDeleteRole(role.id)}
372
+ className="cursor-pointer rounded-lg p-2 text-red-600 hover:bg-red-50"
373
+ title="Supprimer"
374
+ >
375
+ <Trash2 className="h-4 w-4" />
376
+ </button>
377
+ </div>
378
+ </div>
379
+
380
+ <div className="mt-4 flex items-center gap-4 text-sm text-gray-600">
381
+ <div className="flex items-center gap-1">
382
+ <UsersIcon className="h-4 w-4" />
383
+ <span>
384
+ {role.usersCount} utilisateur{role.usersCount > 1 ? 's' : ''}
385
+ </span>
386
+ </div>
387
+ <div className="flex items-center gap-1">
388
+ <Key className="h-4 w-4" />
389
+ <span>
390
+ {role.permissions.length} permission
391
+ {role.permissions.length > 1 ? 's' : ''}
392
+ </span>
393
+ </div>
394
+ </div>
395
+
396
+ <div className="mt-4">
397
+ <h4 className="mb-2 text-xs font-medium tracking-wide text-gray-500 uppercase">
398
+ Permissions
399
+ </h4>
400
+ <div className="flex flex-wrap gap-2">
401
+ {visiblePermissions.map((permCode) => {
402
+ const perm = PERMISSIONS.find((p) => p.code === permCode);
403
+ return (
404
+ <span
405
+ key={permCode}
406
+ className="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700"
407
+ >
408
+ {perm?.name || permCode}
409
+ </span>
410
+ );
411
+ })}
412
+ {remainingCount > 0 && (
413
+ <span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">
414
+ +{remainingCount} autres
415
+ </span>
416
+ )}
417
+ </div>
418
+ </div>
419
+ </div>
420
+ );
421
+ })}
422
+ </div>
423
+ )}
424
+ </div>
425
+
426
+ <RoleModal
427
+ isOpen={showModal}
428
+ onClose={handleCloseModal}
429
+ onSave={handleSaveRole}
430
+ role={selectedRole || undefined}
431
+ />
432
+ </div>
433
+ );
434
+ }
@@ -0,0 +1,57 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { checkPermission } from '@/lib/check-permission';
5
+
6
+ // GET /api/audit-logs - Dernières actions système (utilisateurs / rôles)
7
+ export async function GET(request: NextRequest) {
8
+ try {
9
+ const session = await auth.api.getSession({
10
+ headers: request.headers,
11
+ });
12
+
13
+ if (!session) {
14
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
15
+ }
16
+
17
+ // On réutilise la permission de vue utilisateurs pour l’instant
18
+ const hasPermission = await checkPermission('users.view');
19
+ if (!hasPermission) {
20
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
21
+ }
22
+
23
+ const { searchParams } = new URL(request.url);
24
+ const limit = parseInt(searchParams.get('limit') || '10', 10);
25
+
26
+ const logs = await prisma.auditLog.findMany({
27
+ orderBy: { createdAt: 'desc' },
28
+ take: Math.min(Math.max(limit, 1), 50),
29
+ include: {
30
+ actor: {
31
+ select: { id: true, name: true, email: true },
32
+ },
33
+ targetUser: {
34
+ select: { id: true, name: true, email: true },
35
+ },
36
+ },
37
+ });
38
+
39
+ return NextResponse.json(
40
+ logs.map((log) => ({
41
+ id: log.id,
42
+ action: log.action,
43
+ entityType: log.entityType,
44
+ entityId: log.entityId,
45
+ metadata: log.metadata,
46
+ createdAt: log.createdAt,
47
+ actor: log.actor,
48
+ targetUser: log.targetUser,
49
+ })),
50
+ );
51
+ } catch (error) {
52
+ console.error('Erreur lors de la récupération des logs d’audit:', error);
53
+ return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
54
+ }
55
+ }
56
+
57
+
@@ -0,0 +1,4 @@
1
+ import { auth } from '@/lib/auth';
2
+ import { toNextJsHandler } from 'better-auth/next-js';
3
+
4
+ export const { GET, POST } = toNextJsHandler(auth);
@@ -0,0 +1,31 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+
5
+ // GET /api/auth/check-active - Vérifie si l'utilisateur courant est actif
6
+ export async function GET(request: NextRequest) {
7
+ try {
8
+ const session = await auth.api.getSession({
9
+ headers: request.headers,
10
+ });
11
+
12
+ if (!session || !session.user?.id) {
13
+ return NextResponse.json({ active: false }, { status: 200 });
14
+ }
15
+
16
+ const user = await prisma.user.findUnique({
17
+ where: { id: session.user.id },
18
+ select: { active: true },
19
+ });
20
+
21
+ const isActive = user?.active ?? true;
22
+
23
+ return NextResponse.json({ active: isActive }, { status: 200 });
24
+ } catch (error: any) {
25
+ console.error('Erreur lors de la vérification du statut utilisateur:', error);
26
+ return NextResponse.json(
27
+ { active: false, error: error.message || 'Erreur serveur' },
28
+ { status: 200 },
29
+ );
30
+ }
31
+ }
@@ -0,0 +1,94 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { exchangeGoogleCodeForTokens } from '@/lib/google-calendar';
5
+
6
+ /**
7
+ * GET /api/auth/google/callback
8
+ * Gère le callback OAuth de Google et sauvegarde les tokens
9
+ */
10
+ export async function GET(request: NextRequest) {
11
+ try {
12
+ const session = await auth.api.getSession({
13
+ headers: request.headers,
14
+ });
15
+
16
+ if (!session) {
17
+ return NextResponse.redirect(new URL('/signin', request.url));
18
+ }
19
+
20
+ const { searchParams } = new URL(request.url);
21
+ const code = searchParams.get('code');
22
+ const error = searchParams.get('error');
23
+
24
+ if (error) {
25
+ return NextResponse.redirect(
26
+ new URL(
27
+ `/settings?error=${encodeURIComponent('Erreur lors de la connexion Google')}`,
28
+ request.url,
29
+ ),
30
+ );
31
+ }
32
+
33
+ if (!code) {
34
+ return NextResponse.redirect(
35
+ new URL(
36
+ `/settings?error=${encodeURIComponent("Code d'autorisation manquant")}`,
37
+ request.url,
38
+ ),
39
+ );
40
+ }
41
+
42
+ const redirectUri =
43
+ process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/api/auth/google/callback';
44
+ const tokens = await exchangeGoogleCodeForTokens(code, redirectUri);
45
+
46
+ // Calculer la date d'expiration du token
47
+ const tokenExpiresAt = new Date();
48
+ tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + (tokens.expires_in || 3600));
49
+
50
+ // Récupérer l'email du compte Google (optionnel, via l'API userinfo)
51
+ let googleEmail: string | null = null;
52
+ try {
53
+ const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
54
+ headers: {
55
+ Authorization: `Bearer ${tokens.access_token}`,
56
+ },
57
+ });
58
+ if (userInfoResponse.ok) {
59
+ const userInfo = await userInfoResponse.json();
60
+ googleEmail = userInfo.email || null;
61
+ }
62
+ } catch (err) {
63
+ console.error("Erreur lors de la récupération de l'email Google:", err);
64
+ }
65
+
66
+ // Sauvegarder ou mettre à jour les tokens
67
+ await prisma.userGoogleAccount.upsert({
68
+ where: { userId: session.user.id },
69
+ create: {
70
+ userId: session.user.id,
71
+ accessToken: tokens.access_token,
72
+ refreshToken: tokens.refresh_token || '',
73
+ tokenExpiresAt,
74
+ email: googleEmail,
75
+ },
76
+ update: {
77
+ accessToken: tokens.access_token,
78
+ refreshToken: tokens.refresh_token || undefined,
79
+ tokenExpiresAt,
80
+ email: googleEmail,
81
+ },
82
+ });
83
+
84
+ return NextResponse.redirect(new URL('/settings?success=google_connected', request.url));
85
+ } catch (error: any) {
86
+ console.error('Erreur lors du callback Google:', error);
87
+ return NextResponse.redirect(
88
+ new URL(
89
+ `/settings?error=${encodeURIComponent(error.message || 'Erreur lors de la connexion Google')}`,
90
+ request.url,
91
+ ),
92
+ );
93
+ }
94
+ }
@@ -0,0 +1,32 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+
5
+ /**
6
+ * POST /api/auth/google/disconnect
7
+ * Déconnecte le compte Google de l'utilisateur
8
+ */
9
+ export async function POST(request: NextRequest) {
10
+ try {
11
+ const session = await auth.api.getSession({
12
+ headers: request.headers,
13
+ });
14
+
15
+ if (!session) {
16
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
17
+ }
18
+
19
+ // Supprimer le compte Google
20
+ await prisma.userGoogleAccount.deleteMany({
21
+ where: { userId: session.user.id },
22
+ });
23
+
24
+ return NextResponse.json({ success: true });
25
+ } catch (error: any) {
26
+ console.error('Erreur lors de la déconnexion Google:', error);
27
+ return NextResponse.json(
28
+ { error: error.message || 'Erreur lors de la déconnexion' },
29
+ { status: 500 },
30
+ );
31
+ }
32
+ }
@@ -0,0 +1,34 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ /**
4
+ * GET /api/auth/google
5
+ * Redirige vers l'URL d'autorisation Google OAuth
6
+ */
7
+ export async function GET(request: NextRequest) {
8
+ const clientId = process.env.GOOGLE_CLIENT_ID;
9
+ const redirectUri =
10
+ process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/api/auth/google/callback';
11
+
12
+ if (!clientId) {
13
+ return NextResponse.json({ error: 'GOOGLE_CLIENT_ID non configuré' }, { status: 500 });
14
+ }
15
+
16
+ const scopes = [
17
+ 'https://www.googleapis.com/auth/calendar.events',
18
+ 'https://www.googleapis.com/auth/spreadsheets.readonly',
19
+ 'https://www.googleapis.com/auth/drive.file', // Accès aux fichiers créés par l'application
20
+ ];
21
+
22
+ const params = new URLSearchParams({
23
+ client_id: clientId,
24
+ redirect_uri: redirectUri,
25
+ response_type: 'code',
26
+ scope: scopes.join(' '),
27
+ access_type: 'offline',
28
+ prompt: 'consent', // Force le consentement pour obtenir le refresh_token
29
+ });
30
+
31
+ const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
32
+
33
+ return NextResponse.redirect(authUrl);
34
+ }