feat: Securisation du Backend

This commit is contained in:
Luc SORIGNET
2026-02-27 10:45:36 +01:00
parent 2fef6d61a4
commit fa843097ba
55 changed files with 2898 additions and 910 deletions

View File

@ -1,4 +0,0 @@
{
"presets": ["next/babel"],
"plugins": []
}

View File

@ -31,6 +31,7 @@ import { fetchRegistrationSchoolFileMasters } from '@/app/actions/registerFileGr
import logger from '@/utils/logger';
import { useEstablishment } from '@/context/EstablishmentContext';
import { PlanningProvider, PlanningModes } from '@/context/PlanningContext';
import { updatePlanning } from '@/app/actions/planningAction';
import CompetenciesList from '@/components/Structure/Competencies/CompetenciesList';
export default function Page() {
@ -259,20 +260,10 @@ export default function Page() {
});
};
const handleUpdatePlanning = (url, planningId, updatedData) => {
fetch(`${url}/${planningId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(updatedData),
credentials: 'include',
})
.then((response) => response.json())
const handleUpdatePlanning = (planningId, updatedData) => {
updatePlanning(planningId, updatedData, csrfToken)
.then((data) => {
logger.debug('Planning mis à jour avec succès :', data);
//setDatas(data);
})
.catch((error) => {
logger.error('Erreur :', error);

View File

@ -1,4 +1,15 @@
import logger from '@/utils/logger';
import { signOut } from 'next-auth/react';
let isSigningOut = false;
export const triggerSignOut = async () => {
if (isSigningOut || typeof window === 'undefined') return;
isSigningOut = true;
logger.warn('Session expirée, déconnexion en cours...');
await signOut({ callbackUrl: '/users/login' });
};
/**
*
* @param {*} response
@ -6,6 +17,18 @@ import logger from '@/utils/logger';
*/
export const requestResponseHandler = async (response) => {
try {
if (response.status === 401) {
// On lève une erreur plutôt que de déclencher un signOut automatique.
// Plusieurs requêtes concurrent pourraient déclencher des signOut en cascade.
// Le signOut est géré proprement via RefreshTokenError dans getAuthToken.
const body = await response.json().catch(() => ({}));
const error = new Error(
body?.detail || body?.errorMessage || 'Session expirée'
);
error.status = 401;
throw error;
}
const body = await response?.json();
if (response.ok) {
return body;

View File

@ -1,5 +1,6 @@
import { signOut, signIn } from 'next-auth/react';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
import { fetchWithAuth } from '@/utils/fetchWithAuth';
import {
BE_AUTH_LOGIN_URL,
BE_AUTH_REFRESH_JWT_URL,
@ -73,92 +74,49 @@ export const fetchProfileRoles = (
if (page !== '' && pageSize !== '') {
url = `${BE_AUTH_PROFILES_ROLES_URL}?filter=${filter}&establishment_id=${establishment}&page=${page}`;
}
return fetch(url, {
headers: {
'Content-Type': 'application/json',
},
})
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(url);
};
export const updateProfileRoles = (id, data, csrfToken) => {
const request = new Request(`${BE_AUTH_PROFILES_ROLES_URL}/${id}`, {
return fetchWithAuth(`${BE_AUTH_PROFILES_ROLES_URL}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const deleteProfileRoles = async (id, csrfToken) => {
const response = await fetch(`${BE_AUTH_PROFILES_ROLES_URL}/${id}`, {
export const deleteProfileRoles = (id, csrfToken) => {
return fetchWithAuth(`${BE_AUTH_PROFILES_ROLES_URL}/${id}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
});
if (!response.ok) {
// Extraire le message d'erreur du backend
const errorData = await response.json();
const errorMessage =
errorData?.error ||
'Une erreur est survenue lors de la suppression du profil.';
// Jeter une erreur avec le message spécifique
throw new Error(errorMessage);
}
return response.json();
};
export const fetchProfiles = () => {
return fetch(`${BE_AUTH_PROFILES_URL}`)
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(`${BE_AUTH_PROFILES_URL}`);
};
export const createProfile = (data, csrfToken) => {
const request = new Request(`${BE_AUTH_PROFILES_URL}`, {
return fetchWithAuth(`${BE_AUTH_PROFILES_URL}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const deleteProfile = (id, csrfToken) => {
const request = new Request(`${BE_AUTH_PROFILES_URL}/${id}`, {
return fetchWithAuth(`${BE_AUTH_PROFILES_URL}/${id}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
});
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const updateProfile = (id, data, csrfToken) => {
const request = new Request(`${BE_AUTH_PROFILES_URL}/${id}`, {
return fetchWithAuth(`${BE_AUTH_PROFILES_URL}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(data),
});
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const sendNewPassword = (data, csrfToken) => {

View File

@ -2,33 +2,20 @@ import {
BE_GESTIONEMAIL_SEARCH_RECIPIENTS_URL,
BE_GESTIONEMAIL_SEND_EMAIL_URL,
} from '@/utils/Url';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
import { fetchWithAuth } from '@/utils/fetchWithAuth';
import { getCsrfToken } from '@/utils/getCsrfToken';
// Recherche de destinataires pour email
export const searchRecipients = (establishmentId, query) => {
const url = `${BE_GESTIONEMAIL_SEARCH_RECIPIENTS_URL}/?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(url);
};
// Envoyer un email
export const sendEmail = async (messageData) => {
const csrfToken = getCsrfToken();
return fetch(BE_GESTIONEMAIL_SEND_EMAIL_URL, {
return fetchWithAuth(BE_GESTIONEMAIL_SEND_EMAIL_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(messageData),
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};

View File

@ -7,23 +7,9 @@ import {
BE_GESTIONMESSAGERIE_UPLOAD_FILE_URL,
BE_GESTIONMESSAGERIE_DELETE_CONVERSATION_URL,
} from '@/utils/Url';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
import { fetchWithAuth, getAuthToken } from '@/utils/fetchWithAuth';
import logger from '@/utils/logger';
// Helper pour construire les en-têtes avec CSRF
const buildHeaders = (csrfToken) => {
const headers = {
'Content-Type': 'application/json',
};
// Ajouter le token CSRF
if (csrfToken) {
headers['X-CSRFToken'] = csrfToken;
}
return headers;
};
/**
* Récupère les conversations d'un utilisateur
*/
@ -31,15 +17,12 @@ export const fetchConversations = async (userId, csrfToken) => {
try {
// Utiliser la nouvelle route avec user_id en paramètre d'URL
const url = `${BE_GESTIONMESSAGERIE_CONVERSATIONS_URL}/user/${userId}/`;
const response = await fetch(url, {
method: 'GET',
headers: buildHeaders(csrfToken),
credentials: 'include',
return await fetchWithAuth(url, {
headers: { 'X-CSRFToken': csrfToken },
});
return await requestResponseHandler(response);
} catch (error) {
logger.error('Erreur lors de la récupération des conversations:', error);
return errorHandler(error);
throw error;
}
};
@ -62,15 +45,12 @@ export const fetchMessages = async (
url += `&user_id=${userId}`;
}
const response = await fetch(url, {
method: 'GET',
headers: buildHeaders(csrfToken),
credentials: 'include',
return await fetchWithAuth(url, {
headers: { 'X-CSRFToken': csrfToken },
});
return await requestResponseHandler(response);
} catch (error) {
logger.error('Erreur lors de la récupération des messages:', error);
return errorHandler(error);
throw error;
}
};
@ -79,16 +59,14 @@ export const fetchMessages = async (
*/
export const sendMessage = async (messageData, csrfToken) => {
try {
const response = await fetch(BE_GESTIONMESSAGERIE_SEND_MESSAGE_URL, {
return await fetchWithAuth(BE_GESTIONMESSAGERIE_SEND_MESSAGE_URL, {
method: 'POST',
headers: buildHeaders(csrfToken),
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(messageData),
});
return await requestResponseHandler(response);
} catch (error) {
logger.error("Erreur lors de l'envoi du message:", error);
return errorHandler(error);
throw error;
}
};
@ -103,17 +81,14 @@ export const createConversation = async (participantIds, csrfToken) => {
name: '', // Le nom sera généré côté backend
};
const response = await fetch(BE_GESTIONMESSAGERIE_CREATE_CONVERSATION_URL, {
return await fetchWithAuth(BE_GESTIONMESSAGERIE_CREATE_CONVERSATION_URL, {
method: 'POST',
headers: buildHeaders(csrfToken),
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(requestBody),
});
return await requestResponseHandler(response);
} catch (error) {
logger.error('Erreur lors de la création de la conversation:', error);
return errorHandler(error);
throw error;
}
};
@ -132,16 +107,12 @@ export const searchMessagerieRecipients = async (
const url = `${baseUrl}?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
const response = await fetch(url, {
method: 'GET',
headers: buildHeaders(csrfToken),
credentials: 'include',
return await fetchWithAuth(url, {
headers: { 'X-CSRFToken': csrfToken },
});
return await requestResponseHandler(response);
} catch (error) {
logger.error('Erreur lors de la recherche des destinataires:', error);
return errorHandler(error);
throw error;
}
};
@ -150,19 +121,17 @@ export const searchMessagerieRecipients = async (
*/
export const markAsRead = async (conversationId, userId, csrfToken) => {
try {
const response = await fetch(BE_GESTIONMESSAGERIE_MARK_AS_READ_URL, {
return await fetchWithAuth(BE_GESTIONMESSAGERIE_MARK_AS_READ_URL, {
method: 'POST',
headers: buildHeaders(csrfToken),
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify({
conversation_id: conversationId,
user_id: userId,
}),
});
return await requestResponseHandler(response);
} catch (error) {
logger.error('Erreur lors du marquage des messages comme lus:', error);
return errorHandler(error);
throw error;
}
};
@ -181,6 +150,7 @@ export const uploadFile = async (
formData.append('conversation_id', conversationId);
formData.append('sender_id', senderId);
const token = await getAuthToken();
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
@ -223,7 +193,10 @@ export const uploadFile = async (
xhr.withCredentials = true;
xhr.timeout = 30000;
// Ajouter le header CSRF pour XMLHttpRequest
// Ajouter les headers d'authentification pour XMLHttpRequest
if (token) {
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
}
if (csrfToken) {
xhr.setRequestHeader('X-CSRFToken', csrfToken);
}
@ -238,14 +211,12 @@ export const uploadFile = async (
export const deleteConversation = async (conversationId, csrfToken) => {
try {
const url = `${BE_GESTIONMESSAGERIE_DELETE_CONVERSATION_URL}/${conversationId}/`;
const response = await fetch(url, {
return await fetchWithAuth(url, {
method: 'DELETE',
headers: buildHeaders(csrfToken),
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
});
return await requestResponseHandler(response);
} catch (error) {
logger.error('Erreur lors de la suppression de la conversation:', error);
return errorHandler(error);
throw error;
}
};

View File

@ -1,49 +1,31 @@
import { BE_PLANNING_PLANNINGS_URL, BE_PLANNING_EVENTS_URL } from '@/utils/Url';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
import { fetchWithAuth } from '@/utils/fetchWithAuth';
const getData = (url) => {
return fetch(`${url}`).then(requestResponseHandler).catch(errorHandler);
return fetchWithAuth(url);
};
const createDatas = (url, newData, csrfToken) => {
return fetch(url, {
return fetchWithAuth(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(newData),
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
const updateDatas = (url, updatedData, csrfToken) => {
return fetch(`${url}`, {
return fetchWithAuth(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(updatedData),
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
const removeDatas = (url, csrfToken) => {
return fetch(`${url}`, {
return fetchWithAuth(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
headers: { 'X-CSRFToken': csrfToken },
});
};
export const fetchPlannings = (

View File

@ -5,213 +5,113 @@ import {
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL,
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL
} from '@/utils/Url';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
import { fetchWithAuth, fetchWithAuthRaw } from '@/utils/fetchWithAuth';
// FETCH requests
export async function fetchRegistrationFileGroups(establishment) {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}?establishment_id=${establishment}`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
return fetchWithAuth(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}?establishment_id=${establishment}`
);
if (!response.ok) {
throw new Error('Failed to fetch file groups');
}
return response.json();
}
export const fetchRegistrationFileFromGroup = async (groupId) => {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}/school_file_templates`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
export const fetchRegistrationFileFromGroup = (groupId) => {
return fetchWithAuth(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}/school_file_templates`
);
if (!response.ok) {
throw new Error(
'Erreur lors de la récupération des fichiers associés au groupe'
);
}
return response.json();
};
export const fetchRegistrationSchoolFileMasters = (establishment) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}?establishment_id=${establishment}`;
const request = new Request(`${url}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler).catch(errorHandler);
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}?establishment_id=${establishment}`;
return fetchWithAuth(url);
};
export const fetchRegistrationParentFileMasters = (establishment) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}?establishment_id=${establishment}`;
const request = new Request(`${url}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler).catch(errorHandler);
const url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}?establishment_id=${establishment}`;
return fetchWithAuth(url);
};
export const fetchRegistrationSchoolFileTemplates = (establishment) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}?establishment_id=${establishment}`;
const request = new Request(`${url}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler).catch(errorHandler);
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}?establishment_id=${establishment}`;
return fetchWithAuth(url);
};
// CREATE requests
export async function createRegistrationFileGroup(groupData, csrfToken) {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(groupData),
credentials: 'include',
}
);
if (!response.ok) {
throw new Error('Failed to create file group');
}
return response.json();
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(groupData),
});
}
export const createRegistrationSchoolFileMaster = (data, csrfToken) => {
// Toujours FormData, jamais JSON
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`, {
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken },
body: data,
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
export const createRegistrationParentFileMaster = (data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}`, {
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
export const createRegistrationSchoolFileTemplate = (data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}`, {
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
export const createRegistrationParentFileTemplate = (data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}`, {
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
// EDIT requests
export const editRegistrationFileGroup = async (
groupId,
groupData,
csrfToken
) => {
const response = await fetch(
export const editRegistrationFileGroup = (groupId, groupData, csrfToken) => {
return fetchWithAuth(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(groupData),
}
);
if (!response.ok) {
throw new Error('Erreur lors de la modification du groupe');
}
return response.json();
};
export const editRegistrationSchoolFileMaster = (fileId, data, csrfToken) => {
return fetch(
return fetchWithAuth(
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
{
method: 'PUT',
headers: { 'X-CSRFToken': csrfToken },
body: data,
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
export const editRegistrationParentFileMaster = (id, data, csrfToken) => {
return fetch(
return fetchWithAuth(
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`,
{
method: 'PUT',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
}
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
export const editRegistrationSchoolFileTemplates = (
@ -219,19 +119,14 @@ export const editRegistrationSchoolFileTemplates = (
data,
csrfToken
) => {
return fetch(
return fetchWithAuth(
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${fileId}`,
{
method: 'PUT',
headers: { 'X-CSRFToken': csrfToken },
body: data,
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
export const editRegistrationParentFileTemplates = (
@ -239,86 +134,64 @@ export const editRegistrationParentFileTemplates = (
data,
csrfToken
) => {
return fetch(
return fetchWithAuth(
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${fileId}`,
{
method: 'PUT',
headers: { 'X-CSRFToken': csrfToken },
body: data,
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
}
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
// DELETE requests
export async function deleteRegistrationFileGroup(groupId, csrfToken) {
const response = await fetch(
return fetchWithAuthRaw(
`${BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL}/${groupId}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
}
);
return response;
}
export const deleteRegistrationSchoolFileMaster = (fileId, csrfToken) => {
return fetch(
return fetchWithAuthRaw(
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
}
);
};
export const deleteRegistrationParentFileMaster = (id, csrfToken) => {
return fetch(
return fetchWithAuthRaw(
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL}/${id}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
}
);
};
export const deleteRegistrationSchoolFileTemplates = (fileId, csrfToken) => {
return fetch(
return fetchWithAuthRaw(
`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${fileId}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
}
);
};
export const deleteRegistrationParentFileTemplate = (id, csrfToken) => {
return fetch(
return fetchWithAuthRaw(
`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${id}`,
{
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
}
);
};

View File

@ -10,185 +10,125 @@ import {
BE_SCHOOL_ESTABLISHMENT_URL,
BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL,
} from '@/utils/Url';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
import { fetchWithAuth } from '@/utils/fetchWithAuth';
export const deleteEstablishmentCompetencies = (ids, csrfToken) => {
return fetch(BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL, {
return fetchWithAuth(BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify({ ids }),
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
export const createEstablishmentCompetencies = (newData, csrfToken) => {
return fetch(BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL, {
return fetchWithAuth(BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(newData),
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
export const fetchEstablishmentCompetencies = (establishment, cycle = 1) => {
return fetch(
return fetchWithAuth(
`${BE_SCHOOL_ESTABLISHMENT_COMPETENCIES_URL}?establishment_id=${establishment}&cycle=${cycle}`
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchSpecialities = (establishment) => {
return fetch(
`${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchTeachers = (establishment) => {
return fetch(`${BE_SCHOOL_TEACHERS_URL}?establishment_id=${establishment}`)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchClasses = (establishment) => {
return fetch(
`${BE_SCHOOL_SCHOOLCLASSES_URL}?establishment_id=${establishment}`
)
.then(requestResponseHandler)
.catch(errorHandler);
};
export const fetchClasse = (id) => {
return fetch(`${BE_SCHOOL_SCHOOLCLASSES_URL}/${id}`).then(
requestResponseHandler
);
};
export const fetchSpecialities = (establishment) => {
return fetchWithAuth(
`${BE_SCHOOL_SPECIALITIES_URL}?establishment_id=${establishment}`
);
};
export const fetchTeachers = (establishment) => {
return fetchWithAuth(`${BE_SCHOOL_TEACHERS_URL}?establishment_id=${establishment}`);
};
export const fetchClasses = (establishment) => {
return fetchWithAuth(
`${BE_SCHOOL_SCHOOLCLASSES_URL}?establishment_id=${establishment}`
);
};
export const fetchClasse = (id) => {
return fetchWithAuth(`${BE_SCHOOL_SCHOOLCLASSES_URL}/${id}`);
};
export const fetchSchedules = () => {
return fetch(`${BE_SCHOOL_PLANNINGS_URL}`)
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(`${BE_SCHOOL_PLANNINGS_URL}`);
};
export const fetchRegistrationDiscounts = (establishment) => {
return fetch(
return fetchWithAuth(
`${BE_SCHOOL_DISCOUNTS_URL}?filter=registration&establishment_id=${establishment}`
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
export const fetchTuitionDiscounts = (establishment) => {
return fetch(
return fetchWithAuth(
`${BE_SCHOOL_DISCOUNTS_URL}?filter=tuition&establishment_id=${establishment}`
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
export const fetchRegistrationFees = (establishment) => {
return fetch(
return fetchWithAuth(
`${BE_SCHOOL_FEES_URL}?filter=registration&establishment_id=${establishment}`
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
export const fetchTuitionFees = (establishment) => {
return fetch(
return fetchWithAuth(
`${BE_SCHOOL_FEES_URL}?filter=tuition&establishment_id=${establishment}`
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
export const fetchRegistrationPaymentPlans = (establishment) => {
return fetch(
return fetchWithAuth(
`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=registration&establishment_id=${establishment}`
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
export const fetchTuitionPaymentPlans = (establishment) => {
return fetch(
return fetchWithAuth(
`${BE_SCHOOL_PAYMENT_PLANS_URL}?filter=tuition&establishment_id=${establishment}`
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
export const fetchRegistrationPaymentModes = (establishment) => {
return fetch(
return fetchWithAuth(
`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=registration&establishment_id=${establishment}`
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
export const fetchTuitionPaymentModes = (establishment) => {
return fetch(
return fetchWithAuth(
`${BE_SCHOOL_PAYMENT_MODES_URL}?filter=tuition&establishment_id=${establishment}`
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
export const fetchEstablishment = (establishment) => {
return fetch(`${BE_SCHOOL_ESTABLISHMENT_URL}/${establishment}`)
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(`${BE_SCHOOL_ESTABLISHMENT_URL}/${establishment}`);
};
export const createDatas = (url, newData, csrfToken) => {
return fetch(url, {
return fetchWithAuth(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(newData),
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
export const updateDatas = (url, id, updatedData, csrfToken) => {
return fetch(`${url}/${id}`, {
return fetchWithAuth(`${url}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(updatedData),
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
export const removeDatas = (url, id, csrfToken) => {
return fetch(`${url}/${id}`, {
return fetchWithAuth(`${url}/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
headers: { 'X-CSRFToken': csrfToken },
});
};

View File

@ -1,5 +1,5 @@
import { BE_SETTINGS_SMTP_URL } from '@/utils/Url';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
import { fetchWithAuth } from '@/utils/fetchWithAuth';
export const PENDING = 'pending';
export const SUBSCRIBED = 'subscribed';
@ -10,26 +10,15 @@ export const fetchSmtpSettings = (csrfToken, establishment_id = null) => {
if (establishment_id) {
url += `?establishment_id=${establishment_id}`;
}
return fetch(`${url}`, {
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
})
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(url, {
headers: { 'X-CSRFToken': csrfToken },
});
};
export const editSmtpSettings = (data, csrfToken) => {
return fetch(`${BE_SETTINGS_SMTP_URL}/`, {
return fetchWithAuth(`${BE_SETTINGS_SMTP_URL}/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(data),
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};

View File

@ -11,20 +11,15 @@ import {
} from '@/utils/Url';
import { CURRENT_YEAR_FILTER } from '@/utils/constants';
import { errorHandler, requestResponseHandler } from './actionsHandlers';
import { fetchWithAuth, fetchWithAuthRaw } from '@/utils/fetchWithAuth';
import logger from '@/utils/logger';
export const editStudentCompetencies = (data, csrfToken) => {
const request = new Request(`${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}`, {
return fetchWithAuth(`${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}`, {
method: 'PUT',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
});
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const fetchStudentCompetencies = (id, period) => {
@ -33,13 +28,7 @@ export const fetchStudentCompetencies = (id, period) => {
? `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}&period=${period}`
: `${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}`;
const request = new Request(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler).catch(errorHandler);
return fetchWithAuth(url);
};
export const fetchRegisterForms = (
@ -53,37 +42,22 @@ export const fetchRegisterForms = (
if (page !== '' && pageSize !== '') {
url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}?filter=${filter}&establishment_id=${establishment}&page=${page}&search=${search}`;
}
return fetch(url, {
headers: {
'Content-Type': 'application/json',
},
})
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(url);
};
export const fetchRegisterForm = (id) => {
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`) // Utilisation de studentId au lieu de codeDI
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`); // Utilisation de studentId au lieu de codeDI
};
export const fetchLastGuardian = () => {
return fetch(`${BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL}`)
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(`${BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL}`);
};
export const editRegisterForm = (id, data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
method: 'PUT',
headers: {
'X-CSRFToken': csrfToken,
},
headers: { 'X-CSRFToken': csrfToken },
body: data,
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
export const autoSaveRegisterForm = async (id, data, csrfToken) => {
@ -106,15 +80,12 @@ export const autoSaveRegisterForm = async (id, data, csrfToken) => {
}
autoSaveData.append('auto_save', 'true');
return fetch(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
return fetchWithAuth(`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}`, {
method: 'PATCH', // Utiliser PATCH pour les mises à jour partielles
headers: {
'X-CSRFToken': csrfToken,
},
headers: { 'X-CSRFToken': csrfToken },
body: autoSaveData,
credentials: 'include',
})
.then(requestResponseHandler)
.then(() => {})
.catch(() => {
// Silent fail pour l'auto-save
logger.debug('Auto-save failed silently');
@ -127,62 +98,30 @@ export const autoSaveRegisterForm = async (id, data, csrfToken) => {
export const createRegisterForm = (data, csrfToken) => {
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}`;
return fetch(url, {
return fetchWithAuth(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(data),
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
export const sendRegisterForm = (id) => {
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/send`;
return fetch(url, {
headers: {
'Content-Type': 'application/json',
},
})
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(url);
};
export const resendRegisterForm = (id) => {
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/resend`;
return fetch(url, {
headers: {
'Content-Type': 'application/json',
},
})
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(url);
};
export const archiveRegisterForm = (id) => {
const url = `${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/archive`;
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(url);
};
export const searchStudents = (establishmentId, query) => {
const url = `${BE_SUBSCRIPTION_SEARCH_STUDENTS_URL}/?establishment_id=${establishmentId}&q=${encodeURIComponent(query)}`;
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(url);
};
export const fetchStudents = (establishment, id = null, status = null) => {
@ -195,153 +134,68 @@ export const fetchStudents = (establishment, id = null, status = null) => {
url += `&status=${status}`;
}
}
const request = new Request(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return fetch(request).then(requestResponseHandler).catch(errorHandler);
return fetchWithAuth(url);
};
export const fetchChildren = (id, establishment) => {
const request = new Request(
`${BE_SUBSCRIPTION_CHILDRENS_URL}/${id}?establishment_id=${establishment}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}
return fetchWithAuth(
`${BE_SUBSCRIPTION_CHILDRENS_URL}/${id}?establishment_id=${establishment}`
);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export async function getRegisterFormFileTemplate(fileId) {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTERFORM_FILE_TEMPLATE_URL}/${fileId}`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
return fetchWithAuth(
`${BE_SUBSCRIPTION_REGISTERFORM_FILE_TEMPLATE_URL}/${fileId}`
);
if (!response.ok) {
throw new Error('Failed to fetch file template');
}
return response.json();
}
export const fetchSchoolFileTemplatesFromRegistrationFiles = async (id) => {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/school_file_templates`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
export const fetchSchoolFileTemplatesFromRegistrationFiles = (id) => {
return fetchWithAuth(
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/school_file_templates`
);
if (!response.ok) {
throw new Error(
'Erreur lors de la récupération des fichiers associés au groupe'
);
}
return response.json();
};
export const fetchParentFileTemplatesFromRegistrationFiles = async (id) => {
const response = await fetch(
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/parent_file_templates`,
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
}
export const fetchParentFileTemplatesFromRegistrationFiles = (id) => {
return fetchWithAuth(
`${BE_SUBSCRIPTION_REGISTERFORMS_URL}/${id}/parent_file_templates`
);
if (!response.ok) {
throw new Error(
'Erreur lors de la récupération des fichiers associés au groupe'
);
}
return response.json();
};
export const dissociateGuardian = async (studentId, guardianId) => {
const response = await fetch(
export const dissociateGuardian = (studentId, guardianId) => {
return fetchWithAuth(
`${BE_SUBSCRIPTION_STUDENTS_URL}/${studentId}/guardians/${guardianId}/dissociate`,
{
credentials: 'include',
method: 'PUT',
headers: {
Accept: 'application/json',
},
}
);
if (!response.ok) {
// Extraire le message d'erreur du backend
const errorData = await response.json();
const errorMessage =
errorData?.error || 'Une erreur est survenue lors de la dissociation.';
// Jeter une erreur avec le message spécifique
throw new Error(errorMessage);
}
return response.json();
};
export const fetchAbsences = (establishment) => {
return fetch(
return fetchWithAuth(
`${BE_SUBSCRIPTION_ABSENCES_URL}?establishment_id=${establishment}`
)
.then(requestResponseHandler)
.catch(errorHandler);
);
};
export const createAbsences = (data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_ABSENCES_URL}`, {
return fetchWithAuth(`${BE_SUBSCRIPTION_ABSENCES_URL}`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
export const editAbsences = (absenceId, payload, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_ABSENCES_URL}/${absenceId}`, {
return fetchWithAuth(`${BE_SUBSCRIPTION_ABSENCES_URL}/${absenceId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(payload), // Sérialisez les données en JSON
credentials: 'include',
}).then((response) => {
if (!response.ok) {
return response.json().then((error) => {
throw new Error(error);
});
}
return response.json();
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(payload),
});
};
export const deleteAbsences = (id, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_ABSENCES_URL}/${id}`, {
return fetchWithAuthRaw(`${BE_SUBSCRIPTION_ABSENCES_URL}/${id}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
headers: { 'X-CSRFToken': csrfToken },
});
};
@ -352,16 +206,7 @@ export const deleteAbsences = (id, csrfToken) => {
*/
export const fetchRegistrationSchoolFileMasters = (establishmentId) => {
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}?establishment_id=${establishmentId}`;
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(url);
};
/**
@ -373,22 +218,14 @@ export const fetchRegistrationSchoolFileMasters = (establishmentId) => {
*/
export const saveFormResponses = (templateId, formTemplateData, csrfToken) => {
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${templateId}`;
const payload = {
formTemplateData: formTemplateData,
};
return fetch(url, {
return fetchWithAuth(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
headers: { 'X-CSRFToken': csrfToken },
body: JSON.stringify(payload),
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
});
};
/**
@ -398,14 +235,5 @@ export const saveFormResponses = (templateId, formTemplateData, csrfToken) => {
*/
export const fetchFormResponses = (templateId) => {
const url = `${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${templateId}`;
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
})
.then(requestResponseHandler)
.catch(errorHandler);
return fetchWithAuth(url);
};

View File

@ -48,30 +48,6 @@ export default function FormRenderer({
}
}, [initialValues, reset]);
// Fonction utilitaire pour envoyer les données au backend
const sendFormDataToBackend = async (formData) => {
try {
// Cette fonction peut être remplacée par votre propre implémentation
// Exemple avec fetch:
const response = await fetch('/api/submit-form', {
method: 'POST',
body: formData,
// Les en-têtes sont automatiquement définis pour FormData
});
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
const result = await response.json();
logger.debug('Envoi réussi:', result);
return result;
} catch (error) {
logger.error("Erreur lors de l'envoi:", error);
throw error;
}
};
const onSubmit = async (data) => {
logger.debug('=== DÉBUT onSubmit ===');
logger.debug('Réponses :', data);

View File

@ -1,27 +1,32 @@
import { useEffect, useState } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useEstablishment } from '@/context/EstablishmentContext';
import { FE_USERS_LOGIN_URL, getRedirectUrlFromRole } from '@/utils/Url';
import Loader from '@/components/Loader';
import logger from '@/utils/logger';
const ProtectedRoute = ({ children, requiredRight }) => {
const { user, profileRole } = useEstablishment();
const { data: session, status } = useSession();
const { profileRole } = useEstablishment();
const router = useRouter();
const [hasRequiredRight, setHasRequiredRight] = useState(false);
// Vérifier si l'utilisateur a au moins un rôle correspondant au requiredRight
useEffect(() => {
logger.debug({
user,
profileRole,
requiredRight,
hasRequiredRight,
});
// Ne pas agir tant que NextAuth charge la session
if (status === 'loading') return;
if (user && profileRole !== null) {
logger.debug({ status, profileRole, requiredRight });
if (status === 'unauthenticated') {
router.push(FE_USERS_LOGIN_URL);
return;
}
// status === 'authenticated' — vérifier les droits
if (profileRole !== null && profileRole !== undefined) {
let requiredRightChecked = false;
if (requiredRight && Array.isArray(requiredRight)) {
// Vérifier si l'utilisateur a le droit requis
requiredRightChecked = requiredRight.some(
(right) => profileRole === right
);
@ -30,21 +35,18 @@ const ProtectedRoute = ({ children, requiredRight }) => {
}
setHasRequiredRight(requiredRightChecked);
// Vérifier si l'utilisateur a le droit requis mais pas le bon role on le redirige la page d'accueil associé au role
if (!requiredRightChecked) {
const redirectUrl = getRedirectUrlFromRole(profileRole);
if (redirectUrl !== null) {
router.push(`${redirectUrl}`);
if (redirectUrl) {
router.push(redirectUrl);
}
}
} else {
// User non authentifié
router.push(`${FE_USERS_LOGIN_URL}`);
}
}, [user, profileRole]);
}, [status, profileRole, requiredRight]);
// Autoriser l'affichage si authentifié et rôle correct
return hasRequiredRight ? children : null;
if (status === 'loading' || !hasRequiredRight) return <Loader />;
return children;
};
export default ProtectedRoute;

View File

@ -145,18 +145,19 @@ const TeachersSection = ({
// Retourne le profil existant pour un email
const getUsedProfileForEmail = (email) => {
// On cherche tous les profils dont l'email correspond
const matchingProfiles = profiles.filter(p => p.email === email);
const matchingProfiles = profiles.filter((p) => p.email === email);
// On retourne le premier profil correspondant (ou undefined)
const result = matchingProfiles.length > 0 ? matchingProfiles[0] : undefined;
const result =
matchingProfiles.length > 0 ? matchingProfiles[0] : undefined;
return result;
};
// Met à jour le formData et newTeacher si besoin
const updateFormData = (data) => {
setFormData(prev => ({ ...prev, ...data }));
if (newTeacher) setNewTeacher(prev => ({ ...prev, ...data }));
setFormData((prev) => ({ ...prev, ...data }));
if (newTeacher) setNewTeacher((prev) => ({ ...prev, ...data }));
};
// Récupération des messages d'erreur pour un champ donné
@ -171,7 +172,9 @@ const TeachersSection = ({
const existingProfile = getUsedProfileForEmail(email);
if (existingProfile) {
logger.info(`Adresse email déjà utilisée pour le profil ${existingProfile.id}`);
logger.info(
`Adresse email déjà utilisée pour le profil ${existingProfile.id}`
);
}
updateFormData({
@ -202,8 +205,8 @@ const TeachersSection = ({
logger.debug('[DELETE] Suppression teacher id:', id);
return handleDelete(id)
.then(() => {
setTeachers(prevTeachers =>
prevTeachers.filter(teacher => teacher.id !== id)
setTeachers((prevTeachers) =>
prevTeachers.filter((teacher) => teacher.id !== id)
);
logger.debug('[DELETE] Teacher supprimé:', id);
})
@ -247,13 +250,13 @@ const TeachersSection = ({
createdTeacher.profile
) {
newProfileId = createdTeacher.profile;
foundProfile = profiles.find(p => p.id === newProfileId);
foundProfile = profiles.find((p) => p.id === newProfileId);
}
setTeachers([createdTeacher, ...teachers]);
setNewTeacher(null);
setLocalErrors({});
setFormData(prev => ({
setFormData((prev) => ({
...prev,
existingProfileId: newProfileId,
}));
@ -419,7 +422,7 @@ const TeachersSection = ({
case 'SPECIALITES':
return (
<div className="flex justify-center space-x-2 flex-wrap">
{teacher.specialities_details.map((speciality) => (
{(teacher.specialities_details ?? []).map((speciality) => (
<SpecialityItem
key={speciality.id}
speciality={speciality}

View File

@ -1,4 +1,6 @@
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
import { flushSync } from 'react-dom';
import logger from '@/utils/logger';
const EstablishmentContext = createContext();
@ -46,10 +48,11 @@ export const EstablishmentProvider = ({ children }) => {
const storedUser = sessionStorage.getItem('user');
return storedUser ? JSON.parse(storedUser) : null;
});
const [selectedEstablishmentLogo, setSelectedEstablishmentLogoState] = useState(() => {
const storedLogo = sessionStorage.getItem('selectedEstablishmentLogo');
return storedLogo ? JSON.parse(storedLogo) : null;
});
const [selectedEstablishmentLogo, setSelectedEstablishmentLogoState] =
useState(() => {
const storedLogo = sessionStorage.getItem('selectedEstablishmentLogo');
return storedLogo ? JSON.parse(storedLogo) : null;
});
// Sauvegarder dans sessionStorage à chaque mise à jour
const setSelectedEstablishmentId = (id) => {
@ -106,8 +109,6 @@ export const EstablishmentProvider = ({ children }) => {
}
const user = session.user;
logger.debug('User Session:', user);
setUser(user);
logger.debug('Establishments User= ', user);
const userEstablishments = user.roles.map((role, i) => ({
id: role.establishment__id,
name: role.establishment__name,
@ -117,27 +118,37 @@ export const EstablishmentProvider = ({ children }) => {
role_id: i,
role_type: role.role_type,
}));
setEstablishments(userEstablishments);
logger.debug('Establishments', user.roleIndexLoginDefault);
let roleIndexDefault = 0;
if (user.roles && user.roles.length > 0) {
let roleIndexDefault = 0;
if (userEstablishments.length > user.roleIndexLoginDefault) {
roleIndexDefault = user.roleIndexLoginDefault;
}
setSelectedRoleId(roleIndexDefault);
if (userEstablishments.length > 0) {
setSelectedEstablishmentId(userEstablishments[roleIndexDefault].id);
setSelectedEstablishmentEvaluationFrequency(
userEstablishments[roleIndexDefault].evaluation_frequency
);
setSelectedEstablishmentTotalCapacity(
userEstablishments[roleIndexDefault].total_capacity
);
setSelectedEstablishmentLogo(
userEstablishments[roleIndexDefault].logo
);
setProfileRole(userEstablishments[roleIndexDefault].role_type);
}
// flushSync force React à commiter tous les setState de manière synchrone
// avant que endInitFunctionHandler (router.push) soit appelé.
// Sans ça, ProtectedRoute verrait user=null au premier rendu post-navigation.
flushSync(() => {
setUser(user);
setEstablishments(userEstablishments);
if (user.roles && user.roles.length > 0) {
setSelectedRoleId(roleIndexDefault);
if (userEstablishments.length > 0) {
setSelectedEstablishmentId(userEstablishments[roleIndexDefault].id);
setSelectedEstablishmentEvaluationFrequency(
userEstablishments[roleIndexDefault].evaluation_frequency
);
setSelectedEstablishmentTotalCapacity(
userEstablishments[roleIndexDefault].total_capacity
);
setSelectedEstablishmentLogo(
userEstablishments[roleIndexDefault].logo
);
setProfileRole(userEstablishments[roleIndexDefault].role_type);
}
}
});
logger.debug('Establishments', user.roleIndexLoginDefault);
if (user.roles && user.roles.length > 0) {
if (endInitFunctionHandler) {
const role = session.user.roles[roleIndexDefault].role_type;
endInitFunctionHandler(role);

View File

@ -1,6 +1,5 @@
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { getJWT, refreshJWT } from '@/app/actions/authAction';
import jwt_decode from 'jsonwebtoken';
import logger from '@/utils/logger';
@ -13,19 +12,32 @@ const options = {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials, req) => {
authorize: async (credentials) => {
// URL calculée ici (pas au niveau module) pour garantir que NEXT_PUBLIC_API_URL est chargé
const loginUrl = `${process.env.NEXT_PUBLIC_API_URL}/Auth/login`;
try {
const data = {
email: credentials.email,
password: credentials.password,
};
const res = await fetch(loginUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Connection: close évite le SocketError undici lié au keep-alive vers Daphne
Connection: 'close',
},
body: JSON.stringify({
email: credentials.email,
password: credentials.password,
}),
});
const user = await getJWT(data);
if (user) {
return user;
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body?.errorMessage || 'Identifiants invalides');
}
const user = await res.json();
return user || null;
} catch (error) {
logger.error('Authorize error:', error.message);
throw new Error(error.message || 'Invalid credentials');
}
},
@ -33,8 +45,10 @@ const options = {
],
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 jours
updateAge: 24 * 60 * 60, // 24 heures
maxAge: 60 * 60, // 1 Hour
// 0 = réécrire le cookie à chaque fois que le token change (indispensable avec
// un access token Django de 15 min, sinon le cookie expiré reste en place)
updateAge: 0,
},
cookies: {
sessionToken: {
@ -64,25 +78,61 @@ const options = {
return token;
}
// Token expiré, essayer de le rafraîchir
// Token Django expiré (lifetime = 15 min), essayer de le rafraîchir
logger.info('JWT: access token expiré, tentative de refresh');
if (!token.refresh) {
logger.error('JWT: refresh token absent dans la session');
return { ...token, error: 'RefreshTokenError' };
}
const refreshUrl = `${process.env.NEXT_PUBLIC_API_URL}/Auth/refreshJWT`;
if (!process.env.NEXT_PUBLIC_API_URL) {
logger.error('JWT: NEXT_PUBLIC_API_URL non défini, refresh impossible');
return { ...token, error: 'RefreshTokenError' };
}
try {
const response = await refreshJWT({ refresh: token.refresh });
if (response && response?.token) {
return {
...token,
token: response.token,
refresh: response.refresh,
tokenExpires: jwt_decode.decode(response.token).exp * 1000,
};
} else {
throw new Error('Failed to refresh token');
const res = await fetch(refreshUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Connection: close évite le SocketError undici lié au keep-alive vers Daphne
Connection: 'close',
},
body: JSON.stringify({ refresh: token.refresh }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
logger.error('JWT: refresh échoué', { status: res.status, body });
throw new Error(`Refresh HTTP ${res.status}`);
}
const response = await res.json();
if (!response?.token) {
logger.error('JWT: réponse refresh sans token', { response });
throw new Error('Réponse refresh invalide');
}
logger.info('JWT: refresh réussi');
return {
...token,
token: response.token,
refresh: response.refresh,
tokenExpires: jwt_decode.decode(response.token).exp * 1000,
error: undefined,
};
} catch (error) {
logger.error('Refresh token failed:', error);
return token;
logger.error('JWT: refresh token failed', { message: error.message });
return { ...token, error: 'RefreshTokenError' };
}
},
async session({ session, token }) {
if (token?.error === 'RefreshTokenError') {
session.error = 'RefreshTokenError';
return session;
}
if (token && token?.token) {
const { user_id, email, roles, roleIndexLoginDefault } =
jwt_decode.decode(token.token);

View File

@ -0,0 +1,101 @@
import { getSession } from 'next-auth/react';
import {
requestResponseHandler,
errorHandler,
triggerSignOut,
} from '@/app/actions/actionsHandlers';
import logger from '@/utils/logger';
// Déduplique les appels concurrents à getSession() :
// si plusieurs fetchWithAuth() partent en même temps (chargement de page),
// ils partagent la même promesse au lieu de déclencher N refreshs JWT en parallèle.
let _pendingSessionPromise = null;
const getSessionOnce = () => {
if (!_pendingSessionPromise) {
_pendingSessionPromise = getSession().finally(() => {
_pendingSessionPromise = null;
});
}
return _pendingSessionPromise;
};
/**
* Récupère le token JWT Bearer depuis la session NextAuth.
* @returns {Promise<string|null>}
*/
export const getAuthToken = async () => {
const session = await getSessionOnce();
if (!session) {
logger.warn('getAuthToken: session nulle, aucun token envoyé');
return null;
}
if (session?.error === 'RefreshTokenError') {
logger.warn(
'getAuthToken: RefreshTokenError détecté, déconnexion en cours'
);
await triggerSignOut();
return null;
}
if (!session?.user?.token) {
logger.warn('getAuthToken: session présente mais token absent', {
session,
});
return null;
}
return session.user.token;
};
/**
* Wrapper de fetch qui injecte automatiquement le header Authorization Bearer
* depuis la session NextAuth, puis passe la réponse dans requestResponseHandler.
*
* - Ajoute Content-Type: application/json par défaut (sauf si le body est FormData)
* - Ajoute credentials: 'include' par défaut
* - Les options.headers passées en paramètre surchargent les défauts (ex: X-CSRFToken)
*
* @param {string} url
* @param {RequestInit} options
* @returns {Promise<any>} Corps de la réponse désérialisé
*/
export const fetchWithAuth = async (url, options = {}) => {
const token = await getAuthToken();
const isFormData = options.body instanceof FormData;
const headers = {
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
...options.headers,
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
return fetch(url, {
credentials: 'include',
...options,
headers,
})
.then(requestResponseHandler)
.catch(errorHandler);
};
/**
* Variante de fetchWithAuth qui retourne la Response brute sans passer
* par requestResponseHandler. Utile quand l'appelant gère lui-même response.ok.
*
* @param {string} url
* @param {RequestInit} options
* @returns {Promise<Response>}
*/
export const fetchWithAuthRaw = async (url, options = {}) => {
const token = await getAuthToken();
const headers = {
...options.headers,
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
return fetch(url, {
credentials: 'include',
...options,
headers,
});
};