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,95 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
|
|
5
|
+
// GET /api/reminders - Récupérer tous les rappels de l'utilisateur
|
|
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) {
|
|
13
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const now = new Date();
|
|
17
|
+
const future = new Date(now);
|
|
18
|
+
future.setDate(future.getDate() + 7); // 7 jours à venir
|
|
19
|
+
|
|
20
|
+
// Récupérer toutes les tâches non complétées de l'utilisateur avec rappels
|
|
21
|
+
const tasks = await prisma.task.findMany({
|
|
22
|
+
where: {
|
|
23
|
+
assignedUserId: session.user.id,
|
|
24
|
+
completed: false,
|
|
25
|
+
scheduledAt: {
|
|
26
|
+
gte: now,
|
|
27
|
+
lte: future,
|
|
28
|
+
},
|
|
29
|
+
reminderMinutesBefore: {
|
|
30
|
+
not: null,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
include: {
|
|
34
|
+
contact: {
|
|
35
|
+
select: {
|
|
36
|
+
id: true,
|
|
37
|
+
firstName: true,
|
|
38
|
+
lastName: true,
|
|
39
|
+
email: true,
|
|
40
|
+
phone: true,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
assignedUser: {
|
|
44
|
+
select: {
|
|
45
|
+
id: true,
|
|
46
|
+
name: true,
|
|
47
|
+
email: true,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
orderBy: {
|
|
52
|
+
scheduledAt: 'asc',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Formater les rappels - inclure toutes les tâches avec rappels
|
|
57
|
+
const reminders = tasks
|
|
58
|
+
.filter((task) => {
|
|
59
|
+
if (!task.reminderMinutesBefore) return false;
|
|
60
|
+
const scheduled = new Date(task.scheduledAt);
|
|
61
|
+
const reminderTime = new Date(scheduled.getTime() - task.reminderMinutesBefore * 60 * 1000);
|
|
62
|
+
// Inclure les rappels qui sont passés mais pas encore à l'heure de la tâche
|
|
63
|
+
// ou les rappels qui sont à venir (dans les 7 prochains jours)
|
|
64
|
+
const timeUntilReminder = reminderTime.getTime() - now.getTime();
|
|
65
|
+
return (reminderTime <= now && now < scheduled) || (timeUntilReminder > 0 && timeUntilReminder < 7 * 24 * 60 * 60 * 1000);
|
|
66
|
+
})
|
|
67
|
+
.map((task) => {
|
|
68
|
+
const scheduled = new Date(task.scheduledAt);
|
|
69
|
+
const reminderTime = new Date(
|
|
70
|
+
scheduled.getTime() - (task.reminderMinutesBefore || 0) * 60 * 1000,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
id: `${task.id}-reminder`,
|
|
75
|
+
taskId: task.id,
|
|
76
|
+
type: task.type,
|
|
77
|
+
title: task.title,
|
|
78
|
+
description: task.description,
|
|
79
|
+
priority: task.priority,
|
|
80
|
+
scheduledAt: task.scheduledAt,
|
|
81
|
+
reminderTime: reminderTime.toISOString(),
|
|
82
|
+
reminderMinutesBefore: task.reminderMinutesBefore,
|
|
83
|
+
contact: task.contact,
|
|
84
|
+
assignedUser: task.assignedUser,
|
|
85
|
+
};
|
|
86
|
+
})
|
|
87
|
+
.sort((a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime());
|
|
88
|
+
|
|
89
|
+
return NextResponse.json(reminders);
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
console.error('Erreur lors de la récupération des rappels:', error);
|
|
92
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
import { hashPassword } from '@/lib/auth';
|
|
4
|
+
|
|
5
|
+
export async function POST(request: NextRequest) {
|
|
6
|
+
try {
|
|
7
|
+
const body = await request.json();
|
|
8
|
+
const { token, password } = body;
|
|
9
|
+
|
|
10
|
+
if (!token || !password) {
|
|
11
|
+
return NextResponse.json({ error: 'Token et mot de passe requis' }, { status: 400 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (password.length < 6) {
|
|
15
|
+
return NextResponse.json(
|
|
16
|
+
{ error: 'Le mot de passe doit contenir au moins 6 caractères' },
|
|
17
|
+
{ status: 400 },
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Valider le token
|
|
22
|
+
const verification = await prisma.verification.findFirst({
|
|
23
|
+
where: {
|
|
24
|
+
value: token,
|
|
25
|
+
expiresAt: {
|
|
26
|
+
gt: new Date(),
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!verification) {
|
|
32
|
+
return NextResponse.json({ error: 'Lien invalide ou expiré' }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Trouver l'utilisateur
|
|
36
|
+
const user = await prisma.user.findUnique({
|
|
37
|
+
where: { email: verification.identifier },
|
|
38
|
+
include: {
|
|
39
|
+
accounts: {
|
|
40
|
+
where: { providerId: 'credential' },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!user || user.accounts.length === 0) {
|
|
46
|
+
return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Hasher le nouveau mot de passe
|
|
50
|
+
const hashedPassword = await hashPassword(password);
|
|
51
|
+
|
|
52
|
+
// Mettre à jour le mot de passe dans l'Account
|
|
53
|
+
await prisma.account.update({
|
|
54
|
+
where: { id: user.accounts[0].id },
|
|
55
|
+
data: {
|
|
56
|
+
password: hashedPassword,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Supprimer le token de vérification
|
|
61
|
+
await prisma.verification.delete({
|
|
62
|
+
where: { id: verification.id },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return NextResponse.json({
|
|
66
|
+
success: true,
|
|
67
|
+
message: 'Mot de passe réinitialisé avec succès',
|
|
68
|
+
});
|
|
69
|
+
} catch (error: any) {
|
|
70
|
+
console.error('Erreur lors de la complétion:', error);
|
|
71
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
|
|
4
|
+
export async function POST(request: NextRequest) {
|
|
5
|
+
try {
|
|
6
|
+
const body = await request.json();
|
|
7
|
+
const { email } = body;
|
|
8
|
+
|
|
9
|
+
if (!email) {
|
|
10
|
+
return NextResponse.json({ error: 'Email requis' }, { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Vérifier si l'utilisateur existe
|
|
14
|
+
const user = await prisma.user.findUnique({
|
|
15
|
+
where: { email },
|
|
16
|
+
include: {
|
|
17
|
+
accounts: {
|
|
18
|
+
where: { providerId: 'credential' },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Ne pas révéler si l'email existe ou non (sécurité)
|
|
24
|
+
// Mais on envoie quand même un message de succès
|
|
25
|
+
if (!user || user.accounts.length === 0) {
|
|
26
|
+
// Retourner un succès même si l'utilisateur n'existe pas (sécurité)
|
|
27
|
+
return NextResponse.json({
|
|
28
|
+
success: true,
|
|
29
|
+
message: 'Si cet email existe, un code vous a été envoyé',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Générer un code à 6 chiffres
|
|
34
|
+
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
|
35
|
+
|
|
36
|
+
// Créer ou mettre à jour le token de vérification
|
|
37
|
+
const expiresAt = new Date();
|
|
38
|
+
expiresAt.setMinutes(expiresAt.getMinutes() + 15); // Valide 15 minutes
|
|
39
|
+
|
|
40
|
+
// Supprimer les anciens tokens pour cet email
|
|
41
|
+
await prisma.verification.deleteMany({
|
|
42
|
+
where: {
|
|
43
|
+
identifier: email,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Créer le nouveau token avec le code
|
|
48
|
+
await prisma.verification.create({
|
|
49
|
+
data: {
|
|
50
|
+
id: crypto.randomUUID(),
|
|
51
|
+
identifier: email,
|
|
52
|
+
value: code,
|
|
53
|
+
expiresAt,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Envoyer l'email avec le code
|
|
58
|
+
const baseUrl = process.env.BETTER_AUTH_URL || 'http://localhost:3000';
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await fetch(`${baseUrl}/api/send`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
to: email,
|
|
66
|
+
subject: 'Code de réinitialisation de mot de passe',
|
|
67
|
+
template: 'reset-password',
|
|
68
|
+
code,
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
} catch (emailError) {
|
|
72
|
+
console.error("Erreur lors de l'envoi de l'email:", emailError);
|
|
73
|
+
// On continue même si l'email échoue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return NextResponse.json({
|
|
77
|
+
success: true,
|
|
78
|
+
message: 'Si cet email existe, un code vous a été envoyé',
|
|
79
|
+
});
|
|
80
|
+
} catch (error: any) {
|
|
81
|
+
console.error('Erreur lors de la demande de réinitialisation:', error);
|
|
82
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
|
|
4
|
+
export async function GET(request: NextRequest) {
|
|
5
|
+
try {
|
|
6
|
+
const { searchParams } = new URL(request.url);
|
|
7
|
+
const token = searchParams.get('token');
|
|
8
|
+
|
|
9
|
+
if (!token) {
|
|
10
|
+
return NextResponse.json({ error: 'Token manquant' }, { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Trouver le token de vérification
|
|
14
|
+
const verification = await prisma.verification.findFirst({
|
|
15
|
+
where: {
|
|
16
|
+
value: token,
|
|
17
|
+
expiresAt: {
|
|
18
|
+
gt: new Date(), // Pas expiré
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!verification) {
|
|
24
|
+
return NextResponse.json({ error: 'Lien invalide ou expiré' }, { status: 400 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Vérifier si l'utilisateur existe et a un compte
|
|
28
|
+
const user = await prisma.user.findUnique({
|
|
29
|
+
where: { email: verification.identifier },
|
|
30
|
+
include: {
|
|
31
|
+
accounts: {
|
|
32
|
+
where: { providerId: 'credential' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!user || user.accounts.length === 0) {
|
|
38
|
+
return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return NextResponse.json({
|
|
42
|
+
email: user.email,
|
|
43
|
+
valid: true,
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Erreur lors de la validation:', error);
|
|
47
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
|
|
4
|
+
export async function POST(request: NextRequest) {
|
|
5
|
+
try {
|
|
6
|
+
const body = await request.json();
|
|
7
|
+
const { email, code } = body;
|
|
8
|
+
|
|
9
|
+
if (!email || !code) {
|
|
10
|
+
return NextResponse.json({ error: 'Email et code requis' }, { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (code.length !== 6 || !/^\d+$/.test(code)) {
|
|
14
|
+
return NextResponse.json({ error: 'Code invalide' }, { status: 400 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Trouver le token de vérification
|
|
18
|
+
const verification = await prisma.verification.findFirst({
|
|
19
|
+
where: {
|
|
20
|
+
identifier: email,
|
|
21
|
+
value: code,
|
|
22
|
+
expiresAt: {
|
|
23
|
+
gt: new Date(), // Pas expiré
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!verification) {
|
|
29
|
+
return NextResponse.json({ error: 'Code invalide ou expiré' }, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Vérifier si l'utilisateur existe et a un compte
|
|
33
|
+
const user = await prisma.user.findUnique({
|
|
34
|
+
where: { email },
|
|
35
|
+
include: {
|
|
36
|
+
accounts: {
|
|
37
|
+
where: { providerId: 'credential' },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!user || user.accounts.length === 0) {
|
|
43
|
+
return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Générer un token pour la réinitialisation (différent du code)
|
|
47
|
+
const resetToken = crypto.randomUUID();
|
|
48
|
+
const expiresAt = new Date();
|
|
49
|
+
expiresAt.setHours(expiresAt.getHours() + 1); // Valide 1 heure
|
|
50
|
+
|
|
51
|
+
// Supprimer l'ancien token de code
|
|
52
|
+
await prisma.verification.delete({
|
|
53
|
+
where: { id: verification.id },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Créer un nouveau token pour la réinitialisation
|
|
57
|
+
await prisma.verification.create({
|
|
58
|
+
data: {
|
|
59
|
+
id: crypto.randomUUID(),
|
|
60
|
+
identifier: email,
|
|
61
|
+
value: resetToken,
|
|
62
|
+
expiresAt,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return NextResponse.json({
|
|
67
|
+
success: true,
|
|
68
|
+
token: resetToken,
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Erreur lors de la vérification:', error);
|
|
72
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { headers } from 'next/headers';
|
|
4
|
+
import { prisma } from '@/lib/prisma';
|
|
5
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
6
|
+
import { logAudit } from '@/lib/audit-log';
|
|
7
|
+
|
|
8
|
+
// PUT /api/roles/[id] - Modifier un profil
|
|
9
|
+
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
10
|
+
try {
|
|
11
|
+
const session = await auth.api.getSession({
|
|
12
|
+
headers: await headers(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (!session) {
|
|
16
|
+
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Vérifier que l'utilisateur a la permission de gérer les rôles
|
|
20
|
+
const hasPermission = await checkPermission('users.manage_roles');
|
|
21
|
+
if (!hasPermission) {
|
|
22
|
+
return NextResponse.json({ error: 'Non autorisé' }, { status: 403 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { id } = await params;
|
|
26
|
+
const body = await req.json();
|
|
27
|
+
const { name, description, permissions } = body;
|
|
28
|
+
|
|
29
|
+
// Vérifier que le profil existe
|
|
30
|
+
const existingRole = await prisma.customRole.findUnique({
|
|
31
|
+
where: { id },
|
|
32
|
+
include: {
|
|
33
|
+
_count: {
|
|
34
|
+
select: { users: true },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!existingRole) {
|
|
40
|
+
return NextResponse.json({ error: 'Profil non trouvé' }, { status: 404 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Validation
|
|
44
|
+
if (name && typeof name !== 'string') {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ error: 'Le nom doit être une chaîne de caractères' },
|
|
47
|
+
{ status: 400 },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (permissions && !Array.isArray(permissions)) {
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{ error: 'Les permissions doivent être un tableau' },
|
|
54
|
+
{ status: 400 },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Vérifier que le nouveau nom n'existe pas déjà (si changé)
|
|
59
|
+
if (name && name.trim() !== existingRole.name) {
|
|
60
|
+
const duplicateName = await prisma.customRole.findUnique({
|
|
61
|
+
where: { name: name.trim() },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (duplicateName) {
|
|
65
|
+
return NextResponse.json({ error: 'Un profil avec ce nom existe déjà' }, { status: 400 });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Mettre à jour le profil
|
|
70
|
+
const updatedRole = await prisma.customRole.update({
|
|
71
|
+
where: { id },
|
|
72
|
+
data: {
|
|
73
|
+
...(name && { name: name.trim() }),
|
|
74
|
+
...(description !== undefined && { description: description?.trim() || null }),
|
|
75
|
+
...(permissions && { permissions }),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await logAudit({
|
|
80
|
+
actorId: session.user.id,
|
|
81
|
+
action: 'ROLE_UPDATED',
|
|
82
|
+
entityType: 'ROLE',
|
|
83
|
+
entityId: updatedRole.id,
|
|
84
|
+
metadata: {
|
|
85
|
+
before: {
|
|
86
|
+
name: existingRole.name,
|
|
87
|
+
description: existingRole.description,
|
|
88
|
+
permissions: existingRole.permissions,
|
|
89
|
+
},
|
|
90
|
+
after: {
|
|
91
|
+
name: updatedRole.name,
|
|
92
|
+
description: updatedRole.description,
|
|
93
|
+
permissions: updatedRole.permissions,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return NextResponse.json({
|
|
99
|
+
message: 'Profil modifié avec succès',
|
|
100
|
+
role: {
|
|
101
|
+
id: updatedRole.id,
|
|
102
|
+
name: updatedRole.name,
|
|
103
|
+
description: updatedRole.description,
|
|
104
|
+
permissions: updatedRole.permissions,
|
|
105
|
+
isSystem: updatedRole.isSystem,
|
|
106
|
+
usersCount: existingRole._count.users,
|
|
107
|
+
createdAt: updatedRole.createdAt.toISOString(),
|
|
108
|
+
updatedAt: updatedRole.updatedAt.toISOString(),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Erreur lors de la modification du profil:', error);
|
|
113
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// DELETE /api/roles/[id] - Supprimer un profil
|
|
118
|
+
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
119
|
+
try {
|
|
120
|
+
const session = await auth.api.getSession({
|
|
121
|
+
headers: await headers(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!session) {
|
|
125
|
+
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Vérifier que l'utilisateur a la permission de gérer les rôles
|
|
129
|
+
const hasPermission = await checkPermission('users.manage_roles');
|
|
130
|
+
if (!hasPermission) {
|
|
131
|
+
return NextResponse.json({ error: 'Non autorisé' }, { status: 403 });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { id } = await params;
|
|
135
|
+
|
|
136
|
+
// Vérifier que le profil existe
|
|
137
|
+
const existingRole = await prisma.customRole.findUnique({
|
|
138
|
+
where: { id },
|
|
139
|
+
include: {
|
|
140
|
+
_count: {
|
|
141
|
+
select: { users: true },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!existingRole) {
|
|
147
|
+
return NextResponse.json({ error: 'Profil non trouvé' }, { status: 404 });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Empêcher la suppression si des utilisateurs utilisent ce profil
|
|
151
|
+
if (existingRole._count.users > 0) {
|
|
152
|
+
return NextResponse.json(
|
|
153
|
+
{
|
|
154
|
+
error: `Ce profil ne peut pas être supprimé car ${existingRole._count.users} l'utilisent`,
|
|
155
|
+
},
|
|
156
|
+
{ status: 400 },
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Supprimer le profil
|
|
161
|
+
await prisma.customRole.delete({
|
|
162
|
+
where: { id },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await logAudit({
|
|
166
|
+
actorId: session.user.id,
|
|
167
|
+
action: 'ROLE_DELETED',
|
|
168
|
+
entityType: 'ROLE',
|
|
169
|
+
entityId: existingRole.id,
|
|
170
|
+
metadata: {
|
|
171
|
+
name: existingRole.name,
|
|
172
|
+
description: existingRole.description,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return NextResponse.json({
|
|
177
|
+
message: 'Profil supprimé avec succès',
|
|
178
|
+
});
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error('Erreur lors de la suppression du profil:', error);
|
|
181
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { headers } from 'next/headers';
|
|
4
|
+
import { prisma } from '@/lib/prisma';
|
|
5
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
6
|
+
import { logAudit } from '@/lib/audit-log';
|
|
7
|
+
|
|
8
|
+
// GET /api/roles - Récupérer tous les profils (système + personnalisés)
|
|
9
|
+
export async function GET(req: NextRequest) {
|
|
10
|
+
try {
|
|
11
|
+
const session = await auth.api.getSession({
|
|
12
|
+
headers: await headers(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (!session) {
|
|
16
|
+
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Vérifier que l'utilisateur a la permission de gérer les rôles
|
|
20
|
+
const hasPermission = await checkPermission('users.manage_roles');
|
|
21
|
+
if (!hasPermission) {
|
|
22
|
+
return NextResponse.json({ error: 'Non autorisé' }, { status: 403 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Récupérer tous les profils depuis la BDD (système + personnalisés)
|
|
26
|
+
const roles = await prisma.customRole.findMany({
|
|
27
|
+
include: {
|
|
28
|
+
_count: {
|
|
29
|
+
select: { users: true },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
orderBy: [
|
|
33
|
+
{ isSystem: 'desc' }, // Profils système en premier
|
|
34
|
+
{ createdAt: 'desc' }, // Puis les plus récents
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Formatter les profils
|
|
39
|
+
const formattedRoles = roles.map((role) => ({
|
|
40
|
+
id: role.id,
|
|
41
|
+
name: role.name,
|
|
42
|
+
description: role.description,
|
|
43
|
+
permissions: role.permissions as string[],
|
|
44
|
+
isSystem: role.isSystem,
|
|
45
|
+
usersCount: role._count.users,
|
|
46
|
+
createdAt: role.createdAt.toISOString(),
|
|
47
|
+
updatedAt: role.updatedAt.toISOString(),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
return NextResponse.json(formattedRoles);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Erreur lors de la récupération des profils:', error);
|
|
53
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// POST /api/roles - Créer un nouveau profil personnalisé
|
|
58
|
+
export async function POST(req: NextRequest) {
|
|
59
|
+
try {
|
|
60
|
+
const session = await auth.api.getSession({
|
|
61
|
+
headers: await headers(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!session) {
|
|
65
|
+
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Vérifier que l'utilisateur a la permission de gérer les rôles
|
|
69
|
+
const hasPermission = await checkPermission('users.manage_roles');
|
|
70
|
+
if (!hasPermission) {
|
|
71
|
+
return NextResponse.json({ error: 'Non autorisé' }, { status: 403 });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const body = await req.json();
|
|
75
|
+
const { name, description, permissions } = body;
|
|
76
|
+
|
|
77
|
+
// Validation
|
|
78
|
+
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
79
|
+
return NextResponse.json({ error: 'Le nom du profil est requis' }, { status: 400 });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!Array.isArray(permissions)) {
|
|
83
|
+
return NextResponse.json(
|
|
84
|
+
{ error: 'Les permissions doivent être un tableau' },
|
|
85
|
+
{ status: 400 },
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Vérifier que le nom n'existe pas déjà
|
|
90
|
+
const existing = await prisma.customRole.findUnique({
|
|
91
|
+
where: { name: name.trim() },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (existing) {
|
|
95
|
+
return NextResponse.json({ error: 'Un profil avec ce nom existe déjà' }, { status: 400 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Créer le profil
|
|
99
|
+
const newRole = await prisma.customRole.create({
|
|
100
|
+
data: {
|
|
101
|
+
name: name.trim(),
|
|
102
|
+
description: description?.trim() || null,
|
|
103
|
+
permissions: permissions,
|
|
104
|
+
isSystem: false,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await logAudit({
|
|
109
|
+
actorId: session.user.id,
|
|
110
|
+
action: 'ROLE_CREATED',
|
|
111
|
+
entityType: 'ROLE',
|
|
112
|
+
entityId: newRole.id,
|
|
113
|
+
metadata: {
|
|
114
|
+
name: newRole.name,
|
|
115
|
+
description: newRole.description,
|
|
116
|
+
permissions,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return NextResponse.json(
|
|
121
|
+
{
|
|
122
|
+
message: 'Profil créé avec succès',
|
|
123
|
+
role: {
|
|
124
|
+
id: newRole.id,
|
|
125
|
+
name: newRole.name,
|
|
126
|
+
description: newRole.description,
|
|
127
|
+
permissions: newRole.permissions,
|
|
128
|
+
isSystem: newRole.isSystem,
|
|
129
|
+
usersCount: 0,
|
|
130
|
+
createdAt: newRole.createdAt.toISOString(),
|
|
131
|
+
updatedAt: newRole.updatedAt.toISOString(),
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{ status: 201 },
|
|
135
|
+
);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('Erreur lors de la création du profil:', error);
|
|
138
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
139
|
+
}
|
|
140
|
+
}
|