feat(auth): add profile and QR card identification endpoints
This commit is contained in:
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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 });
|
||||||
|
}
|
||||||
@@ -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<CarteAvecUtilisateur | null> {
|
||||||
|
return prisma.carteEtudiante.findFirst({
|
||||||
|
where: { qrCode },
|
||||||
|
include: { utilisateur: { include: { role: true, campus: true } } },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,9 +3,20 @@ import { prisma } from '../db/prisma';
|
|||||||
|
|
||||||
export type UtilisateurAvecRole = Prisma.UtilisateurGetPayload<{ include: { role: true } }>;
|
export type UtilisateurAvecRole = Prisma.UtilisateurGetPayload<{ include: { role: true } }>;
|
||||||
|
|
||||||
|
export type UtilisateurProfil = Prisma.UtilisateurGetPayload<{
|
||||||
|
include: { role: true; campus: true };
|
||||||
|
}>;
|
||||||
|
|
||||||
export function findByEmail(email: string): Promise<UtilisateurAvecRole | null> {
|
export function findByEmail(email: string): Promise<UtilisateurAvecRole | null> {
|
||||||
return prisma.utilisateur.findUnique({
|
return prisma.utilisateur.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
include: { role: true },
|
include: { role: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findById(id: number): Promise<UtilisateurProfil | null> {
|
||||||
|
return prisma.utilisateur.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: { role: true, campus: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { currentUser } from '../middlewares/current-user';
|
import { currentUser } from '../middlewares/current-user';
|
||||||
|
import { authRoutes } from './auth.routes';
|
||||||
import { materielRoutes } from './materiel.routes';
|
import { materielRoutes } from './materiel.routes';
|
||||||
import { empruntRoutes } from './emprunt.routes';
|
import { empruntRoutes } from './emprunt.routes';
|
||||||
|
|
||||||
export const apiRouter: Router = Router();
|
export const apiRouter: Router = Router();
|
||||||
|
|
||||||
apiRouter.use(currentUser);
|
// /auth gère lui-même ses routes publiques/protégées.
|
||||||
apiRouter.use('/materiels', materielRoutes);
|
apiRouter.use('/auth', authRoutes);
|
||||||
apiRouter.use('/mes-emprunts', empruntRoutes);
|
|
||||||
|
// Le reste exige un utilisateur identifié.
|
||||||
|
apiRouter.use('/materiels', currentUser, materielRoutes);
|
||||||
|
apiRouter.use('/mes-emprunts', currentUser, empruntRoutes);
|
||||||
|
|||||||
@@ -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<UtilisateurProfil> {
|
||||||
|
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<UtilisateurProfil> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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.
|
- 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.
|
- 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
|
## 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)
|
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)
|
3. `Materiel.statut` -> `@default("DISPONIBLE")` (un matériel neuf est disponible, RG07)
|
||||||
4. Tailles de colonnes `@db.NVarChar(n)` (actuellement `NVARCHAR(1000)` partout)
|
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).*
|
||||||
|
|||||||
Reference in New Issue
Block a user