← Retour aux projets
Projet E6

SI Dashboard - Application Web de Gestion du Parc Informatique - Version démo - E6

Application web de gestion du parc informatique avec authentification sécurisée.

Next.jsTypeScriptMySQLbcryptDocker
⚠️ Version démo — Ce projet est présenté ici dans une version de démonstration autonome et sans dépendance à l'infrastructure interne de l'entreprise. Dans la version réelle déployée chez SYMETRIE, l'authentification s'appuie sur LDAP / Active Directory et les groupes utilisateurs sont récupérés via l'attribut memberOf. Pour cette démonstration publique, le module LDAP a été remplacé par une authentification locale via la base MySQL avec mots de passe hachés en bcrypt, et les groupes d'accès sont stockés sous forme de colonnes booléennes dans la table utilisateurs. Les données affichées sont fictives et un compte de démonstration demo / demo permet de tester l'application.

Conception et développement d'une application web full-stack permettant la gestion centralisée du parc informatique et de l'annuaire utilisateurs de l'entreprise SYMETRIE. Cette application offre une interface moderne et sécurisée pour visualiser, filtrer et administrer les ressources IT (utilisateurs, ordinateurs, téléphones) avec un système d'authentification et de contrôle d'accès basé sur les groupes.


Contexte et problématique métier

Le service informatique de SYMETRIE gérait son parc informatique et son annuaire utilisateurs de manière fragmentée :

  • Active Directory — Gestion des comptes et groupes utilisateurs (remplacé ici par une auth locale MySQL)
  • Base MySQL — Inventaire des ordinateurs et téléphones
  • Fichiers Excel — Suivi manuel des attributions

Problèmes identifiés :

  • Pas de vue consolidée des utilisateurs et de leur équipement
  • Difficultés pour identifier les ordinateurs non assignés
  • Gestion des numéros de téléphone (postes et mobiles) dispersée
  • Absence de contrôle d'accès pour les modifications
  • Aucun export centralisé pour les audits

Objectifs du projet :

  • Centraliser la visualisation des utilisateurs avec leurs ordinateurs et téléphones associés
  • Permettre l'assignation/désassignation des ordinateurs aux utilisateurs
  • Implémenter une authentification sécurisée avec gestion de session par cookies signés (HMAC-SHA256)
  • Restreindre les modifications aux membres du groupe Service Informatique (RBAC)
  • Fournir un export PDF de l'annuaire filtré
  • Offrir des filtres avancés par activité, type, groupes AD

Architecture technique globale

L'application suit une architecture Next.js App Router full-stack avec séparation claire entre le frontend React et les API Routes backend :

Stack technologique
CoucheTechnologiesRôle
FrontendReact 19, TypeScript, CSS ModulesInterface utilisateur réactive et typée
FrameworkNext.js 16 (App Router)SSR, API Routes, routing file-based
Backend APINext.js Route HandlersEndpoints REST sécurisés
Base de donnéesMySQL via mysql2Stockage des utilisateurs, ordinateurs, téléphones
Authentificationbcryptjs + cookies signés HMAC-SHA256Vérification du mot de passe haché et session signée (en prod : ldapts / Active Directory)
ValidationZodValidation de schémas côté serveur
ExportjsPDF, jspdf-autotableGénération de PDF côté client
ConteneurisationDocker, docker-composeDéploiement multi-stage optimisé
Architecture applicative
┌─────────────────────────────────────────────────────────────────────────┐
│                         NAVIGATEUR CLIENT                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌──────────────┐      ┌──────────────┐      ┌──────────────┐          │
│   │   Annuaire   │      │  Ordinateurs │      │    Login     │          │
│   │    Page      │      │     Page     │      │    Modal     │          │
│   └──────┬───────┘      └──────┬───────┘      └──────┬───────┘          │
│          │                     │                     │                   │
└──────────┼─────────────────────┼─────────────────────┼───────────────────┘
           │ fetch()             │ fetch()             │ POST
           ▼                     ▼                     ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                      NEXT.JS API ROUTES                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌──────────────┐      ┌──────────────┐      ┌──────────────┐          │
│   │  /api/       │      │  /api/       │      │  /api/auth   │          │
│   │   annuaire   │      │ ordinateurs  │      │ (bcrypt+JWT) │          │
│   └──────┬───────┘      └──────┬───────┘      └──────┬───────┘          │
│          │                     │                     │                   │
│   ┌──────┴─────────────────────┴─────────────────────┴───────┐          │
│   │              MIDDLEWARE (auth-middleware.ts)              │          │
│   │  • requireAuth() - Vérifie les cookies de session         │          │
│   │  • requireGroup() - Vérifie l'appartenance au groupe      │          │
│   │  • Rate Limiter - Protection brute force                  │          │
│   └──────────────────────────────────────────────────────────┘          │
│                                                                          │
└──────────────────────────────────┬──────────────────────────────────────┘
                                   │
                                   ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                            MySQL (Données)                               │
│  ┌────────────────┐    ┌────────────┐    ┌────────────┐                 │
│  │  utilisateurs  │    │ ordinateurs│    │ telephones │                 │
│  │  (auth+groupes │    │ (parc IT)  │    │ (postes/   │                 │
│  │   en colonnes) │    │            │    │  mobiles)  │                 │
│  └────────────────┘    └────────────┘    └────────────┘                 │
│                                                                          │
│  Auth : bcrypt sur mot_de_passe + colonnes Glo_* pour les groupes        │
│  (En production : LDAP / Active Directory remplace cette partie)         │
└─────────────────────────────────────────────────────────────────────────┘

Structure du projet

src/
├── app/                          # Next.js App Router
│   ├── layout.tsx                # Layout global (Header)
│   ├── page.tsx                  # Page d'accueil
│   ├── annuaire/page.tsx         # Page annuaire
│   ├── ordinateurs/page.tsx      # Page ordinateurs
│   ├── unauthorized/page.tsx     # Page accès refusé
│   └── api/                      # Route Handlers (API REST)
│       ├── auth/route.ts         # POST - Authentification (bcrypt)
│       ├── session/route.ts      # GET  - Vérification session
│       ├── health/route.ts       # GET  - Health check
│       ├── annuaire/route.ts     # GET  - Liste utilisateurs
│       ├── ordinateurs/          # GET/PUT - Gestion ordinateurs
│       │   ├── route.ts
│       │   └── annuaire/route.ts # GET  - Liste filtrée + facettes
│       ├── telephones/route.ts   # GET/PUT - Gestion téléphones
│       └── utilisateurs/[id]/mobile/route.ts # PUT - Mise à jour mobile
│
├── components/                   # Composants React
│   ├── auth/LoginModal.tsx       # Modal de connexion
│   ├── common/                   # Header, HubGrid, SortableHeader…
│   └── si/
│       ├── annuaire/             # Tableau, filtres, modals, export
│       └── ordinateurs/          # Tableau et filtres ordinateurs
│
├── libs/                         # Librairies internes
│   ├── db.ts                     # Pool de connexions MySQL
│   ├── api-wrapper.ts            # withErrorHandler + ApiError
│   └── cookie-signer.ts          # Signature HMAC-SHA256 des cookies
│
├── middleware/                   # Middlewares serveur
│   ├── auth-middleware.ts        # requireAuth / requireGroup
│   └── rate-limiter.ts           # Protection brute force
│
├── services/
│   └── audit-logger.ts           # Journalisation des actions
│
├── hooks/                        # Hooks React
│   ├── useAnnuaire.ts
│   ├── useOrdinateursData.ts
│   └── useLocalStorage.ts
│
├── constants/index.ts            # Constantes (groupes, routes, cookies)
├── validations/index.ts          # Schémas Zod
├── utils/                        # formatters, pdf-helpers, request-helpers
├── types/index.ts                # Types TypeScript partagés
└── styles/                       # CSS Modules

Fonctionnalités développées

1. Authentification sécurisée (version démo)

Dans cette version de démonstration, l'authentification s'appuie sur la base MySQL : le mot de passe est haché avec bcrypt (10 rounds) et la session est matérialisée par des cookies signés HMAC-SHA256. En production, ce flux est remplacé par un binding LDAP sur l'Active Directory.

// src/app/api/auth/route.ts
import bcrypt from "bcryptjs";
import { signCookie } from "@/libs/cookie-signer";

export async function POST(req: NextRequest) {
  return withErrorHandler(async () => {
    // 1. Rate limiting par IP (5 tentatives / 15 min)
    const clientIp = getClientIp(req.headers);
    if (!authRateLimiter.check(clientIp)) {
      auditLogger.logRateLimited(clientIp, API_ROUTES.AUTH);
      throw new ApiError(ERROR_MESSAGES.RATE_LIMITED(...), 429);
    }

    // 2. Validation Zod du body
    const parsed = AuthSchema.safeParse(await req.json());
    if (!parsed.success) throw new ApiError("Identifiants requis", 400);
    const { username, password } = parsed.data;

    // 3. Lecture utilisateur + comparaison bcrypt
    const [rows] = await getPool().query<RowDataPacket[]>(
      "SELECT *, mot_de_passe FROM utilisateurs WHERE samaccountname = ?",
      [username]
    );
    const dbUser = rows[0];
    if (!dbUser || !(await bcrypt.compare(password, dbUser.mot_de_passe))) {
      auditLogger.logAuthFailed(username, clientIp);
      throw new ApiError("Identifiants invalides", 401);
    }

    // 4. Reconstruction des groupes depuis les colonnes Glo_* de la DB
    const groups: string[] = [];
    for (const { column, group } of DB_GROUP_COLUMNS) {
      if (flagOn(dbUser[column])) groups.push(group);
    }

    // 5. Cookies signés HMAC-SHA256 (httpOnly, secure, sameSite=strict)
    const response = NextResponse.json({ ok: true, username, groupes: groups });
    response.cookies.set("auth_token", signCookie(username), COOKIE_OPTS);
    response.cookies.set("auth_groups", signCookie(JSON.stringify(groups)), COOKIE_OPTS);
    return response;
  })(req);
}
2. Middleware d'authentification et contrôle d'accès
// src/middleware/auth-middleware.ts
import { unsignCookie } from "@/libs/cookie-signer";

export interface AuthContext {
  username: string;
  groups: string[];
  ip: string;
}

// Lit et vérifie les cookies signés (HMAC-SHA256)
export function parseSessionFromCookies(req: NextRequest) {
  const cookies = parseCookies(req);
  const username = cookies["auth_token"]
    ? unsignCookie(cookies["auth_token"])  // null si signature invalide
    : null;
  const groupsJson = cookies["auth_groups"]
    ? unsignCookie(cookies["auth_groups"])
    : null;
  const groups = groupsJson ? JSON.parse(groupsJson) : [];
  return { username, groups };
}

// Vérifie que l'utilisateur est authentifié
export function requireAuth(req: NextRequest): AuthContext {
  const { username, groups } = parseSessionFromCookies(req);
  if (!username) throw new ApiError("Non autorisé", 401);
  return { username, groups, ip: getClientIp(req.headers) };
}

// Vérifie l'appartenance à au moins un des groupes requis (RBAC)
export function requireGroup(req: NextRequest, allowed: string[]): AuthContext {
  const auth = requireAuth(req);
  if (!auth.groups.some(g => allowed.includes(g))) {
    throw new ApiError("Accès interdit", 403);
  }
  return auth;
}
3. API Annuaire avec jointures multiples
// src/app/api/annuaire/route.ts
export async function GET(req: NextRequest) {
  return withErrorHandler(async () => {
    requireAuth(req);  // Vérification authentification

    const pool = getPool();
    const [rows] = await pool.execute<RowDataPacket[]>(
      `SELECT ${USER_COLUMNS_NO_PASSWORD} FROM utilisateurs ORDER BY nom, prenom`
    );

    // Normalisation des flags binaires (BIT(1) MySQL)
    const normalized = rows.map((r) => ({
      ...r,
      isStagiaire: flagOn(r["Glo_Stagiaire"]),
    }));

    return NextResponse.json(normalized);
  })(req);
}
4. Gestion des ordinateurs avec filtres dynamiques
// src/app/api/ordinateurs/annuaire/route.ts
export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const type = searchParams.get("type");      // Station, Serveur, etc.
  const status = searchParams.get("status");  // assigned, non-assigned
  const os = searchParams.get("os");          // Windows 10, Windows 11...
  const sortBy = searchParams.get("sortBy");
  const sortDir = searchParams.get("sortDir");

  // Construction dynamique de la requête SQL
  let query = `
    SELECT o.*, u.prenom, u.nom as nom_utilisateur, u.activite
    FROM ordinateurs o
    LEFT JOIN utilisateurs u ON o.utilisateur_id = u.id
    WHERE 1=1
  `;
  const params: any[] = [];

  if (type) {
    query += " AND o.type = ?";
    params.push(type);
  }
  if (status === "assigned") {
    query += " AND o.utilisateur_id IS NOT NULL";
  } else if (status === "non-assigned") {
    query += " AND o.utilisateur_id IS NULL";
  }
  // ... autres filtres

  const [rows] = await pool.execute(query, params);
  
  // Calcul des facettes pour les filtres
  const facets = {
    types: [...new Set(rows.map(r => r.type))],
    osList: [...new Set(rows.map(r => r.systeme_exploitation))],
    stats: {
      total: rows.length,
      assigned: rows.filter(r => r.utilisateur_id).length,
      nonAssigned: rows.filter(r => !r.utilisateur_id).length
    }
  };

  return NextResponse.json({ computers: rows, facets });
}
5. Export PDF avec jsPDF-AutoTable
// src/components/si/annuaire/ExportPdfButton.tsx
import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";

function exportToPdf(data: Utilisateur[], phonesByUserId: Record<number, Telephone[]>) {
  const doc = new jsPDF({ orientation: "landscape" });
  
  // En-tête
  doc.setFontSize(18);
  doc.text("Annuaire SYMETRIE", 14, 20);
  doc.setFontSize(10);
  doc.text(`Généré le ${new Date().toLocaleDateString("fr-FR")}`, 14, 28);

  // Préparation des données
  const tableData = data.map(user => {
    const phones = phonesByUserId[user.id] || [];
    const postes = phones.map(p => p.poste).filter(Boolean).join(", ");
    const lignes = phones.map(p => p.lignes_internes).filter(Boolean).join(", ");
    
    return [
      user.trigramme,
      user.prenom,
      user.nom,
      user.samaccountname,
      user.mobiles || "",
      postes,
      lignes,
      user.activite
    ];
  });

  // Génération du tableau
  autoTable(doc, {
    head: [["Trigramme", "Prénom", "Nom", "Login", "Mobile", "Poste", "Ligne", "Statut"]],
    body: tableData,
    startY: 35,
    styles: { fontSize: 8 },
    headStyles: { fillColor: [41, 128, 185] }
  });

  doc.save("annuaire-symetrie.pdf");
}

Sécurité implémentée

Rate Limiting
// src/middleware/rate-limiter.ts
class RateLimiter {
  private attempts: Map<string, { count: number; resetTime: number }> = new Map();
  
  check(ip: string): boolean {
    const now = Date.now();
    const record = this.attempts.get(ip);
    
    if (!record || now > record.resetTime) {
      this.attempts.set(ip, { count: 1, resetTime: now + 60000 }); // 1 minute
      return true;
    }
    
    if (record.count >= 5) {  // Max 5 tentatives par minute
      return false;
    }
    
    record.count++;
    return true;
  }
}
Cookies sécurisés
OptionValeurProtection
httpOnlytrueProtection XSS (non accessible via JavaScript)
securetrueTransmission HTTPS uniquement
sameSitestrictProtection CSRF
maxAge28800 (8h)Expiration automatique de session

Conteneurisation Docker

Dockerfile (Multi-stage build)
# Stage 1: Installation des dépendances
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# Stage 2: Build de l'application
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Stage 3: Image de production optimisée
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT=3000

CMD ["node", "server.js"]
docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: si-dashboard
    ports:
      - "8080:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=mysql-server
      - DB_PORT=3306
      - DB_NAME=si_database
      - LDAP_URL=ldap://domaine.local
    restart: unless-stopped

Interface utilisateur

Page Annuaire — Fonctionnalités
  • Tableau interactif avec tri par colonne (nom, prénom, trigramme...)
  • Filtres latéraux :
    • Par activité (Actif / Inactif)
    • Par type (Utilisateur / Stagiaire)
    • Par groupes AD (sélection multiple)
  • Barre de recherche full-text sur tous les champs
  • Affichage inline des ordinateurs et téléphones
  • Actions conditionnelles (SI uniquement) :
    • Assigner/Désassigner un ordinateur
    • Modifier les téléphones (postes, lignes internes)
    • Modifier le numéro de mobile
  • Export PDF avec filtres appliqués
  • Persistance des filtres via localStorage
Composants React principaux
ComposantResponsabilité
AnnuaireTableRendu du tableau avec colonnes dynamiques (groupes AD)
FiltersSidebarPanneau latéral avec checkboxes de filtrage
ToolbarBarre de recherche et compteur de résultats
AssignPcModalModal de sélection d'ordinateur à assigner
PhonesEditorModalÉdition des téléphones fixes (postes + lignes)
MobileEditorModalÉdition du numéro de mobile
ExportPdfButtonGénération et téléchargement du PDF
LoginModalFormulaire d'authentification

Compétences techniques mobilisées

DomaineTechnologies / Compétences
Développement FrontendReact 19, TypeScript, Hooks (useState, useEffect, useMemo), CSS Modules
Développement BackendNext.js API Routes, REST, Middleware, Gestion des erreurs
Base de donnéesMySQL, Pool de connexions, Requêtes paramétrées, Jointures
Authentificationbcrypt (hachage), Cookies signés HMAC-SHA256, Sessions stateless (LDAP/AD en production)
SécuritéRate limiting, Validation Zod, RBAC (contrôle par groupes), XSS/CSRF
DevOpsDocker multi-stage, docker-compose, Variables d'environnement
UX/UIInterface responsive, Modals, Filtres persistants, Export PDF
Qualité de codeTypeScript strict, ESLint, Architecture modulaire, Types partagés