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.
- package/bin/create-crm-tmp.js +93 -0
- package/package.json +25 -0
- package/template/.prettierignore +33 -0
- package/template/.prettierrc.json +25 -0
- package/template/README.md +173 -0
- package/template/eslint.config.mjs +18 -0
- package/template/exemple-contacts.csv +11 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +64 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
- package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +582 -0
- package/template/prisma.config.ts +14 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
- package/template/src/app/(auth)/layout.tsx +3 -0
- package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
- package/template/src/app/(auth)/reset-password/page.tsx +146 -0
- package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
- package/template/src/app/(auth)/signin/page.tsx +166 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
- package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
- package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
- package/template/src/app/(dashboard)/layout.tsx +30 -0
- package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
- package/template/src/app/(dashboard)/templates/page.tsx +567 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
- package/template/src/app/(dashboard)/users/page.tsx +457 -0
- package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
- package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
- package/template/src/app/api/audit-logs/route.ts +57 -0
- package/template/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/src/app/api/auth/check-active/route.ts +31 -0
- package/template/src/app/api/auth/google/callback/route.ts +94 -0
- package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
- package/template/src/app/api/auth/google/route.ts +34 -0
- package/template/src/app/api/auth/google/status/route.ts +32 -0
- package/template/src/app/api/closing-reasons/route.ts +27 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
- package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
- package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
- package/template/src/app/api/contacts/[id]/route.ts +322 -0
- package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
- package/template/src/app/api/contacts/export/route.ts +270 -0
- package/template/src/app/api/contacts/import/route.ts +381 -0
- package/template/src/app/api/contacts/route.ts +283 -0
- package/template/src/app/api/dashboard/stats/route.ts +299 -0
- package/template/src/app/api/email/track/[id]/route.ts +68 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
- package/template/src/app/api/invite/complete/route.ts +88 -0
- package/template/src/app/api/invite/validate/route.ts +55 -0
- package/template/src/app/api/reminders/route.ts +95 -0
- package/template/src/app/api/reset-password/complete/route.ts +73 -0
- package/template/src/app/api/reset-password/request/route.ts +84 -0
- package/template/src/app/api/reset-password/validate/route.ts +49 -0
- package/template/src/app/api/reset-password/verify/route.ts +74 -0
- package/template/src/app/api/roles/[id]/route.ts +183 -0
- package/template/src/app/api/roles/route.ts +140 -0
- package/template/src/app/api/send/route.ts +282 -0
- package/template/src/app/api/settings/change-password/route.ts +95 -0
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
- package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
- package/template/src/app/api/settings/company/route.ts +121 -0
- package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
- package/template/src/app/api/settings/google-ads/route.ts +122 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
- package/template/src/app/api/settings/google-sheet/route.ts +254 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
- package/template/src/app/api/settings/meta-leads/route.ts +132 -0
- package/template/src/app/api/settings/profile/route.ts +42 -0
- package/template/src/app/api/settings/smtp/route.ts +130 -0
- package/template/src/app/api/settings/smtp/test/route.ts +121 -0
- package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
- package/template/src/app/api/settings/statuses/route.ts +83 -0
- package/template/src/app/api/statuses/route.ts +25 -0
- package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
- package/template/src/app/api/tasks/[id]/route.ts +728 -0
- package/template/src/app/api/tasks/meet/route.ts +240 -0
- package/template/src/app/api/tasks/route.ts +417 -0
- package/template/src/app/api/templates/[id]/route.ts +140 -0
- package/template/src/app/api/templates/route.ts +91 -0
- package/template/src/app/api/users/[id]/route.ts +168 -0
- package/template/src/app/api/users/list/route.ts +45 -0
- package/template/src/app/api/users/me/route.ts +48 -0
- package/template/src/app/api/users/route.ts +250 -0
- package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
- package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
- package/template/src/app/api/workflows/[id]/route.ts +192 -0
- package/template/src/app/api/workflows/process/route.ts +293 -0
- package/template/src/app/api/workflows/route.ts +124 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +1416 -0
- package/template/src/app/layout.tsx +31 -0
- package/template/src/app/page.tsx +32 -0
- package/template/src/components/dashboard/activity-chart.tsx +67 -0
- package/template/src/components/dashboard/contacts-chart.tsx +63 -0
- package/template/src/components/dashboard/recent-activity.tsx +164 -0
- package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
- package/template/src/components/dashboard/stat-card.tsx +61 -0
- package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
- package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
- package/template/src/components/editor.tsx +856 -0
- package/template/src/components/email-template.tsx +35 -0
- package/template/src/components/header.tsx +320 -0
- package/template/src/components/invitation-email-template.tsx +79 -0
- package/template/src/components/meet-cancellation-email-template.tsx +120 -0
- package/template/src/components/meet-confirmation-email-template.tsx +156 -0
- package/template/src/components/meet-update-email-template.tsx +209 -0
- package/template/src/components/page-header.tsx +61 -0
- package/template/src/components/reset-password-email-template.tsx +79 -0
- package/template/src/components/sidebar.tsx +294 -0
- package/template/src/components/skeleton.tsx +380 -0
- package/template/src/components/ui/commands.tsx +396 -0
- package/template/src/components/ui/components.tsx +150 -0
- package/template/src/components/ui/theme.tsx +5 -0
- package/template/src/components/view-as-banner.tsx +45 -0
- package/template/src/components/view-as-modal.tsx +186 -0
- package/template/src/contexts/mobile-menu-context.tsx +31 -0
- package/template/src/contexts/sidebar-context.tsx +107 -0
- package/template/src/contexts/task-reminder-context.tsx +239 -0
- package/template/src/contexts/view-as-context.tsx +84 -0
- package/template/src/hooks/use-user-role.ts +82 -0
- package/template/src/lib/audit-log.ts +45 -0
- package/template/src/lib/auth-client.ts +16 -0
- package/template/src/lib/auth.ts +35 -0
- package/template/src/lib/check-permission.ts +193 -0
- package/template/src/lib/contact-duplicate.ts +112 -0
- package/template/src/lib/contact-interactions.ts +371 -0
- package/template/src/lib/encryption.ts +99 -0
- package/template/src/lib/google-calendar.ts +300 -0
- package/template/src/lib/google-drive.ts +372 -0
- package/template/src/lib/permissions.ts +412 -0
- package/template/src/lib/prisma.ts +32 -0
- package/template/src/lib/roles.ts +120 -0
- package/template/src/lib/template-variables.ts +76 -0
- package/template/src/lib/utils.ts +46 -0
- package/template/src/lib/workflow-executor.ts +482 -0
- package/template/src/proxy.ts +91 -0
- package/template/tsconfig.json +34 -0
- 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
|
+
}
|