From b5a7577e8d279abb8a5fae4f8452f981f939f60f Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Mon, 22 Jun 2026 10:13:45 +0200 Subject: [PATCH] feat(auth): add profile and QR card identification endpoints --- .../src/controllers/auth.controller.ts | 23 ++++++++++++++ .../carte-etudiante.repository.ts | 13 ++++++++ .../repositories/utilisateur.repository.ts | 11 +++++++ eme-backend/src/routes/auth.routes.ts | 11 +++++++ eme-backend/src/routes/index.ts | 10 ++++-- .../src/services/utilisateur.service.ts | 31 +++++++++++++++++++ review.md | 12 +++++-- 7 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 eme-backend/src/controllers/auth.controller.ts create mode 100644 eme-backend/src/repositories/carte-etudiante.repository.ts create mode 100644 eme-backend/src/routes/auth.routes.ts create mode 100644 eme-backend/src/services/utilisateur.service.ts diff --git a/eme-backend/src/controllers/auth.controller.ts b/eme-backend/src/controllers/auth.controller.ts new file mode 100644 index 0000000..42dcbe4 --- /dev/null +++ b/eme-backend/src/controllers/auth.controller.ts @@ -0,0 +1,23 @@ +import { Request, Response } from 'express'; +import { AppError } from '../errors/app-error'; +import { getProfil, identifierParCarte } from '../services/utilisateur.service'; + +export async function getMe(req: Request, res: Response): Promise { + const user = req.user; + if (!user) { + throw new AppError(401, 'Authentification requise'); + } + + const profil = await getProfil(user.id); + res.json({ data: profil }); +} + +export async function identifier(req: Request, res: Response): Promise { + const qr = req.params.qr; + if (typeof qr !== 'string') { + throw new AppError(400, 'QR code invalide'); + } + + const profil = await identifierParCarte(qr); + res.json({ data: profil }); +} diff --git a/eme-backend/src/repositories/carte-etudiante.repository.ts b/eme-backend/src/repositories/carte-etudiante.repository.ts new file mode 100644 index 0000000..252efb4 --- /dev/null +++ b/eme-backend/src/repositories/carte-etudiante.repository.ts @@ -0,0 +1,13 @@ +import { Prisma } from '@prisma/client'; +import { prisma } from '../db/prisma'; + +export type CarteAvecUtilisateur = Prisma.CarteEtudianteGetPayload<{ + include: { utilisateur: { include: { role: true; campus: true } } }; +}>; + +export function findByQrCode(qrCode: string): Promise { + return prisma.carteEtudiante.findFirst({ + where: { qrCode }, + include: { utilisateur: { include: { role: true, campus: true } } }, + }); +} diff --git a/eme-backend/src/repositories/utilisateur.repository.ts b/eme-backend/src/repositories/utilisateur.repository.ts index c367538..c83be2f 100644 --- a/eme-backend/src/repositories/utilisateur.repository.ts +++ b/eme-backend/src/repositories/utilisateur.repository.ts @@ -3,9 +3,20 @@ import { prisma } from '../db/prisma'; export type UtilisateurAvecRole = Prisma.UtilisateurGetPayload<{ include: { role: true } }>; +export type UtilisateurProfil = Prisma.UtilisateurGetPayload<{ + include: { role: true; campus: true }; +}>; + export function findByEmail(email: string): Promise { return prisma.utilisateur.findUnique({ where: { email }, include: { role: true }, }); } + +export function findById(id: number): Promise { + return prisma.utilisateur.findUnique({ + where: { id }, + include: { role: true, campus: true }, + }); +} diff --git a/eme-backend/src/routes/auth.routes.ts b/eme-backend/src/routes/auth.routes.ts new file mode 100644 index 0000000..e81715b --- /dev/null +++ b/eme-backend/src/routes/auth.routes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { currentUser } from '../middlewares/current-user'; +import { getMe, identifier } from '../controllers/auth.controller'; + +export const authRoutes: Router = Router(); + +// Identification par QR code : point d'entrée du parcours, donc public. +authRoutes.get('/carte/:qr', identifier); + +// Profil de l'utilisateur déjà identifié : protégé. +authRoutes.get('/me', currentUser, getMe); diff --git a/eme-backend/src/routes/index.ts b/eme-backend/src/routes/index.ts index 22203b0..72fba39 100644 --- a/eme-backend/src/routes/index.ts +++ b/eme-backend/src/routes/index.ts @@ -1,10 +1,14 @@ import { Router } from 'express'; import { currentUser } from '../middlewares/current-user'; +import { authRoutes } from './auth.routes'; import { materielRoutes } from './materiel.routes'; import { empruntRoutes } from './emprunt.routes'; export const apiRouter: Router = Router(); -apiRouter.use(currentUser); -apiRouter.use('/materiels', materielRoutes); -apiRouter.use('/mes-emprunts', empruntRoutes); +// /auth gère lui-même ses routes publiques/protégées. +apiRouter.use('/auth', authRoutes); + +// Le reste exige un utilisateur identifié. +apiRouter.use('/materiels', currentUser, materielRoutes); +apiRouter.use('/mes-emprunts', currentUser, empruntRoutes); diff --git a/eme-backend/src/services/utilisateur.service.ts b/eme-backend/src/services/utilisateur.service.ts new file mode 100644 index 0000000..672872a --- /dev/null +++ b/eme-backend/src/services/utilisateur.service.ts @@ -0,0 +1,31 @@ +import { AppError } from '../errors/app-error'; +import { findById, UtilisateurProfil } from '../repositories/utilisateur.repository'; +import { findByQrCode } from '../repositories/carte-etudiante.repository'; + +export async function getProfil(utilisateurId: number): Promise { + const profil = await findById(utilisateurId); + if (!profil) { + throw new AppError(404, 'Utilisateur introuvable'); + } + + return profil; +} + +/* RG02 : identification de l'étudiant par le QR code de sa carte. La carte doit + exister, être active et non expirée, et le compte doit être actif. */ +export async function identifierParCarte(qrCode: string): Promise { + const carte = await findByQrCode(qrCode); + if (!carte || !carte.actif) { + throw new AppError(404, 'Carte non reconnue'); + } + + if (carte.dateExpiration.getTime() < Date.now()) { + throw new AppError(401, 'Carte expiree'); + } + + if (!carte.utilisateur.actif) { + throw new AppError(401, 'Compte inactif'); + } + + return carte.utilisateur; +} diff --git a/review.md b/review.md index c38a69f..dee5578 100644 --- a/review.md +++ b/review.md @@ -131,6 +131,12 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - Réutilise l'architecture en couches et l'auth simulée déjà en place. - Vérifié au runtime : Marie 1 (`EN_COURS`, son `CLOTURE` exclu), Lucas 1 (`EN_RETARD`), Sofia 0 (son `RETOUR_NON_CONFORME` exclu), 401 sans en-tête. +### Étape 16 — Block 4 : profil et identification +- `GET /api/auth/me` (protégé) : renvoie le profil complet de l'utilisateur courant (role + campus). +- `GET /api/auth/carte/:qr` (public) : identification par QR code (RG02/RG03) ; contrôles carte active, non expirée et compte actif ; renvoie le profil. +- Refactor du routage : `currentUser` appliqué par sous-routeur (au lieu d'un middleware global) pour laisser la route d'identification publique. +- Vérifié au runtime : `/me` 200 (profil Marie) et 401 sans en-tête ; `/carte` QR valide 200 sans en-tête (route publique), QR inconnu 404. + --- ## Dette technique en attente @@ -141,8 +147,10 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y 2. `MaterielAccessoire` -> `@@unique([materielId, accessoireId])` (un accessoire une seule fois par matériel) 3. `Materiel.statut` -> `@default("DISPONIBLE")` (un matériel neuf est disponible, RG07) 4. Tailles de colonnes `@db.NVarChar(n)` (actuellement `NVARCHAR(1000)` partout) -5. Block 1 : middleware d'erreurs global, logs, ESLint + Prettier, Swagger/OpenAPI +5. Swagger/OpenAPI (Block 1, reporté après les endpoints) +6. Auth réelle Azure AD (Block 3) — remplacera l'auth simulée par en-tête +7. DTO de sortie : les endpoints renvoient l'entité Prisma brute (`microsoftId`, timestamps exposés) — à filtrer avant la prod --- -*Dernière mise à jour : 2026-06-18 — Endpoint "mes emprunts en cours" en place.* +*Dernière mise à jour : 2026-06-22 — Profil et identification par QR en place (3/6 du Block 4).*