feat: Création d'un profile selector [#37,#38]

This commit is contained in:
Luc SORIGNET
2025-04-12 16:07:30 +02:00
parent 4c2e2f8756
commit 89b01b79db
16 changed files with 237 additions and 286 deletions

View File

@ -9,7 +9,7 @@ class Profile(AbstractUser):
USERNAME_FIELD = 'email' USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ('password', ) REQUIRED_FIELDS = ('password', )
roleIndexLoginDefault = models.IntegerField(default=0)
code = models.CharField(max_length=200, default="", blank=True) code = models.CharField(max_length=200, default="", blank=True)
datePeremption = models.CharField(max_length=200, default="", blank=True) datePeremption = models.CharField(max_length=200, default="", blank=True)

View File

@ -14,7 +14,7 @@ class ProfileSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Profile model = Profile
fields = ['id', 'password', 'email', 'code', 'datePeremption', 'username', 'roles'] fields = ['id', 'password', 'email', 'code', 'datePeremption', 'username', 'roles', 'roleIndexLoginDefault']
extra_kwargs = {'password': {'write_only': True}} extra_kwargs = {'password': {'write_only': True}}
def get_roles(self, obj): def get_roles(self, obj):

View File

@ -54,6 +54,7 @@ class SessionView(APIView):
'user': openapi.Schema(type=openapi.TYPE_OBJECT, properties={ 'user': openapi.Schema(type=openapi.TYPE_OBJECT, properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER), 'id': openapi.Schema(type=openapi.TYPE_INTEGER),
'email': openapi.Schema(type=openapi.TYPE_STRING), 'email': openapi.Schema(type=openapi.TYPE_STRING),
'roleIndexLoginDefault': openapi.Schema(type=openapi.TYPE_INTEGER),
'roles': openapi.Schema(type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_OBJECT, properties={ 'roles': openapi.Schema(type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_OBJECT, properties={
'role_type': openapi.Schema(type=openapi.TYPE_STRING), 'role_type': openapi.Schema(type=openapi.TYPE_STRING),
'establishment': openapi.Schema(type=openapi.TYPE_STRING) 'establishment': openapi.Schema(type=openapi.TYPE_STRING)
@ -65,18 +66,16 @@ class SessionView(APIView):
) )
def get(self, request): def get(self, request):
token = request.META.get('HTTP_AUTHORIZATION', '').split('Bearer ')[-1] token = request.META.get('HTTP_AUTHORIZATION', '').split('Bearer ')[-1]
try: try:
decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256']) decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
userid = decoded_token.get('user_id') userid = decoded_token.get('user_id')
user = Profile.objects.get(id=userid) user = Profile.objects.get(id=userid)
roles = ProfileRole.objects.filter(profile=user).values('role_type', 'establishment__name') roles = ProfileRole.objects.filter(profile=user).values('role_type', 'establishment__name')
response_data = { response_data = {
'user': { 'user': {
'id': user.id, 'id': user.id,
'email': user.email, 'email': user.email,
'roleIndexLoginDefault': user.roleIndexLoginDefault,
'roles': list(roles) 'roles': list(roles)
} }
} }
@ -157,11 +156,10 @@ class LoginView(APIView):
operation_description="Connexion utilisateur", operation_description="Connexion utilisateur",
request_body=openapi.Schema( request_body=openapi.Schema(
type=openapi.TYPE_OBJECT, type=openapi.TYPE_OBJECT,
required=['email', 'password', 'role_type'], required=['email', 'password'],
properties={ properties={
'email': openapi.Schema(type=openapi.TYPE_STRING), 'email': openapi.Schema(type=openapi.TYPE_STRING),
'password': openapi.Schema(type=openapi.TYPE_STRING), 'password': openapi.Schema(type=openapi.TYPE_STRING)
'role_type': openapi.Schema(type=openapi.TYPE_STRING)
} }
), ),
responses={ responses={
@ -194,38 +192,15 @@ class LoginView(APIView):
password=data.get('password'), password=data.get('password'),
) )
if user is not None: if user is not None:
role_type = data.get('role_type') # Vérifier si l'utilisateur a un role actif
primary_role = ProfileRole.objects.filter(profile=user, role_type=role_type, is_active=True).first() has_active_role = ProfileRole.objects.filter(profile=user, is_active=True).first()
if not has_active_role:
if not primary_role:
return JsonResponse({"errorMessage": "Profil inactif"}, status=status.HTTP_401_UNAUTHORIZED) return JsonResponse({"errorMessage": "Profil inactif"}, status=status.HTTP_401_UNAUTHORIZED)
login(request, user) login(request, user)
user.save() user.save()
retour = '' retour = ''
access_token, refresh_token = makeToken(user)
# Récupérer tous les rôles de l'utilisateur avec le type spécifié
roles = ProfileRole.objects.filter(profile=user, role_type=role_type).values('role_type', 'establishment__id', 'establishment__name')
# Générer le JWT avec la bonne syntaxe datetime
access_payload = {
'user_id': user.id,
'email': user.email,
'roles': list(roles),
'type': 'access',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
access_token = jwt.encode(access_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
# Générer le Refresh Token (exp: 7 jours)
refresh_payload = {
'user_id': user.id,
'type': 'refresh',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
refresh_token = jwt.encode(refresh_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
return JsonResponse({ return JsonResponse({
'token': access_token, 'token': access_token,
@ -299,35 +274,10 @@ class RefreshJWTView(APIView):
# Récupérer les informations utilisateur # Récupérer les informations utilisateur
user = Profile.objects.get(id=payload['user_id']) user = Profile.objects.get(id=payload['user_id'])
role_type = payload.get('role_type') if not user:
return JsonResponse({'errorMessage': 'Utilisateur non trouvé'}, status=404)
# Récupérer le rôle principal de l'utilisateur new_access_payload, new_refresh_token = makeToken(user)
primary_role = ProfileRole.objects.filter(profile=user, role_type=role_type, is_active=True).first()
if not primary_role:
return JsonResponse({'errorMessage': 'Profil inactif'}, status=400)
# Générer un nouveau Access Token avec les informations complètes
new_access_payload = {
'user_id': user.id,
'email': user.email,
'role_type': primary_role.get_role_type_display(),
'establishment': primary_role.establishment.id,
'type': 'access',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
new_access_token = jwt.encode(new_access_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
new_refresh_payload = {
'user_id': user.id,
'role_type': role_type,
'type': 'refresh',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
new_refresh_token = jwt.encode(new_refresh_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
return JsonResponse({'token': new_access_token, 'refresh': new_refresh_token}, status=200) return JsonResponse({'token': new_access_token, 'refresh': new_refresh_token}, status=200)
@ -341,6 +291,38 @@ class RefreshJWTView(APIView):
logger.error(f"Erreur inattendue: {str(e)}") logger.error(f"Erreur inattendue: {str(e)}")
return JsonResponse({'errorMessage': f'Erreur inattendue: {str(e)}'}, status=400) return JsonResponse({'errorMessage': f'Erreur inattendue: {str(e)}'}, status=400)
def makeToken(user):
"""
Fonction pour créer un token JWT pour l'utilisateur donné.
"""
try:
# Récupérer tous les rôles de l'utilisateur actifs
roles = ProfileRole.objects.filter(profile=user, is_active=True).values('role_type', 'establishment__id', 'establishment__name')
# Générer le JWT avec la bonne syntaxe datetime
access_payload = {
'user_id': user.id,
'email': user.email,
'roleIndexLoginDefault':user.roleIndexLoginDefault,
'roles': list(roles),
'type': 'access',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
access_token = jwt.encode(access_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
# Générer le Refresh Token (exp: 7 jours)
refresh_payload = {
'user_id': user.id,
'type': 'refresh',
'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
refresh_token = jwt.encode(refresh_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
return access_token, refresh_token
except Exception as e:
logger.error(f"Erreur lors de la création du token: {str(e)}")
return None
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')

View File

@ -131,7 +131,6 @@ export default function Layout({
if (roleIndex === -1) { if (roleIndex === -1) {
roleIndex = 0; roleIndex = 0;
} }
setCurrentRoleIndex(roleIndex);
const role = session.user.roles[roleIndex].role_type; const role = session.user.roles[roleIndex].role_type;
setProfileRole(role); setProfileRole(role);
}} }}

View File

@ -10,7 +10,7 @@ import logger from '@/utils/logger';
import { fetchRegisterForms } from '@/app/actions/subscriptionAction'; import { fetchRegisterForms } from '@/app/actions/subscriptionAction';
import { fetchUpcomingEvents } from '@/app/actions/planningAction'; import { fetchUpcomingEvents } from '@/app/actions/planningAction';
import { getSession } from 'next-auth/react'; import { getSession } from 'next-auth/react';
import { getCurrentRoleIndex } from '@/store/Store'; import { useEstablishment } from '@/context/EstablishmentContext';
// Composant EventCard pour afficher les événements // Composant EventCard pour afficher les événements
@ -42,14 +42,11 @@ export default function DashboardPage() {
const [classes, setClasses] = useState([]); const [classes, setClasses] = useState([]);
const [establishmentId, setEstablishmentId] = useState(null); const [establishmentId, setEstablishmentId] = useState(null);
const { selectedEstablishmentId } = useEstablishment();
useEffect(() => { useEffect(() => {
getSession() getSession()
.then(session => { .then(session => {
if (session && session.user) { setEstablishmentId(selectedEstablishmentId);
const establishmentId = session.user.roles[getCurrentRoleIndex()].establishment__id;
setEstablishmentId(establishmentId);
}
}) })
.catch(err => { .catch(err => {
logger.error('Error fetching session:', err); logger.error('Error fetching session:', err);

View File

@ -2,6 +2,7 @@
// src/components/Layout.js // src/components/Layout.js
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import DropdownMenu from '@/components/DropdownMenu'; import DropdownMenu from '@/components/DropdownMenu';
import ProfileSelector from '@/components/ProfileSelector';
import { useRouter } from 'next/navigation'; // Ajout de l'importation import { useRouter } from 'next/navigation'; // Ajout de l'importation
import { User, MessageSquare, LogOut, Settings, Home } from 'lucide-react'; // Ajout de l'importation de l'icône Home import { User, MessageSquare, LogOut, Settings, Home } from 'lucide-react'; // Ajout de l'importation de l'icône Home
import Logo from '@/components/Logo'; // Ajout de l'importation du composant Logo import Logo from '@/components/Logo'; // Ajout de l'importation du composant Logo
@ -11,11 +12,11 @@ import ProtectedRoute from '@/components/ProtectedRoute';
import { disconnect } from '@/app/actions/authAction'; import { disconnect } from '@/app/actions/authAction';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { useSession } from 'next-auth/react';
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import { getRightStr, RIGHTS } from '@/utils/rights'; import { getRightStr, RIGHTS } from '@/utils/rights';
import { getGravatarUrl } from '@/utils/gravatar'; import { getGravatarUrl } from '@/utils/gravatar';
import { useEstablishment } from '@/context/EstablishmentContext';
import Image from 'next/image'; import Image from 'next/image';
import Footer from '@/components/Footer';
export default function Layout({ export default function Layout({
children, children,
@ -23,11 +24,12 @@ export default function Layout({
const router = useRouter(); // Définition de router const router = useRouter(); // Définition de router
const [messages, setMessages] = useState([]); const [messages, setMessages] = useState([]);
const { data: session, status } = useSession();
const [userId, setUserId] = useState(null);
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isPopupVisible, setIsPopupVisible] = useState(false); const [isPopupVisible, setIsPopupVisible] = useState(false);
const { profileRole, user } = useEstablishment();
const softwareName = "N3WT School";
const softwareVersion = `${process.env.NEXT_PUBLIC_APP_VERSION}`;
const handleDisconnect = () => { const handleDisconnect = () => {
setIsPopupVisible(true); setIsPopupVisible(true);
@ -38,40 +40,13 @@ export default function Layout({
disconnect(); disconnect();
}; };
// useEffect(() => {
// if (status === 'loading') return;
// if (!session) {
// router.push(`${FE_USERS_LOGIN_URL}`);
// }
// const userIdFromSession = session.user.id;
// setUserId(userIdFromSession);
// setIsLoading(true);
// fetchMessages(userId)
// .then(data => {
// if (data) {
// setMessages(data);
// }
// logger.debug('Success :', data);
// })
// .catch(error => {
// logger.error('Error fetching data:', error);
// })
// .finally(() => {
// setIsLoading(false);
// });
// }, [userId]);
// if (isLoading) {
// return <div>Loading...</div>;
// }
const dropdownItems = [ const dropdownItems = [
{ {
type: 'info', type: 'info',
content: ( content: (
<div className="px-4 py-2"> <div className="px-4 py-2">
<div className="font-medium">{user?.email || 'Utilisateur'}</div> <div className="font-medium">{user?.email || 'Utilisateur'}</div>
<div className="text-xs text-gray-400">{getRightStr(user?.roles[0]?.role_type) || ''}</div> <div className="text-xs text-gray-400">{getRightStr(profileRole) || ''}</div>
</div> </div>
) )
}, },
@ -91,11 +66,14 @@ const dropdownItems = [
<ProtectedRoute requiredRight={RIGHTS.PARENT}> <ProtectedRoute requiredRight={RIGHTS.PARENT}>
<div className="flex flex-col min-h-screen bg-gray-50"> <div className="flex flex-col min-h-screen bg-gray-50">
{/* Entête */} {/* Entête */}
<header className="bg-white border-b border-gray-200 px-4 py-2 md:px-8 md:py-4 flex items-center justify-between fixed top-0 left-0 right-0 z-10"> <header className="h-16 bg-white border-b border-gray-200 px-4 md:px-8 py-4 flex items-center justify-between fixed top-0 left-0 right-0 z-10">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Logo className="h-6 w-6 md:h-8 md:w-8" /> {/* Utilisation du composant Logo */} <div className="border-b border-gray-200 ">
<ProfileSelector
<div className="text-lg md:text-xl font-semibold">Accueil</div> className="w-64 border-r"
/>
</div>
<div className="text-lg md:text-xl p-2 font-semibold">Accueil</div>
</div> </div>
<div className="flex items-center space-x-2 md:space-x-4"> <div className="flex items-center space-x-2 md:space-x-4">
<button <button
@ -116,6 +94,7 @@ const dropdownItems = [
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-emerald-600"></span> <span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-emerald-600"></span>
)} )}
</div> </div>
<DropdownMenu <DropdownMenu
buttonContent={<Image buttonContent={<Image
src={getGravatarUrl(user?.email)} src={getGravatarUrl(user?.email)}
@ -135,6 +114,8 @@ const dropdownItems = [
<div className="pt-16 md:pt-20 p-4 md:p-8 flex-1"> {/* Ajout de flex-1 pour utiliser toute la hauteur disponible */} <div className="pt-16 md:pt-20 p-4 md:p-8 flex-1"> {/* Ajout de flex-1 pour utiliser toute la hauteur disponible */}
{children} {children}
</div> </div>
{/* Footer responsive */}
<Footer softwareName={softwareName} softwareVersion={softwareVersion} />
</div> </div>
<Popup <Popup
visible={isPopupVisible} visible={isPopupVisible}

View File

@ -7,51 +7,33 @@ import StatusLabel from '@/components/StatusLabel';
import { FE_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url'; import { FE_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url';
import { fetchChildren } from '@/app/actions/subscriptionAction'; import { fetchChildren } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { useSession } from 'next-auth/react';
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import ProfileSelector from '@/components/ProfileSelector';
export default function ParentHomePage() { export default function ParentHomePage() {
const [children, setChildren] = useState([]); const [children, setChildren] = useState([]);
const { data: session, status } = useSession();
const [userId, setUserId] = useState(null); const [userId, setUserId] = useState(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [establishments, setEstablishments] = useState([]); const { user, setProfileRole, selectedEstablishmentId, setSelectedEstablishmentId, establishments } = useEstablishment();
const { selectedEstablishmentId, setSelectedEstablishmentId } = useEstablishment();
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
if (status === 'loading') return; const userIdFromSession = user.user_id;
if (!session || !session.user) {
router.push(`${FE_USERS_LOGIN_URL}`);
} else {
const userIdFromSession = session.user.user_id;
setUserId(userIdFromSession); setUserId(userIdFromSession);
const userEstablishments = session.user.roles.map(role => ({
id: role.establishment__id,
name: role.establishment__name,
role_type: role.role_type
}));
setEstablishments(userEstablishments);
if (!selectedEstablishmentId && userEstablishments.length > 0) {
setSelectedEstablishmentId(userEstablishments[0].id);
}
console.log(selectedEstablishmentId) console.log(selectedEstablishmentId)
fetchChildren(userIdFromSession, selectedEstablishmentId).then(data => { fetchChildren(userIdFromSession, selectedEstablishmentId).then(data => {
setChildren(data); setChildren(data);
}); });
}
}, [status, session, selectedEstablishmentId]); }, [ selectedEstablishmentId]);
const handleEstablishmentChange = (e) => { const handleEstablishmentChange = (e) => {
const establishmentId = parseInt(e.target.value, 10); const establishmentId = parseInt(e.target.value, 10);
setSelectedEstablishmentId(establishmentId); setSelectedEstablishmentId(establishmentId);
const role = establishments.find(est => est.id === establishmentId)?.role_type;
setProfileRole(role);
}; };
function handleEdit(eleveId) { function handleEdit(eleveId) {
// Logique pour éditer le dossier de l'élève // Logique pour éditer le dossier de l'élève
logger.debug(`Edit dossier for student id: ${eleveId}`); logger.debug(`Edit dossier for student id: ${eleveId}`);
@ -107,22 +89,6 @@ export default function ParentHomePage() {
<Users className="h-6 w-6 text-emerald-600" /> <Users className="h-6 w-6 text-emerald-600" />
Enfants Enfants
</h2> </h2>
{establishments.length > 1 && (
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">Sélectionnez un établissement :</label>
<select
value={selectedEstablishmentId}
onChange={handleEstablishmentChange}
className="block w-full mt-1 p-2 border border-gray-300 rounded-md"
>
{establishments.map((establishment, index) => (
<option key={`${establishment.id}-${index}`} value={establishment.id}>
{establishment.name}
</option>
))}
</select>
</div>
)}
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<Table <Table
data={children} data={children}

View File

@ -9,16 +9,16 @@ import Button from '@/components/Button'; // Importez le composant Button
import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires import { User, KeySquare } from 'lucide-react'; // Importez directement les icônes nécessaires
import { import {
FE_USERS_NEW_PASSWORD_URL, FE_USERS_NEW_PASSWORD_URL,
FE_ADMIN_SUBSCRIPTIONS_URL, getRedirectUrlFromRole
FE_PARENTS_HOME_URL
} from '@/utils/Url'; } from '@/utils/Url';
import { login } from '@/app/actions/authAction'; import { login } from '@/app/actions/authAction';
import { getSession } from 'next-auth/react'; import { getSession } from 'next-auth/react';
import { useCsrfToken } from '@/context/CsrfContext'; // Importez le hook useCsrfToken import { useCsrfToken } from '@/context/CsrfContext'; // Importez le hook useCsrfToken
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import ProfileSelector from '@/components/ProfileSelector'; // Importez le composant ProfileSelector
import { RIGHTS } from '@/utils/rights';
import { setCurrentRoleIndex } from '@/store/Store'; import { useEstablishment } from '@/context/EstablishmentContext';
export default function Page() { export default function Page() {
@ -26,8 +26,7 @@ export default function Page() {
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const [userFieldError, setUserFieldError] = useState("") const [userFieldError, setUserFieldError] = useState("")
const [passwordFieldError, setPasswordFieldError] = useState("") const [passwordFieldError, setPasswordFieldError] = useState("")
const [selectedProfile, setSelectedProfile] = useState(1); // Par défaut, sélectionnez ADMIN const { setUser } = useEstablishment();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const router = useRouter(); const router = useRouter();
@ -37,14 +36,15 @@ export default function Page() {
return data.errorMessage === "" return data.errorMessage === ""
} }
function handleFormLogin(formData) { function handleFormLogin(formData) {
setIsLoading(true); setIsLoading(true);
setErrorMessage(""); setErrorMessage("");
login({ login({
email: formData.get('login'), email: formData.get('login'),
password: formData.get('password'), password: formData.get('password')
role_type: selectedProfile
}).then(result => { }).then(result => {
logger.debug('Sign In Result', result); logger.debug('Sign In Result', result);
@ -58,18 +58,16 @@ export default function Page() {
} }
const user = session.user; const user = session.user;
logger.debug('User Session:', user); logger.debug('User Session:', user);
setUser(session.user);
const roles = user.roles.filter(role => role.role_type === selectedProfile); if (session.user.roles && session.user.roles.length > 0) {
if (roles.length > 0) { let roleIndex = 0;
// Redirection en fonction du rôle if( session.user.roles.length > session.user.roleIndexLoginDefault){
// Ne pas désactiver le loader avant la redirection roleIndex = session.user.roleIndexLoginDefault;
const currentRoleIndex = 0; }
setCurrentRoleIndex(currentRoleIndex); const role = session.user.roles[roleIndex].role_type;
const role = roles[currentRoleIndex].role_type; const url = getRedirectUrlFromRole(role);
if (role === RIGHTS.ADMIN || role === RIGHTS.TEACHER) { if (url) {
router.push(FE_ADMIN_SUBSCRIPTIONS_URL); router.push(url);
} else if (role === RIGHTS.PARENT) {
router.push(FE_PARENTS_HOME_URL);
} else { } else {
setIsLoading(false); setIsLoading(false);
setErrorMessage('Type de rôle non géré'); setErrorMessage('Type de rôle non géré');
@ -104,7 +102,6 @@ export default function Page() {
<DjangoCSRFToken csrfToken={csrfToken} /> <DjangoCSRFToken csrfToken={csrfToken} />
<InputTextIcon name="login" type="text" IconItem={User} label="Identifiant" placeholder="Identifiant" errorMsg={userFieldError} className="w-full mb-5" /> <InputTextIcon name="login" type="text" IconItem={User} label="Identifiant" placeholder="Identifiant" errorMsg={userFieldError} className="w-full mb-5" />
<InputTextIcon name="password" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={passwordFieldError} className="w-full mb-5" /> <InputTextIcon name="password" type="password" IconItem={KeySquare} label="Mot de passe" placeholder="Mot de passe" errorMsg={passwordFieldError} className="w-full mb-5" />
<ProfileSelector selectedProfile={selectedProfile} setSelectedProfile={setSelectedProfile} />
<div className="input-group mb-4"> <div className="input-group mb-4">
</div> </div>
<label className="text-red-500">{errorMessage}</label> <label className="text-red-500">{errorMessage}</label>

View File

@ -1,29 +1,65 @@
import React from 'react'; import React, { useState } from 'react';
import { useEstablishment } from '@/context/EstablishmentContext';
import DropdownMenu from '@/components/DropdownMenu';
import { getRightStr } from '@/utils/rights';
import { ChevronDown } from 'lucide-react'; // Import de l'icône
const ProfileSelector = ({ onEstablishmentChange, className = '' }) => {
const { establishments, selectedEstablishmentId, setSelectedEstablishmentId, setProfileRole } = useEstablishment();
const [dropdownOpen, setDropdownOpen] = useState(false);
const handleEstablishmentChange = (establishmentId) => {
setSelectedEstablishmentId(establishmentId);
const role = establishments.find(est => est.id === establishmentId)?.role_type;
setProfileRole(role);
if (onEstablishmentChange) {
onEstablishmentChange(establishmentId);
}
setDropdownOpen(false); // Fermer le menu après sélection
};
// Si on a pas de rôle ou un seul rôle, on n'affiche pas le sélecteur
if (!establishments || establishments.length === 0 || establishments.length === 1) {
return null;
}
const selectedEstablishment = establishments.find(est => est.id === selectedEstablishmentId);
const ProfileSelector = ({ selectedProfile, setSelectedProfile }) => {
return ( return (
<div className="flex space-x-4"> <div className={`relative ${className}`}>
<button <DropdownMenu
type="button" buttonContent={
className={`px-4 py-2 rounded-lg shadow-md focus:outline-none focus:ring-2 focus:ring-blue-300 ${selectedProfile === 1 ? 'bg-blue-500 text-white ring-2 ring-blue-500' : 'bg-gray-200'}`} <div className="h-16 flex items-center gap-2 cursor-pointer px-4 bg-white">
onClick={() => setSelectedProfile(1)} <div className="flex-1">
> <div className="font-bold text-left">{getRightStr(selectedEstablishment?.role_type) || ''}</div>
ADMINISTRATEUR <div className="text-sm text-gray-500 text-left">
</button> {selectedEstablishment?.name || 'Sélectionnez un établissement'}
<button </div>
type="button" </div>
className={`px-4 py-2 rounded-lg shadow-md focus:outline-none focus:ring-2 focus:ring-blue-300 ${selectedProfile === 0 ? 'bg-blue-500 text-white ring-2 ring-blue-500' : 'bg-gray-200'}`} {/* Icône ChevronDown avec rotation conditionnelle */}
onClick={() => setSelectedProfile(0)} <ChevronDown
> className={`w-5 h-5 transition-transform duration-200 ${
PROFESSEUR dropdownOpen ? 'rotate-180' : 'rotate-0'
</button> }`}
<button />
type="button" </div>
className={`px-4 py-2 rounded-lg shadow-md focus:outline-none focus:ring-2 focus:ring-blue-300 ${selectedProfile === 2 ? 'bg-blue-500 text-white ring-2 ring-blue-500' : 'bg-gray-200'}`} }
onClick={() => setSelectedProfile(2)} items={establishments.map(establishment => ({
> type: 'item',
PARENT label: (
</button> <div className="text-left">
<div className="font-bold">{getRightStr(establishment.role_type)}</div>
<div className="text-sm text-gray-500">{establishment.name}</div>
</div>
),
onClick: () => handleEstablishmentChange(establishment.id),
}))}
buttonClassName="w-full"
menuClassName="absolute mt-2 w-full bg-white border border-gray-200 rounded shadow-lg z-10"
dropdownOpen={dropdownOpen}
setDropdownOpen={setDropdownOpen}
/>
</div> </div>
); );
}; };

View File

@ -1,33 +1,33 @@
import React, { useEffect } from 'react'; import { useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useSession } from 'next-auth/react'; import { useEstablishment } from '@/context/EstablishmentContext';
import Loader from '@/components/Loader'; // Importez le composant Loader import { FE_USERS_LOGIN_URL,getRedirectUrlFromRole } from '@/utils/Url';
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
const ProtectedRoute = ({ children, requiredRight }) => { const ProtectedRoute = ({ children, requiredRight }) => {
const { data: session, status } = useSession({
required: true,
onUnauthenticated() {
router.push(`${FE_USERS_LOGIN_URL}`);
}
});
const router = useRouter();
// Ne vérifier que si le statut est définitif const { user, profileRole } = useEstablishment();
if (status === 'loading') { const router = useRouter();
return <Loader />; const hasRequiredRight = (profileRole === requiredRight);
}
// Vérifier si l'utilisateur a au moins un rôle correspondant au requiredRight // Vérifier si l'utilisateur a au moins un rôle correspondant au requiredRight
const hasRequiredRight = session?.user?.roles?.some(role => role.role_type === requiredRight); useEffect(() => {
if(user){
if (session && requiredRight && !hasRequiredRight) { // Vérifier si l'utilisateur a le droit requis mais pas le bon role on le redirige la page d'accueil associé au role
router.push(`${FE_USERS_LOGIN_URL}`); if (!hasRequiredRight) {
return null; const redirectUrl = getRedirectUrlFromRole(profileRole);
router.push(`${redirectUrl}`);
} }
}else{
// User non authentifié
router.push(`${FE_USERS_LOGIN_URL}`);
}
}, [profileRole]);
// Autoriser l'affichage si authentifié et rôle correct // Autoriser l'affichage si authentifié et rôle correct
return session ? children : null; return hasRequiredRight ? children : null;
}; };
export default ProtectedRoute; export default ProtectedRoute;

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEstablishment } from '@/context/EstablishmentContext'; import ProfileSelector from '@/components/ProfileSelector';
const SidebarItem = ({ icon: Icon, text, active, url, onClick }) => ( const SidebarItem = ({ icon: Icon, text, active, url, onClick }) => (
<div <div
@ -15,9 +15,9 @@ const SidebarItem = ({ icon: Icon, text, active, url, onClick }) => (
</div> </div>
); );
function Sidebar({ establishments, currentPage, items, onCloseMobile, onEstablishmentChange }) { function Sidebar({ currentPage, items, onCloseMobile, onEstablishmentChange }) {
const router = useRouter(); const router = useRouter();
const { selectedEstablishmentId, setSelectedEstablishmentId, setProfileRole } = useEstablishment();
const [selectedItem, setSelectedItem] = useState(currentPage); const [selectedItem, setSelectedItem] = useState(currentPage);
useEffect(() => { useEffect(() => {
@ -32,31 +32,15 @@ function Sidebar({ establishments, currentPage, items, onCloseMobile, onEstablis
} }
}; };
const handleEstablishmentChange = (e) => {
const establishmentId = parseInt(e.target.value, 10);
setSelectedEstablishmentId(establishmentId);
const role = establishments.find(est => est.id === establishmentId)?.role_type;
setProfileRole(role);
onEstablishmentChange(establishmentId);
};
return ( return (
<div className="w-64 bg-white border-r h-full border-gray-200 py-6 px-4"> <div className="w-64 bg-white border-r h-full border-gray-200">
<div className="flex items-center mb-8 px-2"> <div className="border-b border-gray-200 ">
<select <ProfileSelector
value={selectedEstablishmentId || ''} onEstablishmentChange={onEstablishmentChange}
onChange={handleEstablishmentChange} className="border-none"
className="form-select block w-full mt-1" />
>
{establishments.map(establishment => (
<option key={establishment.id} value={establishment.id}>
{establishment.name}
</option>
))}
</select>
</div> </div>
<nav className="space-y-1 px-4 py-6">
<nav className="space-y-1">
{items.map((item) => ( {items.map((item) => (
<SidebarItem <SidebarItem
key={item.id} key={item.id}

View File

@ -1,37 +1,41 @@
import React, { createContext, useContext, useState, useEffect } from 'react'; import React, { createContext, useContext, useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
const EstablishmentContext = createContext(); const EstablishmentContext = createContext();
export const EstablishmentProvider = ({ children }) => { export const EstablishmentProvider = ({ children }) => {
const { data: session, status } = useSession();
const [selectedEstablishmentId, setSelectedEstablishmentId] = useState(null); const [selectedEstablishmentId, setSelectedEstablishmentId] = useState(null);
const [profileRole, setProfileRole] = useState(null); const [profileRole, setProfileRole] = useState(null);
const [establishments, setEstablishments] = useState([]); const [establishments, setEstablishments] = useState([]);
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
useEffect(() => { useEffect(() => {
if (status === 'loading') return; // Attendre que le statut de la session soit défini if (user) {
logger.debug('Establishments User= ', user);
if (session && session.user) { // Au changement de l'utilisateur on sette par défaut le premier établissement
setUser(session.user); // et le premier rôle
const userEstablishments = session.user.roles.map(role => ({ const userEstablishments = user.roles.map(role => ({
id: role.establishment__id, id: role.establishment__id,
name: role.establishment__name, name: role.establishment__name,
role_type: role.role_type role_type: role.role_type
})); }));
setEstablishments(userEstablishments); setEstablishments(userEstablishments);
// Sélectionner l'établissement depuis la session ou le premier établissement par défaut logger.debug('Establishments', user.roleIndexLoginDefault);
let roleIndexDefault = 0;
user.roleIndexLoginDefault;
// Sélectionner l'établissement au login par défaut
if (userEstablishments.length > user.roleIndexLoginDefault) {
roleIndexDefault = user.roleIndexLoginDefault;
}
if (userEstablishments.length > 0) { if (userEstablishments.length > 0) {
setSelectedEstablishmentId(userEstablishments[0].id); setSelectedEstablishmentId(userEstablishments[roleIndexDefault].id);
setProfileRole(userEstablishments[0].role_type); setProfileRole(userEstablishments[roleIndexDefault].role_type);
} }
} }
}, [session, status]); }, [user]);
return ( return (
<EstablishmentContext.Provider value={{ selectedEstablishmentId, setSelectedEstablishmentId, profileRole, setProfileRole, establishments, user }}> <EstablishmentContext.Provider value={{ selectedEstablishmentId, setSelectedEstablishmentId, profileRole, setProfileRole, establishments, setUser, user }}>
{children} {children}
</EstablishmentContext.Provider> </EstablishmentContext.Provider>
); );

View File

@ -1,8 +1,9 @@
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
import { createPlanning, fetchEvents, fetchPlannings, updatePlanning, createEvent, deleteEvent, updateEvent } from '@/app/actions/planningAction'; import { createPlanning, fetchEvents, fetchPlannings, updatePlanning, createEvent, deleteEvent, updateEvent } from '@/app/actions/planningAction';
import { useCsrfToken } from './CsrfContext'; import { useCsrfToken } from './CsrfContext';
import { ESTABLISHMENT_ID } from '@/utils/Url';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { useSession } from 'next-auth/react';
import { useEstablishment }from '@/context/EstablishmentContext';
@ -27,13 +28,15 @@ export function PlanningProvider({ children }) {
const [currentDate, setCurrentDate] = useState(new Date()); const [currentDate, setCurrentDate] = useState(new Date());
const [viewType, setViewType] = useState('week'); // Changer 'month' en 'week' const [viewType, setViewType] = useState('week'); // Changer 'month' en 'week'
const [hiddenSchedules, setHiddenSchedules] = useState([]); const [hiddenSchedules, setHiddenSchedules] = useState([]);
const { selectedEstablishmentId } = useEstablishment();
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
useEffect(()=>{ useEffect(()=>{
fetchPlannings().then((data) => { fetchPlannings().then((data) => {
setSchedules(data) setSchedules(data)
if(data.length > 0){
setSelectedSchedule(data[0].id); setSelectedSchedule(data[0].id);
}
}); });
fetchEvents().then((data)=>{ fetchEvents().then((data)=>{
setEvents(data); setEvents(data);
@ -47,9 +50,6 @@ export function PlanningProvider({ children }) {
createEvent(newEvent).then((data) => { createEvent(newEvent).then((data) => {
setEvents((prevEvents) => [...prevEvents, data]); setEvents((prevEvents) => [...prevEvents, data]);
}); });
console.log('newEvent',newEvent);
//dssetEvents((prevEvents) => [...prevEvents, newEvent]);
}; };
const handleUpdateEvent = (id, updatedEvent) => { const handleUpdateEvent = (id, updatedEvent) => {
@ -69,10 +69,8 @@ export function PlanningProvider({ children }) {
}; };
const addSchedule = (newSchedule) => { const addSchedule = (newSchedule) => {
//FIXME:Gerenr lestablshment
logger.debug('newSchedule',newSchedule); logger.debug('newSchedule',newSchedule);
newSchedule.establishment = ESTABLISHMENT_ID; newSchedule.establishment = selectedEstablishmentId;
createPlanning(newSchedule,csrfToken).then((data) => { createPlanning(newSchedule,csrfToken).then((data) => {
setSchedules((prevSchedules) => [...prevSchedules, data]); setSchedules((prevSchedules) => [...prevSchedules, data]);
}); });

View File

@ -11,15 +11,13 @@ const options = {
name: 'Credentials', name: 'Credentials',
credentials: { credentials: {
email: { label: 'Email', type: 'email' }, email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' }, password: { label: 'Password', type: 'password' }
role_type: { label: 'Role Type', type: 'text' }
}, },
authorize: async (credentials, req) => { authorize: async (credentials, req) => {
try { try {
const data = { const data = {
email: credentials.email, email: credentials.email,
password: credentials.password, password: credentials.password
role_type: credentials.role_type
}; };
const user = await getJWT(data); const user = await getJWT(data);
@ -87,14 +85,15 @@ const options = {
}, },
async session({ session, token }) { async session({ session, token }) {
if (token && token?.token) { if (token && token?.token) {
const { user_id, email, roles } = jwt_decode.decode(token.token); const { user_id, email, roles, roleIndexLoginDefault } = jwt_decode.decode(token.token);
session.user = { session.user = {
...session.user, ...session.user,
token: token.token, token: token.token,
refresh: token.refresh, refresh: token.refresh,
user_id: user_id, user_id: user_id,
email: email, email: email,
roles: roles roles: roles,
roleIndexLoginDefault : roleIndexLoginDefault
}; };
} }
return session; return session;

View File

@ -1,11 +0,0 @@
// Current Role Index
// Cette fonction permet de stocker et l'index du rôle actuel dans le localStorage
export function setCurrentRoleIndex(currentRoleIndex){
localStorage.setItem('currentRoleIndex', currentRoleIndex);
}
export function getCurrentRoleIndex(){
const currentRoleIndex = localStorage.getItem('currentRoleIndex');
return currentRoleIndex? currentRoleIndex : 0;
}

View File

@ -1,7 +1,7 @@
import { RIGHTS } from '@/utils/rights';
export const BASE_URL = process.env.NEXT_PUBLIC_API_URL; export const BASE_URL = process.env.NEXT_PUBLIC_API_URL;
//URL-Back-End //URL-Back-End
// GESTION DocuSeal // GESTION DocuSeal
@ -106,3 +106,22 @@ export const FE_PARENTS_EDIT_INSCRIPTION_URL = `/parents/editInscription`
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`
/**
* Fonction pour obtenir l'URL de redirection en fonction du rôle
* @param {RIGHTS} role
* @returns {string|null} L'URL de redirection ou null si le rôle n'est pas géré
*/
export function getRedirectUrlFromRole(role) {
switch (role) {
case RIGHTS.ADMIN:
return FE_ADMIN_SUBSCRIPTIONS_URL;
case RIGHTS.TEACHER:
return FE_ADMIN_SUBSCRIPTIONS_URL;
case RIGHTS.PARENT:
return FE_PARENTS_HOME_URL;
default:
return null;
}
}