From 23ab7d04ef0c940b7008e8bc7d4b43b373d16d40 Mon Sep 17 00:00:00 2001 From: N3WT DE COMPET Date: Fri, 30 May 2025 14:19:01 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Utilisation=20d'une=20clef=20API=20Docu?= =?UTF-8?q?seal=20par=20=C3=A9tablissement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Back-End/Auth/views.py | 2 +- Back-End/DocuSeal/views.py | 113 ++++++++++++------ Back-End/Establishment/models.py | 1 + Back-End/N3wtSchool/settings.py | 4 +- Back-End/start.py | 2 +- Front-End/prod.env | 3 +- Front-End/src/app/[locale]/admin/page.js | 23 +++- .../src/app/[locale]/admin/structure/page.js | 3 +- .../subscriptions/createSubscription/page.js | 13 +- .../subscriptions/editSubscription/page.js | 3 +- .../[locale]/parents/editSubscription/page.js | 3 +- .../app/actions/registerFileGroupAction.js | 29 ++++- .../Structure/Files/FileUploadDocuSeal.js | 10 +- .../Structure/Files/FilesGroupsManagement.js | 60 ++++------ Front-End/src/context/EstablishmentContext.js | 18 +++ .../src/pages/api/docuseal/cloneTemplate.js | 5 +- .../api/docuseal/downloadTemplate/[slug].js | 6 +- .../src/pages/api/docuseal/generateToken.js | 6 +- .../src/pages/api/docuseal/removeTemplate.js | 6 +- Front-End/src/utils/Url.js | 1 + docker-compose.yml | 79 ++++++++---- 21 files changed, 256 insertions(+), 134 deletions(-) diff --git a/Back-End/Auth/views.py b/Back-End/Auth/views.py index 45a050b..9594c74 100644 --- a/Back-End/Auth/views.py +++ b/Back-End/Auth/views.py @@ -223,7 +223,7 @@ def makeToken(user): """ try: # Récupérer tous les rôles de l'utilisateur actifs - roles = ProfileRole.objects.filter(profile=user, is_active=True).values('role_type', 'establishment__id', 'establishment__name', 'establishment__evaluation_frequency', 'establishment__total_capacity') + roles = ProfileRole.objects.filter(profile=user, is_active=True).values('role_type', 'establishment__id', 'establishment__name', 'establishment__evaluation_frequency', 'establishment__total_capacity', 'establishment__api_docuseal') # Générer le JWT avec la bonne syntaxe datetime access_payload = { diff --git a/Back-End/DocuSeal/views.py b/Back-End/DocuSeal/views.py index f2b1e03..9a21cb3 100644 --- a/Back-End/DocuSeal/views.py +++ b/Back-End/DocuSeal/views.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from rest_framework.decorators import api_view from rest_framework.response import Response @@ -7,49 +6,67 @@ from rest_framework import status import jwt import datetime import requests +from Establishment.models import Establishment @csrf_exempt @api_view(['POST']) def generate_jwt_token(request): - # Vérifier la clé API + # Récupérer l'établissement concerné (par ID ou autre info transmise) + establishment_id = request.data.get('establishment_id') + if not establishment_id: + return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST) + + try: + establishment = Establishment.objects.get(id=establishment_id) + except Establishment.DoesNotExist: + return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND) + + # Vérifier la clé API reçue dans le header api_key = request.headers.get('X-Auth-Token') - if not api_key or api_key != settings.DOCUSEAL_JWT["API_KEY"]: - return Response({'error': 'Invalid API key'}, status=status.HTTP_401_UNAUTHORIZED) + if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal: + return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED) # Récupérer les données de la requête user_email = request.data.get('user_email') documents_urls = request.data.get('documents_urls', []) - id = request.data.get('id') # Récupérer le id + template_id = request.data.get('id') - # Vérifier les données requises if not user_email: return Response({'error': 'User email is required'}, status=status.HTTP_400_BAD_REQUEST) - # Utiliser la configuration JWT de DocuSeal depuis les settings - jwt_secret = settings.DOCUSEAL_JWT['API_KEY'] + # Utiliser la clé API de l'établissement comme secret JWT + jwt_secret = establishment.api_docuseal jwt_algorithm = settings.DOCUSEAL_JWT['ALGORITHM'] expiration_delta = settings.DOCUSEAL_JWT['EXPIRATION_DELTA'] - # Définir le payload payload = { 'user_email': user_email, 'documents_urls': documents_urls, - 'template_id': id, # Ajouter le id au payload - 'exp': datetime.datetime.utcnow() + expiration_delta # Temps d'expiration du token + 'template_id': template_id, + 'exp': datetime.datetime.utcnow() + expiration_delta } - # Générer le token JWT token = jwt.encode(payload, jwt_secret, algorithm=jwt_algorithm) - return Response({'token': token}, status=status.HTTP_200_OK) @csrf_exempt @api_view(['POST']) def clone_template(request): - # Vérifier la clé API + # Récupérer l'établissement concerné + establishment_id = request.data.get('establishment_id') + print(f"establishment_id : {establishment_id}") + if not establishment_id: + return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST) + + try: + establishment = Establishment.objects.get(id=establishment_id) + except Establishment.DoesNotExist: + return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND) + + # Vérifier la clé API reçue dans le header api_key = request.headers.get('X-Auth-Token') - if not api_key or api_key != settings.DOCUSEAL_JWT["API_KEY"]: - return Response({'error': 'Invalid API key'}, status=status.HTTP_401_UNAUTHORIZED) + if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal: + return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED) # Récupérer les données de la requête document_id = request.data.get('templateId') @@ -57,7 +74,7 @@ def clone_template(request): is_required = request.data.get('is_required') # Vérifier les données requises - if not document_id : + if not document_id: return Response({'error': 'template ID is required'}, status=status.HTTP_400_BAD_REQUEST) # URL de l'API de DocuSeal pour cloner le template @@ -67,7 +84,7 @@ def clone_template(request): try: response = requests.post(clone_url, headers={ 'Content-Type': 'application/json', - 'X-Auth-Token': settings.DOCUSEAL_JWT['API_KEY'] + 'X-Auth-Token': establishment.api_docuseal }) if response.status_code != status.HTTP_200_OK: @@ -79,12 +96,15 @@ def clone_template(request): # URL de l'API de DocuSeal pour créer une submission submission_url = f'https://docuseal.com/api/submissions' - # Faire la requête pour cloner le template try: clone_id = data['id'] - response = requests.post(submission_url, json={'template_id':clone_id, 'send_email': False, 'submitters': [{'email': email}]}, headers={ + response = requests.post(submission_url, json={ + 'template_id': clone_id, + 'send_email': False, + 'submitters': [{'email': email}] + }, headers={ 'Content-Type': 'application/json', - 'X-Auth-Token': settings.DOCUSEAL_JWT['API_KEY'] + 'X-Auth-Token': establishment.api_docuseal }) if response.status_code != status.HTTP_200_OK: @@ -93,10 +113,10 @@ def clone_template(request): data = response.json() data[0]['id'] = clone_id return Response(data[0], status=status.HTTP_200_OK) - + except requests.RequestException as e: return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - else : + else: print(f'NOT REQUIRED -> on ne crée pas de submission') return Response(data, status=status.HTTP_200_OK) @@ -106,18 +126,28 @@ def clone_template(request): @csrf_exempt @api_view(['DELETE']) def remove_template(request, id): - # Vérifier la clé API - api_key = request.headers.get('X-Auth-Token') - if not api_key or api_key != settings.DOCUSEAL_JWT["API_KEY"]: - return Response({'error': 'Invalid API key'}, status=status.HTTP_401_UNAUTHORIZED) + # Récupérer l'établissement concerné + establishment_id = request.GET.get('establishment_id') + if not establishment_id: + return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST) - # URL de l'API de DocuSeal pour cloner le template + try: + establishment = Establishment.objects.get(id=establishment_id) + except Establishment.DoesNotExist: + return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND) + + # Vérifier la clé API reçue dans le header + api_key = request.headers.get('X-Auth-Token') + if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal: + return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED) + + # URL de l'API de DocuSeal pour supprimer le template + clone_url = f'https://docuseal.com/api/templates/{id}' - # Faire la requête pour cloner le template try: response = requests.delete(clone_url, headers={ - 'X-Auth-Token': settings.DOCUSEAL_JWT['API_KEY'] + 'X-Auth-Token': establishment.api_docuseal }) if response.status_code != status.HTTP_200_OK: @@ -132,23 +162,32 @@ def remove_template(request, id): @csrf_exempt @api_view(['GET']) def download_template(request, slug): - # Vérifier la clé API + # Récupérer l'établissement concerné + establishment_id = request.GET.get('establishment_id') + if not establishment_id: + return Response({'error': 'establishment_id requis'}, status=status.HTTP_400_BAD_REQUEST) + + try: + establishment = Establishment.objects.get(id=establishment_id) + except Establishment.DoesNotExist: + return Response({'error': "Établissement introuvable"}, status=status.HTTP_404_NOT_FOUND) + + # Vérifier la clé API reçue dans le header api_key = request.headers.get('X-Auth-Token') - if not api_key or api_key != settings.DOCUSEAL_JWT["API_KEY"]: - return Response({'error': 'Invalid API key'}, status=status.HTTP_401_UNAUTHORIZED) + if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal: + return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED) # Vérifier les données requises - if not slug : + if not slug: return Response({'error': 'slug is required'}, status=status.HTTP_400_BAD_REQUEST) - # URL de l'API de DocuSeal pour cloner le template + # URL de l'API de DocuSeal pour télécharger le template download_url = f'https://docuseal.com/submitters/{slug}/download' - # Faire la requête pour cloner le template try: response = requests.get(download_url, headers={ 'Content-Type': 'application/json', - 'X-Auth-Token': settings.DOCUSEAL_JWT['API_KEY'] + 'X-Auth-Token': establishment.api_docuseal }) if response.status_code != status.HTTP_200_OK: diff --git a/Back-End/Establishment/models.py b/Back-End/Establishment/models.py index cbeb4d5..86d8a1d 100644 --- a/Back-End/Establishment/models.py +++ b/Back-End/Establishment/models.py @@ -21,6 +21,7 @@ class Establishment(models.Model): licence_code = models.CharField(max_length=100, blank=True) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) + api_docuseal = models.CharField(max_length=255, blank=True, null=True) def __str__(self): return self.name \ No newline at end of file diff --git a/Back-End/N3wtSchool/settings.py b/Back-End/N3wtSchool/settings.py index 73a97fd..202c926 100644 --- a/Back-End/N3wtSchool/settings.py +++ b/Back-End/N3wtSchool/settings.py @@ -363,12 +363,10 @@ SIMPLE_JWT = { } # Configuration for DocuSeal JWT -DOCUSEAL_API_KEY="LRvUTQCbMSSpManYKshdQk9Do6rBQgjHyPrbGfxU3Jg" DOCUSEAL_JWT = { 'ALGORITHM': 'HS256', 'SIGNING_KEY': SECRET_KEY, - 'EXPIRATION_DELTA': timedelta(hours=1), - 'API_KEY': DOCUSEAL_API_KEY + 'EXPIRATION_DELTA': timedelta(hours=1) } # Django Channels Configuration diff --git a/Back-End/start.py b/Back-End/start.py index 570bdbf..867a684 100644 --- a/Back-End/start.py +++ b/Back-End/start.py @@ -14,7 +14,7 @@ test_mode = os.getenv('TEST_MODE', 'False') == 'True' commands = [ ["python", "manage.py", "collectstatic", "--noinput"], - #["python", "manage.py", "flush", "--noinput"], + ["python", "manage.py", "flush", "--noinput"], ["python", "manage.py", "makemigrations", "Common", "--noinput"], ["python", "manage.py", "makemigrations", "Establishment", "--noinput"], ["python", "manage.py", "makemigrations", "Settings", "--noinput"], diff --git a/Front-End/prod.env b/Front-End/prod.env index 99015de..d83185a 100644 --- a/Front-End/prod.env +++ b/Front-End/prod.env @@ -2,5 +2,4 @@ NEXT_PUBLIC_API_URL=_NEXT_PUBLIC_API_URL_ NEXT_PUBLIC_WSAPI_URL=_NEXT_PUBLIC_WSAPI_URL_ NEXT_PUBLIC_USE_FAKE_DATA=_NEXT_PUBLIC_USE_FAKE_DATA_ AUTH_SECRET=_AUTH_SECRET_ -NEXTAUTH_URL=_NEXTAUTH_URL_ -DOCUSEAL_API_KEY=_DOCUSEAL_API_KEY_ \ No newline at end of file +NEXTAUTH_URL=_NEXTAUTH_URL_ \ No newline at end of file diff --git a/Front-End/src/app/[locale]/admin/page.js b/Front-End/src/app/[locale]/admin/page.js index 9af7345..aadba19 100644 --- a/Front-End/src/app/[locale]/admin/page.js +++ b/Front-End/src/app/[locale]/admin/page.js @@ -1,7 +1,7 @@ 'use client'; import React, { useState, useEffect } from 'react'; import { useTranslations } from 'next-intl'; -import { Users, Clock, CalendarCheck, School } from 'lucide-react'; +import { Users, Clock, CalendarCheck, School, AlertTriangle, CheckCircle2 } from 'lucide-react'; import Loader from '@/components/Loader'; import StatCard from '@/components/StatCard'; import logger from '@/utils/logger'; @@ -39,7 +39,7 @@ export default function DashboardPage() { const [upcomingEvents, setUpcomingEvents] = useState([]); const [absencesToday, setAbsencesToday] = useState([]); - const { selectedEstablishmentId, selectedEstablishmentTotalCapacity } = + const { selectedEstablishmentId, selectedEstablishmentTotalCapacity, apiDocuseal } = useEstablishment(); const [statusDistribution, setStatusDistribution] = useState([ @@ -168,7 +168,24 @@ export default function DashboardPage() { return (
-

{t('dashboard')}

+
+ + {apiDocuseal ? ( + + ) : ( + + )} + {apiDocuseal + ? 'Clé API Docuseal renseignée' + : 'Clé API Docuseal manquante'} + +
{/* Statistiques principales */}
diff --git a/Front-End/src/app/[locale]/admin/structure/page.js b/Front-End/src/app/[locale]/admin/structure/page.js index 7ba682d..1e38917 100644 --- a/Front-End/src/app/[locale]/admin/structure/page.js +++ b/Front-End/src/app/[locale]/admin/structure/page.js @@ -52,7 +52,7 @@ export default function Page() { ); const csrfToken = useCsrfToken(); - const { selectedEstablishmentId } = useEstablishment(); + const { selectedEstablishmentId, apiDocuseal } = useEstablishment(); useEffect(() => { if (selectedEstablishmentId) { @@ -353,6 +353,7 @@ export default function Page() {
), diff --git a/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js index 1b07efd..63735da 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/createSubscription/page.js @@ -96,7 +96,7 @@ export default function CreateSubscriptionPage() { const { getNiveauLabel } = useClasses(); const formDataRef = useRef(formData); - const { selectedEstablishmentId } = useEstablishment(); + const { selectedEstablishmentId, apiDocuseal } = useEstablishment(); const csrfToken = useCsrfToken(); const router = useRouter(); @@ -473,8 +473,6 @@ export default function CreateSubscriptionPage() { } })(); - logger.debug('test : ', guardians); - const data = { student: { last_name: formDataRef.current.studentLastName, @@ -532,12 +530,14 @@ export default function CreateSubscriptionPage() { const clonePromises = masters.map((templateMaster) => cloneTemplate( templateMaster.id, - formData.guardianEmail, - templateMaster.is_required + formDataRef.current.guardianEmail, + templateMaster.is_required, + selectedEstablishmentId, + apiDocuseal ) .then((clonedDocument) => { const cloneData = { - name: `${templateMaster.name}_${formData.studentFirstName}_${formData.studentLastName}`, + name: `${templateMaster.name}_${formDataRef.current.studentFirstName}_${formDataRef.current.studentLastName}`, slug: clonedDocument.slug, id: clonedDocument.id, master: templateMaster.id, @@ -655,6 +655,7 @@ export default function CreateSubscriptionPage() { return { ...prevData, selectedGuardians: updatedSelectedGuardians, + guardianEmail: guardian.associated_profile_email, }; }); }; diff --git a/Front-End/src/app/[locale]/admin/subscriptions/editSubscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/editSubscription/page.js index b730d75..0f47c0c 100644 --- a/Front-End/src/app/[locale]/admin/subscriptions/editSubscription/page.js +++ b/Front-End/src/app/[locale]/admin/subscriptions/editSubscription/page.js @@ -17,7 +17,7 @@ export default function Page() { const [formErrors, setFormErrors] = useState({}); const csrfToken = useCsrfToken(); - const { selectedEstablishmentId } = useEstablishment(); + const { selectedEstablishmentId, apiDocuseal } = useEstablishment(); const [isLoading, setIsLoading] = useState(false); const handleSubmit = (data) => { @@ -47,6 +47,7 @@ export default function Page() { studentId={studentId} csrfToken={csrfToken} selectedEstablishmentId={selectedEstablishmentId} + apiDocuseal = {apiDocuseal} onSubmit={handleSubmit} cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL} errors={formErrors} diff --git a/Front-End/src/app/[locale]/parents/editSubscription/page.js b/Front-End/src/app/[locale]/parents/editSubscription/page.js index 7b79610..736bd24 100644 --- a/Front-End/src/app/[locale]/parents/editSubscription/page.js +++ b/Front-End/src/app/[locale]/parents/editSubscription/page.js @@ -14,7 +14,7 @@ export default function Page() { const enable = searchParams.get('enabled') === 'true'; const router = useRouter(); const csrfToken = useCsrfToken(); - const { selectedEstablishmentId } = useEstablishment(); + const { selectedEstablishmentId, apiDocuseal } = useEstablishment(); const handleSubmit = async (data) => { try { @@ -31,6 +31,7 @@ export default function Page() { studentId={studentId} csrfToken={csrfToken} selectedEstablishmentId={selectedEstablishmentId} + apiDocuseal = {apiDocuseal} onSubmit={handleSubmit} cancelUrl={FE_PARENTS_HOME_URL} enable={enable} diff --git a/Front-End/src/app/actions/registerFileGroupAction.js b/Front-End/src/app/actions/registerFileGroupAction.js index 611b82b..444ceec 100644 --- a/Front-End/src/app/actions/registerFileGroupAction.js +++ b/Front-End/src/app/actions/registerFileGroupAction.js @@ -7,6 +7,7 @@ import { FE_API_DOCUSEAL_CLONE_URL, FE_API_DOCUSEAL_DOWNLOAD_URL, FE_API_DOCUSEAL_GENERATE_TOKEN, + FE_API_DOCUSEAL_DELETE_URL } from '@/utils/Url'; import { errorHandler, requestResponseHandler } from './actionsHandlers'; @@ -337,8 +338,23 @@ export const deleteRegistrationParentFileTemplate = (id, csrfToken) => { }; // API requests +export const removeTemplate = (templateId, selectedEstablishmentId, apiDocuseal) => { + return fetch(`${FE_API_DOCUSEAL_DELETE_URL}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + templateId, + establishment_id :selectedEstablishmentId, + apiDocuseal + }), + }) + .then(requestResponseHandler) + .catch(errorHandler); +}; -export const cloneTemplate = (templateId, email, is_required) => { +export const cloneTemplate = (templateId, email, is_required, selectedEstablishmentId, apiDocuseal) => { return fetch(`${FE_API_DOCUSEAL_CLONE_URL}`, { method: 'POST', headers: { @@ -348,14 +364,17 @@ export const cloneTemplate = (templateId, email, is_required) => { templateId, email, is_required, + establishment_id :selectedEstablishmentId, + apiDocuseal }), }) .then(requestResponseHandler) .catch(errorHandler); }; -export const downloadTemplate = (slug) => { - return fetch(`${FE_API_DOCUSEAL_DOWNLOAD_URL}/${slug}`, { +export const downloadTemplate = (slug, selectedEstablishmentId, apiDocuseal) => { + const url = `${FE_API_DOCUSEAL_DOWNLOAD_URL}/${slug}?establishment_id=${selectedEstablishmentId}&apiDocuseal=${apiDocuseal}`; + return fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -365,13 +384,13 @@ export const downloadTemplate = (slug) => { .catch(errorHandler); }; -export const generateToken = (email, id = null) => { +export const generateToken = (email, id = null, selectedEstablishmentId, apiDocuseal) => { return fetch(`${FE_API_DOCUSEAL_GENERATE_TOKEN}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ user_email: email, id }), + body: JSON.stringify({ user_email: email, id, establishment_id :selectedEstablishmentId, apiDocuseal }), }) .then(requestResponseHandler) .catch(errorHandler); diff --git a/Front-End/src/components/Structure/Files/FileUploadDocuSeal.js b/Front-End/src/components/Structure/Files/FileUploadDocuSeal.js index 39f946f..da9f70f 100644 --- a/Front-End/src/components/Structure/Files/FileUploadDocuSeal.js +++ b/Front-End/src/components/Structure/Files/FileUploadDocuSeal.js @@ -30,7 +30,7 @@ export default function FileUploadDocuSeal({ const csrfToken = useCsrfToken(); - const { selectedEstablishmentId } = useEstablishment(); + const { selectedEstablishmentId, user, apiDocuseal } = useEstablishment(); useEffect(() => { fetchRegistrationFileGroups(selectedEstablishmentId).then((data) => @@ -44,10 +44,12 @@ export default function FileUploadDocuSeal({ }, [fileToEdit]); useEffect(() => { - const email = 'n3wt.school@gmail.com'; + if (!user && !user?.email) { + return; + } const id = fileToEdit ? fileToEdit.id : null; - generateToken(email, id) + generateToken(user?.email, id, selectedEstablishmentId, apiDocuseal) .then((data) => { setToken(data.token); }) @@ -119,7 +121,7 @@ export default function FileUploadDocuSeal({ guardianDetails.forEach((guardian, index) => { logger.debug('creation du clone avec required : ', is_required); - cloneTemplate(templateMaster?.id, guardian.email, is_required) + cloneTemplate(templateMaster?.id, guardian.email, is_required, selectedEstablishmentId, apiDocuseal) .then((clonedDocument) => { // Sauvegarde des schoolFileTemplates clonés dans la base de données const data = { diff --git a/Front-End/src/components/Structure/Files/FilesGroupsManagement.js b/Front-End/src/components/Structure/Files/FilesGroupsManagement.js index 7e7f0ff..ad1ff4c 100644 --- a/Front-End/src/components/Structure/Files/FilesGroupsManagement.js +++ b/Front-End/src/components/Structure/Files/FilesGroupsManagement.js @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Download, Edit3, Trash2, FolderPlus, Signature } from 'lucide-react'; +import { Download, Edit3, Trash2, FolderPlus, Signature, AlertTriangle } from 'lucide-react'; import Modal from '@/components/Modal'; import Table from '@/components/Table'; import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal'; @@ -22,6 +22,8 @@ import { deleteRegistrationFileGroup, deleteRegistrationSchoolFileMaster, deleteRegistrationParentFileMaster, + + removeTemplate } from '@/app/actions/registerFileGroupAction'; import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm'; import logger from '@/utils/logger'; @@ -35,6 +37,7 @@ import AlertMessage from '@/components/AlertMessage'; export default function FilesGroupsManagement({ csrfToken, selectedEstablishmentId, + apiDocuseal }) { const [schoolFileMasters, setSchoolFileMasters] = useState([]); const [schoolFileTemplates, setSchoolFileTemplates] = useState([]); @@ -114,17 +117,19 @@ export default function FilesGroupsManagement({ setRemovePopupOnConfirm(() => () => { setIsLoading(true); // Supprimer les clones associés via l'API DocuSeal - const removeClonesPromises = schoolFileTemplates - .filter((template) => template.master === templateMaster.id) - .map((template) => removeTemplate(template.id)); - - // Ajouter la suppression du master à la liste des promesses - removeClonesPromises.push(removeTemplate(templateMaster.id)); + const removeClonesPromises = [ + ...schoolFileTemplates + .filter((template) => template.master === templateMaster.id) + .map((template) => + removeTemplate(template.id, selectedEstablishmentId, apiDocuseal) + ), + removeTemplate(templateMaster.id, selectedEstablishmentId, apiDocuseal), + ]; // Attendre que toutes les suppressions dans DocuSeal soient terminées Promise.all(removeClonesPromises) .then((responses) => { - const allSuccessful = responses.every((response) => response.ok); + const allSuccessful = responses.every((response) => response && response.id); if (allSuccessful) { logger.debug('Master et clones supprimés avec succès de DocuSeal.'); @@ -188,31 +193,6 @@ export default function FilesGroupsManagement({ }); }; - const removeTemplate = (templateId) => { - return fetch('/api/docuseal/removeTemplate/', { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrfToken, - }, - body: JSON.stringify({ - templateId, - }), - }) - .then((response) => { - if (!response.ok) { - return response.json().then((err) => { - throw new Error(err.message); - }); - } - return response; - }) - .catch((error) => { - logger.error('Error removing template:', error); - throw error; - }); - }; - const editTemplateMaster = (file) => { setIsEditing(true); setFileToEdit(file); @@ -562,13 +542,25 @@ export default function FilesGroupsManagement({ icon={Signature} title="Formulaires à remplir" description="Gérez les formulaires nécessitant une signature électronique." - button={true} + button={apiDocuseal} buttonOpeningModal={true} onClick={() => { setIsModalOpen(true); setIsEditing(false); }} /> +
+ + {!apiDocuseal && ( + + )} + {!apiDocuseal && 'Clé API Docuseal manquante'} + +
{ const storedUser = sessionStorage.getItem('user'); return storedUser ? JSON.parse(storedUser) : null; }); + const [apiDocuseal, setApiDocusealState] = useState(() => { + const storedApiDocuseal = sessionStorage.getItem('apiDocuseal'); + return storedApiDocuseal ? JSON.parse(storedApiDocuseal) : null; + }); // Sauvegarder dans sessionStorage à chaque mise à jour const setSelectedEstablishmentId = (id) => { @@ -86,6 +90,11 @@ export const EstablishmentProvider = ({ children }) => { sessionStorage.setItem('user', JSON.stringify(user)); }; + const setApiDocuseal = (api) => { + setApiDocusealState(api); + sessionStorage.setItem('apiDocuseal', JSON.stringify(api)); + }; + /** * Fonction d'initialisation du contexte avec la session (appelée lors du login) * @param {*} session @@ -104,6 +113,7 @@ export const EstablishmentProvider = ({ children }) => { name: role.establishment__name, evaluation_frequency: role.establishment__evaluation_frequency, total_capacity: role.establishment__total_capacity, + api_docuseal: role.establishment__api_docuseal, role_id: i, role_type: role.role_type, })); @@ -123,6 +133,9 @@ export const EstablishmentProvider = ({ children }) => { setSelectedEstablishmentTotalCapacity( userEstablishments[roleIndexDefault].total_capacity ); + setApiDocuseal( + userEstablishments[roleIndexDefault].api_docuseal + ); setProfileRole(userEstablishments[roleIndexDefault].role_type); } if (endInitFunctionHandler) { @@ -140,6 +153,9 @@ export const EstablishmentProvider = ({ children }) => { setProfileRoleState(null); setEstablishmentsState([]); setUserState(null); + setSelectedEstablishmentEvaluationFrequencyState(null); + setSelectedEstablishmentTotalCapacityState(null); + setApiDocusealState(null); sessionStorage.clear(); }; @@ -154,6 +170,8 @@ export const EstablishmentProvider = ({ children }) => { setSelectedEstablishmentEvaluationFrequency, selectedEstablishmentTotalCapacity, setSelectedEstablishmentTotalCapacity, + apiDocuseal, + setApiDocuseal, selectedRoleId, setSelectedRoleId, profileRole, diff --git a/Front-End/src/pages/api/docuseal/cloneTemplate.js b/Front-End/src/pages/api/docuseal/cloneTemplate.js index fa2dd54..8f9aeaa 100644 --- a/Front-End/src/pages/api/docuseal/cloneTemplate.js +++ b/Front-End/src/pages/api/docuseal/cloneTemplate.js @@ -3,18 +3,19 @@ import { BE_DOCUSEAL_CLONE_TEMPLATE } from '@/utils/Url'; export default function handler(req, res) { if (req.method === 'POST') { - const { templateId, email, is_required } = req.body; + const { templateId, email, is_required, establishment_id, apiDocuseal } = req.body; fetch(BE_DOCUSEAL_CLONE_TEMPLATE, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-Auth-Token': process.env.DOCUSEAL_API_KEY, + 'X-Auth-Token': apiDocuseal, }, body: JSON.stringify({ templateId, email, is_required, + establishment_id, }), }) .then((response) => { diff --git a/Front-End/src/pages/api/docuseal/downloadTemplate/[slug].js b/Front-End/src/pages/api/docuseal/downloadTemplate/[slug].js index 5d209cc..9ae309c 100644 --- a/Front-End/src/pages/api/docuseal/downloadTemplate/[slug].js +++ b/Front-End/src/pages/api/docuseal/downloadTemplate/[slug].js @@ -3,13 +3,13 @@ import { BE_DOCUSEAL_DOWNLOAD_TEMPLATE } from '@/utils/Url'; export default function handler(req, res) { if (req.method === 'GET') { - const { slug } = req.query; + const { slug, establishment_id, apiDocuseal } = req.query; logger.debug('slug : ', slug); - fetch(`${BE_DOCUSEAL_DOWNLOAD_TEMPLATE}/${slug}`, { + fetch(`${BE_DOCUSEAL_DOWNLOAD_TEMPLATE}/${slug}?establishment_id=${establishment_id}`, { method: 'GET', headers: { - 'X-Auth-Token': process.env.DOCUSEAL_API_KEY, + 'X-Auth-Token': apiDocuseal, }, }) .then((response) => { diff --git a/Front-End/src/pages/api/docuseal/generateToken.js b/Front-End/src/pages/api/docuseal/generateToken.js index 7f263d6..2069abe 100644 --- a/Front-End/src/pages/api/docuseal/generateToken.js +++ b/Front-End/src/pages/api/docuseal/generateToken.js @@ -3,13 +3,15 @@ import { BE_DOCUSEAL_GET_JWT } from '@/utils/Url'; export default function handler(req, res) { if (req.method === 'POST') { + const { apiDocuseal, ...rest } = req.body; + fetch(BE_DOCUSEAL_GET_JWT, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-Auth-Token': process.env.DOCUSEAL_API_KEY, + 'X-Auth-Token': apiDocuseal, }, - body: JSON.stringify(req.body), + body: JSON.stringify(rest), }) .then((response) => { logger.debug('Response status:', response.status); diff --git a/Front-End/src/pages/api/docuseal/removeTemplate.js b/Front-End/src/pages/api/docuseal/removeTemplate.js index 0725bce..b3a9467 100644 --- a/Front-End/src/pages/api/docuseal/removeTemplate.js +++ b/Front-End/src/pages/api/docuseal/removeTemplate.js @@ -3,12 +3,12 @@ import { BE_DOCUSEAL_REMOVE_TEMPLATE } from '@/utils/Url'; export default function handler(req, res) { if (req.method === 'DELETE') { - const { templateId } = req.body; + const { templateId, establishment_id, apiDocuseal } = req.body; - fetch(`${BE_DOCUSEAL_REMOVE_TEMPLATE}/${templateId}`, { + fetch(`${BE_DOCUSEAL_REMOVE_TEMPLATE}/${templateId}?establishment_id=${establishment_id}`, { method: 'DELETE', headers: { - 'X-Auth-Token': process.env.DOCUSEAL_API_KEY, + 'X-Auth-Token': apiDocuseal, }, }) .then((response) => { diff --git a/Front-End/src/utils/Url.js b/Front-End/src/utils/Url.js index cdac09e..5d5881a 100644 --- a/Front-End/src/utils/Url.js +++ b/Front-End/src/utils/Url.js @@ -136,6 +136,7 @@ export const FE_PARENTS_EDIT_SUBSCRIPTION_URL = '/parents/editSubscription'; export const FE_API_DOCUSEAL_GENERATE_TOKEN = '/api/docuseal/generateToken'; export const FE_API_DOCUSEAL_CLONE_URL = '/api/docuseal/cloneTemplate'; export const FE_API_DOCUSEAL_DOWNLOAD_URL = '/api/docuseal/downloadTemplate'; +export const FE_API_DOCUSEAL_DELETE_URL = '/api/docuseal/removeTemplate'; /** * Fonction pour obtenir l'URL de redirection en fonction du rôle diff --git a/docker-compose.yml b/docker-compose.yml index a8a3d99..42f1e08 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,15 +16,40 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_DB: school TZ: Europe/Paris + # docuseal_db: + # image: postgres:latest + # environment: + # POSTGRES_USER: postgres + # POSTGRES_PASSWORD: postgres + # DOCUSEAL_DB_HOST: docuseal_db + # POSTGRES_DB: docuseal + # ports: + # - 5433:5432 # port différent si besoin d'accès direct depuis l'hôte - docuseal: - image: docuseal/docuseal:latest - depends_on: - - database - ports: - - 3001:3000 - environment: - - DATABASE_URL=postgresql://postgres:postgres@database:5432/docuseal + # docuseal: + # image: docuseal/docuseal:latest + # container_name: docuseal_app + # depends_on: + # - docuseal_db + # ports: + # - "3001:3000" + # environment: + # DATABASE_URL: postgresql://postgres:postgres@docuseal_db:5432/docuseal + # volumes: + # - ./docuseal:/data/docuseal + + # caddy: + # image: caddy:2 + # container_name: caddy + # restart: unless-stopped + # ports: + # - "4000:4443" + # volumes: + # - ./Caddyfile:/etc/caddy/Caddyfile + # - caddy_data:/data + # - caddy_config:/config + # depends_on: + # - docuseal backend: build: @@ -44,25 +69,26 @@ services: depends_on: - redis - database - - docuseal + #- docuseal command: python start.py - init_docuseal_users: - build: - context: . - dockerfile: Dockerfile - depends_on: - - docuseal - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - USER_FIRST_NAME: n3wt - USER_LAST_NAME: school - USER_COMPANY: n3wt.innov - USER_EMAIL: n3wt.school@gmail.com - USER_PASSWORD: n3wt1234 - volumes: - - ./initDocusealUsers.sh:/docker-entrypoint-initdb.d/initDocusealUsers.sh + # init_docuseal_users: + # build: + # context: . + # dockerfile: Dockerfile + # depends_on: + # - docuseal + # environment: + # DOCUSEAL_DB_HOST: docuseal_db + # POSTGRES_USER: postgres + # POSTGRES_PASSWORD: postgres + # USER_FIRST_NAME: n3wt + # USER_LAST_NAME: school + # USER_COMPANY: n3wt.innov + # USER_EMAIL: n3wt.school@gmail.com + # USER_PASSWORD: n3wt1234 + # volumes: + # - ./initDocusealUsers.sh:/docker-entrypoint-initdb.d/initDocusealUsers.sh # frontend: # build: @@ -79,3 +105,6 @@ services: # - TZ=Europe/Paris # depends_on: # - backend +volumes: + caddy_data: + caddy_config: