Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8f044924e | |||
| ab1bf29b7f | |||
| 101e52236f | |||
| b5a7577e8d | |||
| 20490a05c5 | |||
| bee21b9fd8 | |||
| bc83308d05 | |||
| 63c56ce775 | |||
| 57266b5414 | |||
| d67cf8d65e |
+21
-1
@@ -1,8 +1,28 @@
|
|||||||
# Node.js (backend)
|
# Node.js (backend)
|
||||||
**/node_modules/
|
**/node_modules/
|
||||||
**/dist/
|
**/dist/
|
||||||
**/.env
|
|
||||||
**/*.log
|
**/*.log
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Variables d'environnement (secrets) — seul l'exemple est versionné
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
!**/.env.example
|
||||||
|
|
||||||
|
# Clés et certificats (Azure AD, TLS)
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
|
*.cert
|
||||||
|
*.pfx
|
||||||
|
*.p12
|
||||||
|
|
||||||
|
# Dumps / sauvegardes de base
|
||||||
|
*.bak
|
||||||
|
*.dump
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
**/coverage/
|
||||||
|
|
||||||
# Local project context
|
# Local project context
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
prisma/migrations
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['dist/**', 'node_modules/**', 'prisma/migrations/**'],
|
||||||
|
},
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
prettier,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: { ...globals.node },
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'error',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'error',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'error',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||||
|
'prefer-const': 'error',
|
||||||
|
'no-console': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
Generated
+1076
-1
File diff suppressed because it is too large
Load Diff
@@ -4,28 +4,38 @@
|
|||||||
"description": "EME — Emprunt Matériel ENSUP — API REST",
|
"description": "EME — Emprunt Matériel ENSUP — API REST",
|
||||||
"main": "dist/app.js",
|
"main": "dist/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc -p tsconfig.build.json",
|
||||||
"start": "node dist/app.js",
|
"start": "node dist/app.js",
|
||||||
"dev": "nodemon --exec ts-node src/app.ts",
|
"dev": "nodemon --exec ts-node src/app.ts",
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:migrate": "prisma migrate dev",
|
"prisma:migrate": "prisma migrate dev",
|
||||||
"prisma:studio": "prisma studio",
|
"prisma:studio": "prisma studio",
|
||||||
"seed": "ts-node prisma/seed.ts",
|
"seed": "ts-node prisma/seed.ts",
|
||||||
"fixtures": "ts-node prisma/fixtures.ts"
|
"fixtures": "ts-node prisma/fixtures.ts",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"prisma/**/*.ts\"",
|
||||||
|
"format:check": "prettier --check \"src/**/*.ts\" \"prisma/**/*.ts\""
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^25.9.1",
|
"@types/node": "^25.9.1",
|
||||||
|
"eslint": "^10.5.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"globals": "^17.6.0",
|
||||||
"nodemon": "^3.1.14",
|
"nodemon": "^3.1.14",
|
||||||
|
"prettier": "^3.8.4",
|
||||||
"prisma": "^7.8.0",
|
"prisma": "^7.8.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3",
|
||||||
|
"typescript-eslint": "^8.61.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/adapter-mssql": "^7.8.0",
|
"@prisma/adapter-mssql": "^7.8.0",
|
||||||
|
|||||||
@@ -62,23 +62,23 @@ async function main(): Promise<void> {
|
|||||||
// Références (doivent exister via le seed).
|
// Références (doivent exister via le seed).
|
||||||
const roleEtudiant = required(
|
const roleEtudiant = required(
|
||||||
await prisma.role.findUnique({ where: { code: 'ETUDIANT' } }),
|
await prisma.role.findUnique({ where: { code: 'ETUDIANT' } }),
|
||||||
'Role ETUDIANT manquant — lance d\'abord `npm run seed`',
|
"Role ETUDIANT manquant — lance d'abord `npm run seed`",
|
||||||
);
|
);
|
||||||
const roleResponsable = required(
|
const roleResponsable = required(
|
||||||
await prisma.role.findUnique({ where: { code: 'RESPONSABLE' } }),
|
await prisma.role.findUnique({ where: { code: 'RESPONSABLE' } }),
|
||||||
'Role RESPONSABLE manquant — lance d\'abord `npm run seed`',
|
"Role RESPONSABLE manquant — lance d'abord `npm run seed`",
|
||||||
);
|
);
|
||||||
const campus = required(
|
const campus = required(
|
||||||
await prisma.campus.findFirst({ where: { nom: 'Campus Saint-Christophe' } }),
|
await prisma.campus.findFirst({ where: { nom: 'Campus Saint-Christophe' } }),
|
||||||
'Campus manquant — lance d\'abord `npm run seed`',
|
"Campus manquant — lance d'abord `npm run seed`",
|
||||||
);
|
);
|
||||||
const salle = required(
|
const salle = required(
|
||||||
await prisma.sallePret.findFirst({ where: { campusId: campus.id } }),
|
await prisma.sallePret.findFirst({ where: { campusId: campus.id } }),
|
||||||
'Salle de pret manquante — lance d\'abord `npm run seed`',
|
"Salle de pret manquante — lance d'abord `npm run seed`",
|
||||||
);
|
);
|
||||||
const poste = required(
|
const poste = required(
|
||||||
await prisma.posteEmprunt.findFirst({ where: { sallePretId: salle.id } }),
|
await prisma.posteEmprunt.findFirst({ where: { sallePretId: salle.id } }),
|
||||||
'Poste manquant — lance d\'abord `npm run seed`',
|
"Poste manquant — lance d'abord `npm run seed`",
|
||||||
);
|
);
|
||||||
const catOrdi = required(
|
const catOrdi = required(
|
||||||
await prisma.categorieMateriel.findFirst({ where: { nom: 'Ordinateur portable' } }),
|
await prisma.categorieMateriel.findFirst({ where: { nom: 'Ordinateur portable' } }),
|
||||||
@@ -305,8 +305,20 @@ async function main(): Promise<void> {
|
|||||||
});
|
});
|
||||||
await prisma.checklistElement.createMany({
|
await prisma.checklistElement.createMany({
|
||||||
data: [
|
data: [
|
||||||
{ checklistId: clDepart1.id, accessoireId: chargeur.id, nomElement: 'Chargeur', etat: 'PRESENT', quantiteConstatee: 1 },
|
{
|
||||||
{ checklistId: clDepart1.id, accessoireId: souris.id, nomElement: 'Souris', etat: 'PRESENT', quantiteConstatee: 1 },
|
checklistId: clDepart1.id,
|
||||||
|
accessoireId: chargeur.id,
|
||||||
|
nomElement: 'Chargeur',
|
||||||
|
etat: 'PRESENT',
|
||||||
|
quantiteConstatee: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checklistId: clDepart1.id,
|
||||||
|
accessoireId: souris.id,
|
||||||
|
nomElement: 'Souris',
|
||||||
|
etat: 'PRESENT',
|
||||||
|
quantiteConstatee: 1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -393,8 +405,20 @@ async function main(): Promise<void> {
|
|||||||
});
|
});
|
||||||
await prisma.checklistElement.createMany({
|
await prisma.checklistElement.createMany({
|
||||||
data: [
|
data: [
|
||||||
{ checklistId: clDepart4.id, accessoireId: housse.id, nomElement: 'Housse', etat: 'PRESENT', quantiteConstatee: 1 },
|
{
|
||||||
{ checklistId: clDepart4.id, accessoireId: chargeur.id, nomElement: 'Chargeur', etat: 'PRESENT', quantiteConstatee: 1 },
|
checklistId: clDepart4.id,
|
||||||
|
accessoireId: housse.id,
|
||||||
|
nomElement: 'Housse',
|
||||||
|
etat: 'PRESENT',
|
||||||
|
quantiteConstatee: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checklistId: clDepart4.id,
|
||||||
|
accessoireId: chargeur.id,
|
||||||
|
nomElement: 'Chargeur',
|
||||||
|
etat: 'PRESENT',
|
||||||
|
quantiteConstatee: 1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const clRetour4 = await prisma.checklist.create({
|
const clRetour4 = await prisma.checklist.create({
|
||||||
@@ -407,8 +431,21 @@ async function main(): Promise<void> {
|
|||||||
});
|
});
|
||||||
await prisma.checklistElement.createMany({
|
await prisma.checklistElement.createMany({
|
||||||
data: [
|
data: [
|
||||||
{ checklistId: clRetour4.id, accessoireId: housse.id, nomElement: 'Housse', etat: 'ABSENT', quantiteConstatee: 0, commentaire: 'Manquante au retour' },
|
{
|
||||||
{ checklistId: clRetour4.id, accessoireId: chargeur.id, nomElement: 'Chargeur', etat: 'PRESENT', quantiteConstatee: 1 },
|
checklistId: clRetour4.id,
|
||||||
|
accessoireId: housse.id,
|
||||||
|
nomElement: 'Housse',
|
||||||
|
etat: 'ABSENT',
|
||||||
|
quantiteConstatee: 0,
|
||||||
|
commentaire: 'Manquante au retour',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checklistId: clRetour4.id,
|
||||||
|
accessoireId: chargeur.id,
|
||||||
|
nomElement: 'Chargeur',
|
||||||
|
etat: 'PRESENT',
|
||||||
|
quantiteConstatee: 1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
console.log('[fixtures] 4 emprunts + checklists crees');
|
console.log('[fixtures] 4 emprunts + checklists crees');
|
||||||
@@ -464,7 +501,7 @@ async function main(): Promise<void> {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
console.log('[fixtures] 2 entrees d\'historique creees');
|
console.log("[fixtures] 2 entrees d'historique creees");
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { logger } from './utils/logger';
|
|||||||
import { requestLogger } from './middlewares/request-logger';
|
import { requestLogger } from './middlewares/request-logger';
|
||||||
import { notFoundHandler } from './middlewares/not-found';
|
import { notFoundHandler } from './middlewares/not-found';
|
||||||
import { errorHandler } from './middlewares/error-handler';
|
import { errorHandler } from './middlewares/error-handler';
|
||||||
|
import { apiRouter } from './routes';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -17,6 +18,8 @@ app.get('/health', (_req, res) => {
|
|||||||
res.json({ status: 'ok', app: 'EME API' });
|
res.json({ status: 'ok', app: 'EME API' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use('/api', apiRouter);
|
||||||
|
|
||||||
/* notFoundHandler et errorHandler doivent rester après toutes les routes :
|
/* notFoundHandler et errorHandler doivent rester après toutes les routes :
|
||||||
le premier capture les URL non gérées, le second clôt la chaîne Express. */
|
le premier capture les URL non gérées, le second clôt la chaîne Express. */
|
||||||
app.use(notFoundHandler);
|
app.use(notFoundHandler);
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { AppError } from '../errors/app-error';
|
||||||
|
import { getProfil, identifierParCarte } from '../services/utilisateur.service';
|
||||||
|
import { toUtilisateurResponse } from '../dtos/utilisateur.dto';
|
||||||
|
|
||||||
|
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: toUtilisateurResponse(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: toUtilisateurResponse(profil) });
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { AppError } from '../errors/app-error';
|
||||||
|
import { listerMesEmpruntsEnCours } from '../services/emprunt.service';
|
||||||
|
import { toEmpruntResponse } from '../dtos/emprunt.dto';
|
||||||
|
|
||||||
|
export async function getMesEmprunts(req: Request, res: Response): Promise<void> {
|
||||||
|
const user = req.user;
|
||||||
|
if (!user) {
|
||||||
|
throw new AppError(401, 'Authentification requise');
|
||||||
|
}
|
||||||
|
|
||||||
|
const emprunts = await listerMesEmpruntsEnCours(user.id);
|
||||||
|
res.json({ data: emprunts.map(toEmpruntResponse) });
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { AppError } from '../errors/app-error';
|
||||||
|
import { listerCatalogue } from '../services/materiel.service';
|
||||||
|
import { toMaterielResponse } from '../dtos/materiel.dto';
|
||||||
|
|
||||||
|
function parseCategorieId(value: unknown): number | undefined {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||||
|
throw new AppError(400, 'categorieId invalide');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCatalogue(req: Request, res: Response): Promise<void> {
|
||||||
|
const user = req.user;
|
||||||
|
if (!user) {
|
||||||
|
throw new AppError(401, 'Authentification requise');
|
||||||
|
}
|
||||||
|
|
||||||
|
const recherche = typeof req.query.q === 'string' ? req.query.q : undefined;
|
||||||
|
const categorieId = parseCategorieId(req.query.categorieId);
|
||||||
|
|
||||||
|
const materiels = await listerCatalogue(user.campusId, { categorieId, recherche });
|
||||||
|
res.json({ data: materiels.map(toMaterielResponse) });
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { EmpruntAvecMateriel } from '../repositories/emprunt.repository';
|
||||||
|
import { MaterielResponse, toMaterielResponse } from './materiel.dto';
|
||||||
|
|
||||||
|
export interface EmpruntResponse {
|
||||||
|
id: number;
|
||||||
|
dateEmprunt: Date;
|
||||||
|
dateRetourPrevue: Date;
|
||||||
|
dateRetourReelle: Date | null;
|
||||||
|
statut: string;
|
||||||
|
materiel: MaterielResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toEmpruntResponse(emprunt: EmpruntAvecMateriel): EmpruntResponse {
|
||||||
|
return {
|
||||||
|
id: emprunt.id,
|
||||||
|
dateEmprunt: emprunt.dateEmprunt,
|
||||||
|
dateRetourPrevue: emprunt.dateRetourPrevue,
|
||||||
|
dateRetourReelle: emprunt.dateRetourReelle,
|
||||||
|
statut: emprunt.statut,
|
||||||
|
materiel: toMaterielResponse(emprunt.materiel),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { MaterielAvecCategorie } from '../repositories/materiel.repository';
|
||||||
|
|
||||||
|
export interface CategorieResponse {
|
||||||
|
id: number;
|
||||||
|
nom: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaterielResponse {
|
||||||
|
id: number;
|
||||||
|
nom: string;
|
||||||
|
marque: string;
|
||||||
|
modele: string;
|
||||||
|
reference: string;
|
||||||
|
statut: string;
|
||||||
|
etatGeneral: string;
|
||||||
|
categorie: CategorieResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toMaterielResponse(materiel: MaterielAvecCategorie): MaterielResponse {
|
||||||
|
return {
|
||||||
|
id: materiel.id,
|
||||||
|
nom: materiel.nom,
|
||||||
|
marque: materiel.marque,
|
||||||
|
modele: materiel.modele,
|
||||||
|
reference: materiel.reference,
|
||||||
|
statut: materiel.statut,
|
||||||
|
etatGeneral: materiel.etatGeneral,
|
||||||
|
categorie: {
|
||||||
|
id: materiel.categorie.id,
|
||||||
|
nom: materiel.categorie.nom,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { UtilisateurProfil } from '../repositories/utilisateur.repository';
|
||||||
|
|
||||||
|
export interface RoleResponse {
|
||||||
|
code: string;
|
||||||
|
libelle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CampusResponse {
|
||||||
|
id: number;
|
||||||
|
nom: string;
|
||||||
|
ville: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UtilisateurResponse {
|
||||||
|
id: number;
|
||||||
|
nom: string;
|
||||||
|
prenom: string;
|
||||||
|
email: string;
|
||||||
|
classe: string | null;
|
||||||
|
role: RoleResponse;
|
||||||
|
campus: CampusResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toUtilisateurResponse(utilisateur: UtilisateurProfil): UtilisateurResponse {
|
||||||
|
return {
|
||||||
|
id: utilisateur.id,
|
||||||
|
nom: utilisateur.nom,
|
||||||
|
prenom: utilisateur.prenom,
|
||||||
|
email: utilisateur.email,
|
||||||
|
classe: utilisateur.classe,
|
||||||
|
role: {
|
||||||
|
code: utilisateur.role.code,
|
||||||
|
libelle: utilisateur.role.libelle,
|
||||||
|
},
|
||||||
|
campus: {
|
||||||
|
id: utilisateur.campus.id,
|
||||||
|
nom: utilisateur.campus.nom,
|
||||||
|
ville: utilisateur.campus.ville,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { AppError } from '../errors/app-error';
|
||||||
|
import { findByEmail } from '../repositories/utilisateur.repository';
|
||||||
|
import { AuthenticatedUser } from '../types/authenticated-user';
|
||||||
|
|
||||||
|
/* Auth simulée (temporaire) : en attendant Azure AD, l'utilisateur courant est
|
||||||
|
résolu depuis l'en-tête `x-user-email`. À remplacer par la validation du JWT. */
|
||||||
|
export async function currentUser(req: Request, _res: Response, next: NextFunction): Promise<void> {
|
||||||
|
const email = req.header('x-user-email');
|
||||||
|
if (!email) {
|
||||||
|
throw new AppError(401, 'En-tete x-user-email requis (auth simulee)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const utilisateur = await findByEmail(email);
|
||||||
|
if (!utilisateur || !utilisateur.actif) {
|
||||||
|
throw new AppError(401, 'Utilisateur non reconnu');
|
||||||
|
}
|
||||||
|
|
||||||
|
const authenticated: AuthenticatedUser = {
|
||||||
|
id: utilisateur.id,
|
||||||
|
email: utilisateur.email,
|
||||||
|
roleCode: utilisateur.role.code,
|
||||||
|
campusId: utilisateur.campusId,
|
||||||
|
};
|
||||||
|
|
||||||
|
req.user = authenticated;
|
||||||
|
next();
|
||||||
|
}
|
||||||
@@ -5,7 +5,12 @@ import { logger } from '../utils/logger';
|
|||||||
|
|
||||||
/* Le 4e argument est requis par Express pour identifier un gestionnaire d'erreurs,
|
/* Le 4e argument est requis par Express pour identifier un gestionnaire d'erreurs,
|
||||||
même s'il n'est pas utilisé. */
|
même s'il n'est pas utilisé. */
|
||||||
export function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction): void {
|
export function errorHandler(
|
||||||
|
err: unknown,
|
||||||
|
_req: Request,
|
||||||
|
res: Response,
|
||||||
|
_next: NextFunction,
|
||||||
|
): void {
|
||||||
const isAppError = err instanceof AppError;
|
const isAppError = err instanceof AppError;
|
||||||
const statusCode = isAppError ? err.statusCode : 500;
|
const statusCode = isAppError ? err.statusCode : 500;
|
||||||
const message = isAppError ? err.message : 'Erreur interne du serveur';
|
const message = isAppError ? err.message : 'Erreur interne du serveur';
|
||||||
|
|||||||
@@ -27,12 +27,7 @@ export const ETAT_CHECKLIST_ELEMENT = ['PRESENT', 'ABSENT', 'DETERIORE'] as cons
|
|||||||
|
|
||||||
export type EtatChecklistElement = (typeof ETAT_CHECKLIST_ELEMENT)[number];
|
export type EtatChecklistElement = (typeof ETAT_CHECKLIST_ELEMENT)[number];
|
||||||
|
|
||||||
export const STATUT_ANOMALIE = [
|
export const STATUT_ANOMALIE = ['DETECTEE', 'EN_COURS_TRAITEMENT', 'RESOLUE', 'CLOTUREE'] as const;
|
||||||
'DETECTEE',
|
|
||||||
'EN_COURS_TRAITEMENT',
|
|
||||||
'RESOLUE',
|
|
||||||
'CLOTUREE',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type StatutAnomalie = (typeof STATUT_ANOMALIE)[number];
|
export type StatutAnomalie = (typeof STATUT_ANOMALIE)[number];
|
||||||
|
|
||||||
|
|||||||
@@ -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 } } },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { prisma } from '../db/prisma';
|
||||||
|
import { StatutEmprunt } from '../models/enums';
|
||||||
|
|
||||||
|
export type EmpruntAvecMateriel = Prisma.EmpruntGetPayload<{
|
||||||
|
include: { materiel: { include: { categorie: true } } };
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const STATUTS_EN_COURS: StatutEmprunt[] = ['EN_COURS', 'EN_RETARD'];
|
||||||
|
|
||||||
|
export function findEnCoursParUtilisateur(utilisateurId: number): Promise<EmpruntAvecMateriel[]> {
|
||||||
|
return prisma.emprunt.findMany({
|
||||||
|
where: {
|
||||||
|
utilisateurId,
|
||||||
|
statut: { in: STATUTS_EN_COURS },
|
||||||
|
},
|
||||||
|
include: { materiel: { include: { categorie: true } } },
|
||||||
|
orderBy: { dateRetourPrevue: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { prisma } from '../db/prisma';
|
||||||
|
|
||||||
|
export type MaterielAvecCategorie = Prisma.MaterielGetPayload<{ include: { categorie: true } }>;
|
||||||
|
|
||||||
|
export interface CatalogueFiltres {
|
||||||
|
categorieId?: number;
|
||||||
|
recherche?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findDisponiblesParCampus(
|
||||||
|
campusId: number,
|
||||||
|
filtres: CatalogueFiltres,
|
||||||
|
): Promise<MaterielAvecCategorie[]> {
|
||||||
|
const { categorieId, recherche } = filtres;
|
||||||
|
|
||||||
|
return prisma.materiel.findMany({
|
||||||
|
where: {
|
||||||
|
campusId,
|
||||||
|
actif: true,
|
||||||
|
statut: 'DISPONIBLE',
|
||||||
|
...(categorieId !== undefined ? { categorieId } : {}),
|
||||||
|
...(recherche
|
||||||
|
? {
|
||||||
|
OR: [
|
||||||
|
{ nom: { contains: recherche } },
|
||||||
|
{ marque: { contains: recherche } },
|
||||||
|
{ modele: { contains: recherche } },
|
||||||
|
{ reference: { contains: recherche } },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
include: { categorie: true },
|
||||||
|
orderBy: { nom: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
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<UtilisateurAvecRole | null> {
|
||||||
|
return prisma.utilisateur.findUnique({
|
||||||
|
where: { email },
|
||||||
|
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);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { getMesEmprunts } from '../controllers/emprunt.controller';
|
||||||
|
|
||||||
|
export const empruntRoutes: Router = Router();
|
||||||
|
|
||||||
|
empruntRoutes.get('/', getMesEmprunts);
|
||||||
@@ -0,0 +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();
|
||||||
|
|
||||||
|
// /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);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { getCatalogue } from '../controllers/materiel.controller';
|
||||||
|
|
||||||
|
export const materielRoutes: Router = Router();
|
||||||
|
|
||||||
|
materielRoutes.get('/', getCatalogue);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { findEnCoursParUtilisateur, EmpruntAvecMateriel } from '../repositories/emprunt.repository';
|
||||||
|
|
||||||
|
/* RG15 : un étudiant ne consulte que ses propres emprunts, et uniquement ceux
|
||||||
|
non encore restitués (en cours ou en retard). */
|
||||||
|
export function listerMesEmpruntsEnCours(utilisateurId: number): Promise<EmpruntAvecMateriel[]> {
|
||||||
|
return findEnCoursParUtilisateur(utilisateurId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import {
|
||||||
|
findDisponiblesParCampus,
|
||||||
|
CatalogueFiltres,
|
||||||
|
MaterielAvecCategorie,
|
||||||
|
} from '../repositories/materiel.repository';
|
||||||
|
|
||||||
|
/* RG10 : après identification, seuls les matériels disponibles du campus de
|
||||||
|
l'étudiant sont affichés. */
|
||||||
|
export function listerCatalogue(
|
||||||
|
campusId: number,
|
||||||
|
filtres: CatalogueFiltres,
|
||||||
|
): Promise<MaterielAvecCategorie[]> {
|
||||||
|
return findDisponiblesParCampus(campusId, filtres);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export interface AuthenticatedUser {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
roleCode: string;
|
||||||
|
campusId: number;
|
||||||
|
}
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { AuthenticatedUser } from './authenticated-user';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
user?: AuthenticatedUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": false,
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
@@ -2,16 +2,15 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"rootDir": "./src",
|
|
||||||
"outDir": "./dist",
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"types": ["node"]
|
"types": ["node"],
|
||||||
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*", "prisma/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@ Ces décisions s'appliquent à tout le projet, sauf mention contraire.
|
|||||||
| ERD tenu à jour en miroir du schéma (`docs/conceptions/uml/ERD.md`) | La source de vérité doit refléter le code réel (nullable, `UK`, etc.). |
|
| ERD tenu à jour en miroir du schéma (`docs/conceptions/uml/ERD.md`) | La source de vérité doit refléter le code réel (nullable, `UK`, etc.). |
|
||||||
| Workflow Git : commits sur `develop` uniquement | `master` est figé ; `develop` reste toujours à jour de `master` (aucune divergence puisque master ne bouge plus). |
|
| Workflow Git : commits sur `develop` uniquement | `master` est figé ; `develop` reste toujours à jour de `master` (aucune divergence puisque master ne bouge plus). |
|
||||||
| Process avant chaque migration : `prisma format` -> `prisma validate` -> `npm run build` -> migration | Détecter toute erreur de schéma/typage avant de toucher la base. |
|
| Process avant chaque migration : `prisma format` -> `prisma validate` -> `npm run build` -> migration | Détecter toute erreur de schéma/typage avant de toucher la base. |
|
||||||
|
| DTO nommés `XxxResponse` (sortie) et `XxxRequest` (entrée) | Le sens est explicite dans le nom. Sortie = mapping (`toXxxResponse`), entrée = validation. |
|
||||||
|
|
||||||
### Note outillage — migrations en environnement non-interactif
|
### Note outillage — migrations en environnement non-interactif
|
||||||
|
|
||||||
@@ -45,26 +46,26 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
|
|||||||
- Defaults + timestamps auto ajoutés sur Campus, Utilisateur, CategorieMateriel (Role n'a pas de timestamps -> non modifié).
|
- Defaults + timestamps auto ajoutés sur Campus, Utilisateur, CategorieMateriel (Role n'a pas de timestamps -> non modifié).
|
||||||
- `@unique` sur `Role.code`, `Utilisateur.microsoftId`, `Utilisateur.email`.
|
- `@unique` sur `Role.code`, `Utilisateur.microsoftId`, `Utilisateur.email`.
|
||||||
- `Utilisateur.classe` -> nullable : un `RESPONSABLE` n'a pas de classe (seuls les étudiants en ont).
|
- `Utilisateur.classe` -> nullable : un `RESPONSABLE` n'a pas de classe (seuls les étudiants en ont).
|
||||||
- ERD mis à jour en miroir (`UK`, `nullable`).
|
- Ajout dans l'ERD des annotations `UK` (sur `code`, `microsoft_id`, `email`) et `nullable` (sur `classe`).
|
||||||
- Choix laissé de côté : tailles `@db.NVarChar(n)` (colonnes restées en `NVARCHAR(1000)` par défaut). À reconsidérer plus tard.
|
- Choix laissé de côté : tailles `@db.NVarChar(n)` (colonnes restées en `NVARCHAR(1000)` par défaut). À reconsidérer plus tard.
|
||||||
|
|
||||||
### Étape 3 — Bloc A : Infrastructure (SallePret, PosteEmprunt) — commit `d026b75`
|
### Étape 3 — Bloc A : Infrastructure (SallePret, PosteEmprunt) — commit `d026b75`
|
||||||
- `SallePret` rattachée à `Campus` ; `PosteEmprunt` rattaché à `SallePret`. Conforme à l'ERD.
|
- `SallePret` rattachée à `Campus` (FK `campus_id`) ; `PosteEmprunt` rattaché à `SallePret` (FK `salle_pret_id`).
|
||||||
|
|
||||||
### Étape 4 — Bloc B : CarteEtudiante — commit `dfa73cd`
|
### Étape 4 — Bloc B : CarteEtudiante — commit `dfa73cd`
|
||||||
- Rattachée à `Utilisateur`.
|
- Rattachée à `Utilisateur`.
|
||||||
- Dette notée : `qr_code` devrait être `@unique` (sert à l'identification, RG02) — non posé pour rester fidèle à l'ERD.
|
- Dette notée : `qr_code` devrait être `@unique` (sert à l'identification, RG02) — non posé pour rester fidèle à l'ERD.
|
||||||
|
|
||||||
### Étape 5 — Bloc C : Matériel (Materiel, Accessoire, MaterielAccessoire) — commit `0774ee3`
|
### Étape 5 — Bloc C : Matériel (Materiel, Accessoire, MaterielAccessoire) — commit `0774ee3`
|
||||||
- Table de liaison `MaterielAccessoire` (sans timestamps, conforme ERD).
|
- Table de liaison `MaterielAccessoire` entre `Materiel` et `Accessoire`, sans colonnes `created_at`/`updated_at`.
|
||||||
- Dette notée : `@@unique([materielId, accessoireId])` et `Materiel.statut @default("DISPONIBLE")`.
|
- Dette notée : `@@unique([materielId, accessoireId])` et `Materiel.statut @default("DISPONIBLE")`.
|
||||||
|
|
||||||
### Étape 6 — Bloc D : Transactions (Emprunt, Checklist, ChecklistElement) — commit `6908a2b`
|
### Étape 6 — Bloc D : Transactions (Emprunt, Checklist, ChecklistElement) — commit `6908a2b`
|
||||||
- Champs de retour/optionnels nullable : `dateRetourReelle`, `modeIdentificationRetour`, `commentaireDepart`, `commentaireRetour`, `Checklist.commentaire`, `ChecklistElement.commentaire`.
|
- Champs de retour/optionnels nullable : `dateRetourReelle`, `modeIdentificationRetour`, `commentaireDepart`, `commentaireRetour`, `Checklist.commentaire`, `ChecklistElement.commentaire`.
|
||||||
- Pourquoi : RG12 crée l'emprunt au départ ; les infos de retour n'existent pas encore. En `NOT NULL`, la création serait impossible.
|
- Pourquoi : RG12 crée l'emprunt au départ ; les infos de retour n'existent pas encore. En `NOT NULL`, la création serait impossible.
|
||||||
- `ChecklistElement.accessoireId` nullable : conforme au diagramme de classes (`Accessoire "0..1"`), un élément peut être un libellé libre sans accessoire catalogué.
|
- `ChecklistElement.accessoireId` rendu nullable : un élément de checklist peut être un libellé libre, sans accessoire catalogué associé (cardinalité 0..1).
|
||||||
- Les 9 FK en `NoAction` : traçabilité + évitement des cascade paths.
|
- Les 9 FK en `NoAction` : traçabilité + évitement des cascade paths.
|
||||||
- ERD mis à jour en miroir (champs annotés `nullable`).
|
- Annotation `nullable` ajoutée dans l'ERD sur les champs de retour/commentaires d'`Emprunt` et `accessoire_id`/`commentaire` de checklist.
|
||||||
|
|
||||||
### Étape 7 — Stratégie Git : branche `develop`
|
### Étape 7 — Stratégie Git : branche `develop`
|
||||||
- Création de `develop` depuis `master`. Désormais tous les commits vont sur `develop` ; `master` figé au Bloc C.
|
- Création de `develop` depuis `master`. Désormais tous les commits vont sur `develop` ; `master` figé au Bloc C.
|
||||||
@@ -75,8 +76,8 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
|
|||||||
- Double relation `Anomalie` vers `Utilisateur` nommée : `@relation("AnomalieEtudiant")` (étudiant concerné) et `@relation("AnomalieResponsable")` (responsable qui traite).
|
- Double relation `Anomalie` vers `Utilisateur` nommée : `@relation("AnomalieEtudiant")` (étudiant concerné) et `@relation("AnomalieResponsable")` (responsable qui traite).
|
||||||
- Defaults : `Anomalie.detecteeAutomatiquement @default(true)`, `Notification.lu @default(false)`, `Notification.dateCreation @default(now())`.
|
- Defaults : `Anomalie.detecteeAutomatiquement @default(true)`, `Notification.lu @default(false)`, `Notification.dateCreation @default(now())`.
|
||||||
- Les 12 FK en `NoAction` (traçabilité).
|
- Les 12 FK en `NoAction` (traçabilité).
|
||||||
- `Notification` et `Historique` sans `updatedAt` (conforme ERD).
|
- `Notification` et `Historique` créées sans colonne `updatedAt` (seulement `date_creation`/`created_at`).
|
||||||
- ERD mis à jour en miroir.
|
- Annotation `nullable` ajoutée dans l'ERD sur `traitee_par_id`, `observation`, `date_resolution`, `anomalie_id` et les 5 FK optionnelles d'`Historique`.
|
||||||
|
|
||||||
### Étape 9 — Seeds de référence — commit `2c3a5db`
|
### Étape 9 — Seeds de référence — commit `2c3a5db`
|
||||||
- Fichier `prisma/seed.ts` autonome (adapter MSSQL, lecture directe des variables DB), lancé via `npm run seed` (script ajouté au `package.json`).
|
- Fichier `prisma/seed.ts` autonome (adapter MSSQL, lecture directe des variables DB), lancé via `npm run seed` (script ajouté au `package.json`).
|
||||||
@@ -106,6 +107,48 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
|
|||||||
- Express 5 propage nativement les erreurs des handlers async : pas de wrapper `asyncHandler` nécessaire.
|
- Express 5 propage nativement les erreurs des handlers async : pas de wrapper `asyncHandler` nécessaire.
|
||||||
- Vérifié au runtime : `/health` 200, route inconnue 404 en JSON, requêtes journalisées.
|
- Vérifié au runtime : `/health` 200, route inconnue 404 en JSON, requêtes journalisées.
|
||||||
|
|
||||||
|
### Étape 12 — Block 1 : ESLint + Prettier
|
||||||
|
- ESLint 10 (flat config `eslint.config.mjs`) + typescript-eslint 8 ; Prettier 3 (`.prettierrc.json`).
|
||||||
|
- Règles de qualité strictes encodées : `no-explicit-any`, `no-non-null-assertion`, `explicit-module-boundary-types`, `no-unused-vars` (ignore le préfixe `_`), `prefer-const`. `eslint-config-prettier` désactive les règles en conflit avec Prettier.
|
||||||
|
- Prettier : quotes simples, point-virgule, 100 colonnes, 2 espaces, trailing commas.
|
||||||
|
- Scripts : `lint`, `lint:fix`, `format`, `format:check`.
|
||||||
|
- État : lint vert sur tout le code existant ; formatage appliqué (3 fichiers) ; build OK.
|
||||||
|
|
||||||
|
### Étape 13 — Correctif tooling : type-check des scripts Prisma
|
||||||
|
- Problème : `prisma/seed.ts` et `fixtures.ts` étaient hors du `include` du tsconfig ; l'IDE ne chargeait pas les types Node (`process` non reconnu, 2 erreurs).
|
||||||
|
- Correctif : `tsconfig.json` élargi à `src` + `prisma` en `noEmit` (IDE et `tsc --noEmit`) ; nouveau `tsconfig.build.json` dédié à la compilation `src` -> `dist` (`npm run build`).
|
||||||
|
- Bénéfice : les scripts Prisma sont désormais type-checkés (angle mort comblé).
|
||||||
|
|
||||||
|
### Étape 14 — Block 4 : architecture en couches + 1er endpoint (catalogue matériel)
|
||||||
|
- Mise en place de l'architecture en couches : `routes/` -> `controllers/` -> `services/` -> `repositories/` (un dossier par couche).
|
||||||
|
- Auth simulée temporaire : middleware `current-user` qui résout l'utilisateur via l'en-tête `x-user-email` et pose `req.user` (typé via `types/authenticated-user.ts` + augmentation `Express.Request`). À remplacer par la validation JWT au Block 3.
|
||||||
|
- Endpoint `GET /api/materiels` (catalogue) : applique RG10 (matériels `DISPONIBLE` du campus de l'étudiant), filtres `categorieId` et `q` (recherche nom/marque/modèle/référence).
|
||||||
|
- Format de réponse standardisé : `{ "data": ... }` (cohérent avec `{ "error": ... }`).
|
||||||
|
- Vérifié au runtime : 401 sans en-tête / utilisateur inconnu, 200 avec les disponibles du campus, filtres OK, 400 sur `categorieId` invalide.
|
||||||
|
|
||||||
|
### Étape 15 — Block 4 : endpoint "mes emprunts en cours"
|
||||||
|
- Endpoint `GET /api/mes-emprunts` : retourne les emprunts non restitués (`EN_COURS`, `EN_RETARD`) de l'utilisateur courant, triés par date de retour prévue, avec le matériel et sa catégorie inclus.
|
||||||
|
- RG15 : filtrage par `utilisateurId` (chacun ne voit que ses propres emprunts).
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### Étape 17 — DTO de sortie sur les endpoints existants
|
||||||
|
- Dossier `src/dtos/` : par ressource, une interface `XxxResponse` + un mapper pur `toXxxResponse` (utilisateur, matériel, emprunt).
|
||||||
|
- Mapping effectué dans le controller (couche présentation) ; les services continuent de renvoyer les entités (réutilisables).
|
||||||
|
- Champs internes désormais masqués : `microsoftId`, timestamps, FK brutes, `numeroInventaire`/`numeroSerie`.
|
||||||
|
- Vérifié au runtime sur `/me`, `/materiels`, `/mes-emprunts`.
|
||||||
|
|
||||||
|
### Étape 18 — Durcissement du .gitignore (sécurité)
|
||||||
|
- Trou comblé : `**/.env` ne couvrait pas les variantes (`.env.local`, `.env.production`...). Ajout de `**/.env.*` avec exception `!**/.env.example`.
|
||||||
|
- Préventif : clés/certificats (`*.pem`, `*.key`, `*.crt`, `*.cert`, `*.pfx`, `*.p12`) pour Azure AD/TLS à venir ; dumps de base (`*.bak`, `*.dump`) ; `**/coverage/` ; `*.tsbuildinfo`.
|
||||||
|
- Vérifié : aucun secret n'était déjà suivi ; les vrais `.env` restent ignorés ; `.env.example` (placeholders) reste versionné.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dette technique en attente
|
## Dette technique en attente
|
||||||
@@ -116,8 +159,9 @@ 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Dernière mise à jour : 2026-06-15 — Middlewares backend (gestion d'erreurs, logs) en place.*
|
*Dernière mise à jour : 2026-06-22 — Durcissement .gitignore (sécurité secrets).*
|
||||||
|
|||||||
Reference in New Issue
Block a user