feat: Utilisation d'une clef API Docuseal par établissement

This commit is contained in:
N3WT DE COMPET
2025-05-30 14:19:01 +02:00
parent 8cf22905e5
commit 23ab7d04ef
21 changed files with 256 additions and 134 deletions

View File

@ -223,7 +223,7 @@ def makeToken(user):
""" """
try: try:
# Récupérer tous les rôles de l'utilisateur actifs # 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 # Générer le JWT avec la bonne syntaxe datetime
access_payload = { access_payload = {

View File

@ -1,5 +1,4 @@
from django.conf import settings from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
@ -7,49 +6,67 @@ from rest_framework import status
import jwt import jwt
import datetime import datetime
import requests import requests
from Establishment.models import Establishment
@csrf_exempt @csrf_exempt
@api_view(['POST']) @api_view(['POST'])
def generate_jwt_token(request): 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') api_key = request.headers.get('X-Auth-Token')
if not api_key or api_key != settings.DOCUSEAL_JWT["API_KEY"]: if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
return Response({'error': 'Invalid API key'}, status=status.HTTP_401_UNAUTHORIZED) return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
# Récupérer les données de la requête # Récupérer les données de la requête
user_email = request.data.get('user_email') user_email = request.data.get('user_email')
documents_urls = request.data.get('documents_urls', []) 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: if not user_email:
return Response({'error': 'User email is required'}, status=status.HTTP_400_BAD_REQUEST) return Response({'error': 'User email is required'}, status=status.HTTP_400_BAD_REQUEST)
# Utiliser la configuration JWT de DocuSeal depuis les settings # Utiliser la clé API de l'établissement comme secret JWT
jwt_secret = settings.DOCUSEAL_JWT['API_KEY'] jwt_secret = establishment.api_docuseal
jwt_algorithm = settings.DOCUSEAL_JWT['ALGORITHM'] jwt_algorithm = settings.DOCUSEAL_JWT['ALGORITHM']
expiration_delta = settings.DOCUSEAL_JWT['EXPIRATION_DELTA'] expiration_delta = settings.DOCUSEAL_JWT['EXPIRATION_DELTA']
# Définir le payload
payload = { payload = {
'user_email': user_email, 'user_email': user_email,
'documents_urls': documents_urls, 'documents_urls': documents_urls,
'template_id': id, # Ajouter le id au payload 'template_id': template_id,
'exp': datetime.datetime.utcnow() + expiration_delta # Temps d'expiration du token 'exp': datetime.datetime.utcnow() + expiration_delta
} }
# Générer le token JWT
token = jwt.encode(payload, jwt_secret, algorithm=jwt_algorithm) token = jwt.encode(payload, jwt_secret, algorithm=jwt_algorithm)
return Response({'token': token}, status=status.HTTP_200_OK) return Response({'token': token}, status=status.HTTP_200_OK)
@csrf_exempt @csrf_exempt
@api_view(['POST']) @api_view(['POST'])
def clone_template(request): 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') api_key = request.headers.get('X-Auth-Token')
if not api_key or api_key != settings.DOCUSEAL_JWT["API_KEY"]: if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
return Response({'error': 'Invalid API key'}, status=status.HTTP_401_UNAUTHORIZED) return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
# Récupérer les données de la requête # Récupérer les données de la requête
document_id = request.data.get('templateId') document_id = request.data.get('templateId')
@ -57,7 +74,7 @@ def clone_template(request):
is_required = request.data.get('is_required') is_required = request.data.get('is_required')
# Vérifier les données requises # 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) return Response({'error': 'template ID 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 cloner le template
@ -67,7 +84,7 @@ def clone_template(request):
try: try:
response = requests.post(clone_url, headers={ response = requests.post(clone_url, headers={
'Content-Type': 'application/json', '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: 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 # URL de l'API de DocuSeal pour créer une submission
submission_url = f'https://docuseal.com/api/submissions' submission_url = f'https://docuseal.com/api/submissions'
# Faire la requête pour cloner le template
try: try:
clone_id = data['id'] 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', '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: if response.status_code != status.HTTP_200_OK:
@ -93,10 +113,10 @@ def clone_template(request):
data = response.json() data = response.json()
data[0]['id'] = clone_id data[0]['id'] = clone_id
return Response(data[0], status=status.HTTP_200_OK) return Response(data[0], status=status.HTTP_200_OK)
except requests.RequestException as e: except requests.RequestException as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 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') print(f'NOT REQUIRED -> on ne crée pas de submission')
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
@ -106,18 +126,28 @@ def clone_template(request):
@csrf_exempt @csrf_exempt
@api_view(['DELETE']) @api_view(['DELETE'])
def remove_template(request, id): def remove_template(request, id):
# Vérifier la clé API # Récupérer l'établissement concerné
api_key = request.headers.get('X-Auth-Token') establishment_id = request.GET.get('establishment_id')
if not api_key or api_key != settings.DOCUSEAL_JWT["API_KEY"]: if not establishment_id:
return Response({'error': 'Invalid API key'}, status=status.HTTP_401_UNAUTHORIZED) 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}' clone_url = f'https://docuseal.com/api/templates/{id}'
# Faire la requête pour cloner le template
try: try:
response = requests.delete(clone_url, headers={ 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: if response.status_code != status.HTTP_200_OK:
@ -132,23 +162,32 @@ def remove_template(request, id):
@csrf_exempt @csrf_exempt
@api_view(['GET']) @api_view(['GET'])
def download_template(request, slug): 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') api_key = request.headers.get('X-Auth-Token')
if not api_key or api_key != settings.DOCUSEAL_JWT["API_KEY"]: if not api_key or not establishment.api_docuseal or api_key != establishment.api_docuseal:
return Response({'error': 'Invalid API key'}, status=status.HTTP_401_UNAUTHORIZED) return Response({'error': 'Clé API invalide'}, status=status.HTTP_401_UNAUTHORIZED)
# Vérifier les données requises # Vérifier les données requises
if not slug : if not slug:
return Response({'error': 'slug is required'}, status=status.HTTP_400_BAD_REQUEST) 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' download_url = f'https://docuseal.com/submitters/{slug}/download'
# Faire la requête pour cloner le template
try: try:
response = requests.get(download_url, headers={ response = requests.get(download_url, headers={
'Content-Type': 'application/json', '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: if response.status_code != status.HTTP_200_OK:

View File

@ -21,6 +21,7 @@ class Establishment(models.Model):
licence_code = models.CharField(max_length=100, blank=True) licence_code = models.CharField(max_length=100, blank=True)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
api_docuseal = models.CharField(max_length=255, blank=True, null=True)
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -363,12 +363,10 @@ SIMPLE_JWT = {
} }
# Configuration for DocuSeal JWT # Configuration for DocuSeal JWT
DOCUSEAL_API_KEY="LRvUTQCbMSSpManYKshdQk9Do6rBQgjHyPrbGfxU3Jg"
DOCUSEAL_JWT = { DOCUSEAL_JWT = {
'ALGORITHM': 'HS256', 'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY, 'SIGNING_KEY': SECRET_KEY,
'EXPIRATION_DELTA': timedelta(hours=1), 'EXPIRATION_DELTA': timedelta(hours=1)
'API_KEY': DOCUSEAL_API_KEY
} }
# Django Channels Configuration # Django Channels Configuration

View File

@ -14,7 +14,7 @@ test_mode = os.getenv('TEST_MODE', 'False') == 'True'
commands = [ commands = [
["python", "manage.py", "collectstatic", "--noinput"], ["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", "Common", "--noinput"],
["python", "manage.py", "makemigrations", "Establishment", "--noinput"], ["python", "manage.py", "makemigrations", "Establishment", "--noinput"],
["python", "manage.py", "makemigrations", "Settings", "--noinput"], ["python", "manage.py", "makemigrations", "Settings", "--noinput"],

View File

@ -2,5 +2,4 @@ NEXT_PUBLIC_API_URL=_NEXT_PUBLIC_API_URL_
NEXT_PUBLIC_WSAPI_URL=_NEXT_PUBLIC_WSAPI_URL_ NEXT_PUBLIC_WSAPI_URL=_NEXT_PUBLIC_WSAPI_URL_
NEXT_PUBLIC_USE_FAKE_DATA=_NEXT_PUBLIC_USE_FAKE_DATA_ NEXT_PUBLIC_USE_FAKE_DATA=_NEXT_PUBLIC_USE_FAKE_DATA_
AUTH_SECRET=_AUTH_SECRET_ AUTH_SECRET=_AUTH_SECRET_
NEXTAUTH_URL=_NEXTAUTH_URL_ NEXTAUTH_URL=_NEXTAUTH_URL_
DOCUSEAL_API_KEY=_DOCUSEAL_API_KEY_

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslations } from 'next-intl'; 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 Loader from '@/components/Loader';
import StatCard from '@/components/StatCard'; import StatCard from '@/components/StatCard';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
@ -39,7 +39,7 @@ export default function DashboardPage() {
const [upcomingEvents, setUpcomingEvents] = useState([]); const [upcomingEvents, setUpcomingEvents] = useState([]);
const [absencesToday, setAbsencesToday] = useState([]); const [absencesToday, setAbsencesToday] = useState([]);
const { selectedEstablishmentId, selectedEstablishmentTotalCapacity } = const { selectedEstablishmentId, selectedEstablishmentTotalCapacity, apiDocuseal } =
useEstablishment(); useEstablishment();
const [statusDistribution, setStatusDistribution] = useState([ const [statusDistribution, setStatusDistribution] = useState([
@ -168,7 +168,24 @@ export default function DashboardPage() {
return ( return (
<div key={selectedEstablishmentId} className="p-6"> <div key={selectedEstablishmentId} className="p-6">
<h1 className="text-2xl font-bold mb-6">{t('dashboard')}</h1> <div className="flex items-center gap-3 mb-6">
<span
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
apiDocuseal
? 'bg-green-100 text-green-700 border border-green-300'
: 'bg-red-100 text-red-700 border border-red-300'
}`}
>
{apiDocuseal ? (
<CheckCircle2 className="w-4 h-4 mr-2 text-green-500" />
) : (
<AlertTriangle className="w-4 h-4 mr-2 text-red-500" />
)}
{apiDocuseal
? 'Clé API Docuseal renseignée'
: 'Clé API Docuseal manquante'}
</span>
</div>
{/* Statistiques principales */} {/* Statistiques principales */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">

View File

@ -52,7 +52,7 @@ export default function Page() {
); );
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment(); const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
useEffect(() => { useEffect(() => {
if (selectedEstablishmentId) { if (selectedEstablishmentId) {
@ -353,6 +353,7 @@ export default function Page() {
<FilesGroupsManagement <FilesGroupsManagement
csrfToken={csrfToken} csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId} selectedEstablishmentId={selectedEstablishmentId}
apiDocuseal={apiDocuseal}
/> />
</div> </div>
), ),

View File

@ -96,7 +96,7 @@ export default function CreateSubscriptionPage() {
const { getNiveauLabel } = useClasses(); const { getNiveauLabel } = useClasses();
const formDataRef = useRef(formData); const formDataRef = useRef(formData);
const { selectedEstablishmentId } = useEstablishment(); const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
const router = useRouter(); const router = useRouter();
@ -473,8 +473,6 @@ export default function CreateSubscriptionPage() {
} }
})(); })();
logger.debug('test : ', guardians);
const data = { const data = {
student: { student: {
last_name: formDataRef.current.studentLastName, last_name: formDataRef.current.studentLastName,
@ -532,12 +530,14 @@ export default function CreateSubscriptionPage() {
const clonePromises = masters.map((templateMaster) => const clonePromises = masters.map((templateMaster) =>
cloneTemplate( cloneTemplate(
templateMaster.id, templateMaster.id,
formData.guardianEmail, formDataRef.current.guardianEmail,
templateMaster.is_required templateMaster.is_required,
selectedEstablishmentId,
apiDocuseal
) )
.then((clonedDocument) => { .then((clonedDocument) => {
const cloneData = { const cloneData = {
name: `${templateMaster.name}_${formData.studentFirstName}_${formData.studentLastName}`, name: `${templateMaster.name}_${formDataRef.current.studentFirstName}_${formDataRef.current.studentLastName}`,
slug: clonedDocument.slug, slug: clonedDocument.slug,
id: clonedDocument.id, id: clonedDocument.id,
master: templateMaster.id, master: templateMaster.id,
@ -655,6 +655,7 @@ export default function CreateSubscriptionPage() {
return { return {
...prevData, ...prevData,
selectedGuardians: updatedSelectedGuardians, selectedGuardians: updatedSelectedGuardians,
guardianEmail: guardian.associated_profile_email,
}; };
}); });
}; };

View File

@ -17,7 +17,7 @@ export default function Page() {
const [formErrors, setFormErrors] = useState({}); const [formErrors, setFormErrors] = useState({});
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment(); const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handleSubmit = (data) => { const handleSubmit = (data) => {
@ -47,6 +47,7 @@ export default function Page() {
studentId={studentId} studentId={studentId}
csrfToken={csrfToken} csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId} selectedEstablishmentId={selectedEstablishmentId}
apiDocuseal = {apiDocuseal}
onSubmit={handleSubmit} onSubmit={handleSubmit}
cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL} cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL}
errors={formErrors} errors={formErrors}

View File

@ -14,7 +14,7 @@ export default function Page() {
const enable = searchParams.get('enabled') === 'true'; const enable = searchParams.get('enabled') === 'true';
const router = useRouter(); const router = useRouter();
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment(); const { selectedEstablishmentId, apiDocuseal } = useEstablishment();
const handleSubmit = async (data) => { const handleSubmit = async (data) => {
try { try {
@ -31,6 +31,7 @@ export default function Page() {
studentId={studentId} studentId={studentId}
csrfToken={csrfToken} csrfToken={csrfToken}
selectedEstablishmentId={selectedEstablishmentId} selectedEstablishmentId={selectedEstablishmentId}
apiDocuseal = {apiDocuseal}
onSubmit={handleSubmit} onSubmit={handleSubmit}
cancelUrl={FE_PARENTS_HOME_URL} cancelUrl={FE_PARENTS_HOME_URL}
enable={enable} enable={enable}

View File

@ -7,6 +7,7 @@ import {
FE_API_DOCUSEAL_CLONE_URL, FE_API_DOCUSEAL_CLONE_URL,
FE_API_DOCUSEAL_DOWNLOAD_URL, FE_API_DOCUSEAL_DOWNLOAD_URL,
FE_API_DOCUSEAL_GENERATE_TOKEN, FE_API_DOCUSEAL_GENERATE_TOKEN,
FE_API_DOCUSEAL_DELETE_URL
} from '@/utils/Url'; } from '@/utils/Url';
import { errorHandler, requestResponseHandler } from './actionsHandlers'; import { errorHandler, requestResponseHandler } from './actionsHandlers';
@ -337,8 +338,23 @@ export const deleteRegistrationParentFileTemplate = (id, csrfToken) => {
}; };
// API requests // 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}`, { return fetch(`${FE_API_DOCUSEAL_CLONE_URL}`, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -348,14 +364,17 @@ export const cloneTemplate = (templateId, email, is_required) => {
templateId, templateId,
email, email,
is_required, is_required,
establishment_id :selectedEstablishmentId,
apiDocuseal
}), }),
}) })
.then(requestResponseHandler) .then(requestResponseHandler)
.catch(errorHandler); .catch(errorHandler);
}; };
export const downloadTemplate = (slug) => { export const downloadTemplate = (slug, selectedEstablishmentId, apiDocuseal) => {
return fetch(`${FE_API_DOCUSEAL_DOWNLOAD_URL}/${slug}`, { const url = `${FE_API_DOCUSEAL_DOWNLOAD_URL}/${slug}?establishment_id=${selectedEstablishmentId}&apiDocuseal=${apiDocuseal}`;
return fetch(url, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -365,13 +384,13 @@ export const downloadTemplate = (slug) => {
.catch(errorHandler); .catch(errorHandler);
}; };
export const generateToken = (email, id = null) => { export const generateToken = (email, id = null, selectedEstablishmentId, apiDocuseal) => {
return fetch(`${FE_API_DOCUSEAL_GENERATE_TOKEN}`, { return fetch(`${FE_API_DOCUSEAL_GENERATE_TOKEN}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ user_email: email, id }), body: JSON.stringify({ user_email: email, id, establishment_id :selectedEstablishmentId, apiDocuseal }),
}) })
.then(requestResponseHandler) .then(requestResponseHandler)
.catch(errorHandler); .catch(errorHandler);

View File

@ -30,7 +30,7 @@ export default function FileUploadDocuSeal({
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment(); const { selectedEstablishmentId, user, apiDocuseal } = useEstablishment();
useEffect(() => { useEffect(() => {
fetchRegistrationFileGroups(selectedEstablishmentId).then((data) => fetchRegistrationFileGroups(selectedEstablishmentId).then((data) =>
@ -44,10 +44,12 @@ export default function FileUploadDocuSeal({
}, [fileToEdit]); }, [fileToEdit]);
useEffect(() => { useEffect(() => {
const email = 'n3wt.school@gmail.com'; if (!user && !user?.email) {
return;
}
const id = fileToEdit ? fileToEdit.id : null; const id = fileToEdit ? fileToEdit.id : null;
generateToken(email, id) generateToken(user?.email, id, selectedEstablishmentId, apiDocuseal)
.then((data) => { .then((data) => {
setToken(data.token); setToken(data.token);
}) })
@ -119,7 +121,7 @@ export default function FileUploadDocuSeal({
guardianDetails.forEach((guardian, index) => { guardianDetails.forEach((guardian, index) => {
logger.debug('creation du clone avec required : ', is_required); 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) => { .then((clonedDocument) => {
// Sauvegarde des schoolFileTemplates clonés dans la base de données // Sauvegarde des schoolFileTemplates clonés dans la base de données
const data = { const data = {

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 Modal from '@/components/Modal';
import Table from '@/components/Table'; import Table from '@/components/Table';
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal'; import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
@ -22,6 +22,8 @@ import {
deleteRegistrationFileGroup, deleteRegistrationFileGroup,
deleteRegistrationSchoolFileMaster, deleteRegistrationSchoolFileMaster,
deleteRegistrationParentFileMaster, deleteRegistrationParentFileMaster,
removeTemplate
} from '@/app/actions/registerFileGroupAction'; } from '@/app/actions/registerFileGroupAction';
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm'; import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
@ -35,6 +37,7 @@ import AlertMessage from '@/components/AlertMessage';
export default function FilesGroupsManagement({ export default function FilesGroupsManagement({
csrfToken, csrfToken,
selectedEstablishmentId, selectedEstablishmentId,
apiDocuseal
}) { }) {
const [schoolFileMasters, setSchoolFileMasters] = useState([]); const [schoolFileMasters, setSchoolFileMasters] = useState([]);
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]); const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
@ -114,17 +117,19 @@ export default function FilesGroupsManagement({
setRemovePopupOnConfirm(() => () => { setRemovePopupOnConfirm(() => () => {
setIsLoading(true); setIsLoading(true);
// Supprimer les clones associés via l'API DocuSeal // Supprimer les clones associés via l'API DocuSeal
const removeClonesPromises = schoolFileTemplates const removeClonesPromises = [
.filter((template) => template.master === templateMaster.id) ...schoolFileTemplates
.map((template) => removeTemplate(template.id)); .filter((template) => template.master === templateMaster.id)
.map((template) =>
// Ajouter la suppression du master à la liste des promesses removeTemplate(template.id, selectedEstablishmentId, apiDocuseal)
removeClonesPromises.push(removeTemplate(templateMaster.id)); ),
removeTemplate(templateMaster.id, selectedEstablishmentId, apiDocuseal),
];
// Attendre que toutes les suppressions dans DocuSeal soient terminées // Attendre que toutes les suppressions dans DocuSeal soient terminées
Promise.all(removeClonesPromises) Promise.all(removeClonesPromises)
.then((responses) => { .then((responses) => {
const allSuccessful = responses.every((response) => response.ok); const allSuccessful = responses.every((response) => response && response.id);
if (allSuccessful) { if (allSuccessful) {
logger.debug('Master et clones supprimés avec succès de DocuSeal.'); 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) => { const editTemplateMaster = (file) => {
setIsEditing(true); setIsEditing(true);
setFileToEdit(file); setFileToEdit(file);
@ -562,13 +542,25 @@ export default function FilesGroupsManagement({
icon={Signature} icon={Signature}
title="Formulaires à remplir" title="Formulaires à remplir"
description="Gérez les formulaires nécessitant une signature électronique." description="Gérez les formulaires nécessitant une signature électronique."
button={true} button={apiDocuseal}
buttonOpeningModal={true} buttonOpeningModal={true}
onClick={() => { onClick={() => {
setIsModalOpen(true); setIsModalOpen(true);
setIsEditing(false); setIsEditing(false);
}} }}
/> />
<div className="mb-4">
<span
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
!apiDocuseal && 'bg-red-100 text-red-700 border border-red-300'
}`}
>
{!apiDocuseal && (
<AlertTriangle className="w-4 h-4 mr-2 text-red-500" />
)}
{!apiDocuseal && 'Clé API Docuseal manquante'}
</span>
</div>
<Table <Table
data={filteredFiles} data={filteredFiles}
columns={columnsFiles} columns={columnsFiles}

View File

@ -46,6 +46,10 @@ export const EstablishmentProvider = ({ children }) => {
const storedUser = sessionStorage.getItem('user'); const storedUser = sessionStorage.getItem('user');
return storedUser ? JSON.parse(storedUser) : null; 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 // Sauvegarder dans sessionStorage à chaque mise à jour
const setSelectedEstablishmentId = (id) => { const setSelectedEstablishmentId = (id) => {
@ -86,6 +90,11 @@ export const EstablishmentProvider = ({ children }) => {
sessionStorage.setItem('user', JSON.stringify(user)); 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) * Fonction d'initialisation du contexte avec la session (appelée lors du login)
* @param {*} session * @param {*} session
@ -104,6 +113,7 @@ export const EstablishmentProvider = ({ children }) => {
name: role.establishment__name, name: role.establishment__name,
evaluation_frequency: role.establishment__evaluation_frequency, evaluation_frequency: role.establishment__evaluation_frequency,
total_capacity: role.establishment__total_capacity, total_capacity: role.establishment__total_capacity,
api_docuseal: role.establishment__api_docuseal,
role_id: i, role_id: i,
role_type: role.role_type, role_type: role.role_type,
})); }));
@ -123,6 +133,9 @@ export const EstablishmentProvider = ({ children }) => {
setSelectedEstablishmentTotalCapacity( setSelectedEstablishmentTotalCapacity(
userEstablishments[roleIndexDefault].total_capacity userEstablishments[roleIndexDefault].total_capacity
); );
setApiDocuseal(
userEstablishments[roleIndexDefault].api_docuseal
);
setProfileRole(userEstablishments[roleIndexDefault].role_type); setProfileRole(userEstablishments[roleIndexDefault].role_type);
} }
if (endInitFunctionHandler) { if (endInitFunctionHandler) {
@ -140,6 +153,9 @@ export const EstablishmentProvider = ({ children }) => {
setProfileRoleState(null); setProfileRoleState(null);
setEstablishmentsState([]); setEstablishmentsState([]);
setUserState(null); setUserState(null);
setSelectedEstablishmentEvaluationFrequencyState(null);
setSelectedEstablishmentTotalCapacityState(null);
setApiDocusealState(null);
sessionStorage.clear(); sessionStorage.clear();
}; };
@ -154,6 +170,8 @@ export const EstablishmentProvider = ({ children }) => {
setSelectedEstablishmentEvaluationFrequency, setSelectedEstablishmentEvaluationFrequency,
selectedEstablishmentTotalCapacity, selectedEstablishmentTotalCapacity,
setSelectedEstablishmentTotalCapacity, setSelectedEstablishmentTotalCapacity,
apiDocuseal,
setApiDocuseal,
selectedRoleId, selectedRoleId,
setSelectedRoleId, setSelectedRoleId,
profileRole, profileRole,

View File

@ -3,18 +3,19 @@ import { BE_DOCUSEAL_CLONE_TEMPLATE } from '@/utils/Url';
export default function handler(req, res) { export default function handler(req, res) {
if (req.method === 'POST') { 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, { fetch(BE_DOCUSEAL_CLONE_TEMPLATE, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Auth-Token': process.env.DOCUSEAL_API_KEY, 'X-Auth-Token': apiDocuseal,
}, },
body: JSON.stringify({ body: JSON.stringify({
templateId, templateId,
email, email,
is_required, is_required,
establishment_id,
}), }),
}) })
.then((response) => { .then((response) => {

View File

@ -3,13 +3,13 @@ import { BE_DOCUSEAL_DOWNLOAD_TEMPLATE } from '@/utils/Url';
export default function handler(req, res) { export default function handler(req, res) {
if (req.method === 'GET') { if (req.method === 'GET') {
const { slug } = req.query; const { slug, establishment_id, apiDocuseal } = req.query;
logger.debug('slug : ', slug); logger.debug('slug : ', slug);
fetch(`${BE_DOCUSEAL_DOWNLOAD_TEMPLATE}/${slug}`, { fetch(`${BE_DOCUSEAL_DOWNLOAD_TEMPLATE}/${slug}?establishment_id=${establishment_id}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'X-Auth-Token': process.env.DOCUSEAL_API_KEY, 'X-Auth-Token': apiDocuseal,
}, },
}) })
.then((response) => { .then((response) => {

View File

@ -3,13 +3,15 @@ import { BE_DOCUSEAL_GET_JWT } from '@/utils/Url';
export default function handler(req, res) { export default function handler(req, res) {
if (req.method === 'POST') { if (req.method === 'POST') {
const { apiDocuseal, ...rest } = req.body;
fetch(BE_DOCUSEAL_GET_JWT, { fetch(BE_DOCUSEAL_GET_JWT, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', '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) => { .then((response) => {
logger.debug('Response status:', response.status); logger.debug('Response status:', response.status);

View File

@ -3,12 +3,12 @@ import { BE_DOCUSEAL_REMOVE_TEMPLATE } from '@/utils/Url';
export default function handler(req, res) { export default function handler(req, res) {
if (req.method === 'DELETE') { 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', method: 'DELETE',
headers: { headers: {
'X-Auth-Token': process.env.DOCUSEAL_API_KEY, 'X-Auth-Token': apiDocuseal,
}, },
}) })
.then((response) => { .then((response) => {

View File

@ -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_GENERATE_TOKEN = '/api/docuseal/generateToken';
export const FE_API_DOCUSEAL_CLONE_URL = '/api/docuseal/cloneTemplate'; 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_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 * Fonction pour obtenir l'URL de redirection en fonction du rôle

View File

@ -16,15 +16,40 @@ services:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_DB: school POSTGRES_DB: school
TZ: Europe/Paris 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: # docuseal:
image: docuseal/docuseal:latest # image: docuseal/docuseal:latest
depends_on: # container_name: docuseal_app
- database # depends_on:
ports: # - docuseal_db
- 3001:3000 # ports:
environment: # - "3001:3000"
- DATABASE_URL=postgresql://postgres:postgres@database:5432/docuseal # 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: backend:
build: build:
@ -44,25 +69,26 @@ services:
depends_on: depends_on:
- redis - redis
- database - database
- docuseal #- docuseal
command: python start.py command: python start.py
init_docuseal_users: # init_docuseal_users:
build: # build:
context: . # context: .
dockerfile: Dockerfile # dockerfile: Dockerfile
depends_on: # depends_on:
- docuseal # - docuseal
environment: # environment:
POSTGRES_USER: postgres # DOCUSEAL_DB_HOST: docuseal_db
POSTGRES_PASSWORD: postgres # POSTGRES_USER: postgres
USER_FIRST_NAME: n3wt # POSTGRES_PASSWORD: postgres
USER_LAST_NAME: school # USER_FIRST_NAME: n3wt
USER_COMPANY: n3wt.innov # USER_LAST_NAME: school
USER_EMAIL: n3wt.school@gmail.com # USER_COMPANY: n3wt.innov
USER_PASSWORD: n3wt1234 # USER_EMAIL: n3wt.school@gmail.com
volumes: # USER_PASSWORD: n3wt1234
- ./initDocusealUsers.sh:/docker-entrypoint-initdb.d/initDocusealUsers.sh # volumes:
# - ./initDocusealUsers.sh:/docker-entrypoint-initdb.d/initDocusealUsers.sh
# frontend: # frontend:
# build: # build:
@ -79,3 +105,6 @@ services:
# - TZ=Europe/Paris # - TZ=Europe/Paris
# depends_on: # depends_on:
# - backend # - backend
volumes:
caddy_data:
caddy_config: