Aller au contenu

Valeurs sensibles retirées (2026-07-04) — voir l'original (non versionné) dans le projet si besoin des vraies valeurs.


Documentation technique — tekdhi.dev

Rédigée le 2026-05-19. Couvre l'intégralité du projet : frontend React, backend Express DDD, base de données, Docker, Caddy, interface d'administration.


Table des matières

  1. Vue d'ensemble
  2. Structure du projet
  3. Backend — Architecture DDD
  4. Frontend — React + Vite
  5. Base de données
  6. Authentification et sécurité
  7. Upload d'images
  8. Docker
  9. Caddy (reverse proxy)
  10. Interface d'administration
  11. Variables d'environnement
  12. Déploiement
  13. Maintenance — commandes utiles

1. Vue d'ensemble

Qu'est-ce que c'est ?

Site personnel tekdhi.dev avec : - Landing page statique (présentation, compétences, projets, services) - Blog dynamique avec catégories et articles gérés via une API REST - Interface d'administration protégée par email + mot de passe (JWT)

Stack technique

Composant Technologie
Frontend React 18, Vite 5, Tailwind CSS 3, React Router 6
Éditeur de contenu Tiptap 3 (WYSIWYG HTML)
Backend API Express 5, TypeScript strict
ORM Prisma 5
Base de données PostgreSQL 15 (container shared-postgres)
Auth JWT (jsonwebtoken) + bcryptjs
Upload fichiers Multer
Logs Winston (JSON en prod, colorisé en dev)
Validation Zod
Conteneurisation Docker + Docker Compose
Reverse proxy Caddy (container shared-caddy)
Réseau Docker shared_infra (réseau externe partagé entre tous les services du VPS)

Ports et domaines

Service Port local Domaine public
API backend 127.0.0.1:3902 https://blog-api.tekdhi.dev
Frontend (prod) https://tekdhi.dev
Frontend (dev) localhost:5173

2. Structure du projet

/home/kanmaber/projets/tekdhi-site/
├── BackEnd/                    ← API Express + DDD
│   ├── prisma/
│   │   └── schema.prisma       ← Schéma DB (3 modèles)
│   ├── src/
│   │   ├── Modules/
│   │   │   ├── Auth/           ← Authentification admin
│   │   │   ├── Articles/       ← CRUD articles
│   │   │   └── Categories/     ← CRUD catégories
│   │   ├── shared/
│   │   │   ├── domain/         ← ErreurMetier, interfaces
│   │   │   └── infrastructure/
│   │   │       ├── di/         ← DIModule, DIContainer
│   │   │       ├── logger/     ← WinstonLogger
│   │   │       └── middleware/ ← authentifierAdmin, uploadImage
│   │   ├── scripts/
│   │   │   └── seed-admin.ts   ← Création compte admin
│   │   └── server.ts           ← Point d'entrée Express
│   ├── .env                    ← Variables d'environnement (dev)
│   ├── docker-compose.yml      ← Définition container tekdhi-api
│   ├── Dockerfile              ← Image multi-stage (builder + production)
│   └── package.json
│
├── FrontEnd/                   ← Application React
│   ├── src/
│   │   ├── modules/
│   │   │   ├── shared/         ← DI, repositories, types communs
│   │   │   ├── landing/        ← Sections page d'accueil
│   │   │   ├── about/          ← Page /a-propos
│   │   │   ├── blog/           ← Pages /blog et /blog/:slug
│   │   │   └── admin/          ← Interface d'administration /admin
│   │   ├── App.tsx             ← Routeur principal
│   │   └── main.tsx            ← Point d'entrée React
│   ├── index.html              ← Métadonnées SEO, Open Graph, JSON-LD
│   ├── .env                    ← dev : VITE_API_URL=http://localhost:3902
│   ├── .env.production         ← prod : VITE_API_URL=https://blog-api.tekdhi.dev
│   ├── tailwind.config.js      ← Tokens de couleurs personnalisés
│   └── vite.config.ts          ← Alias @/, code splitting Tiptap/vendor
│
├── docs/
│   └── TECHNIQUE.md            ← Ce fichier
│
└── deploy.sh                   ← Script de déploiement (build + copie)

Répertoire de déploiement frontend

/home/kanmaber/tekdhi-landing/     ← Servi par Caddy comme tekdhi.dev
├── index.html                     ← SPA React (React Router)
├── assets/                        ← JS/CSS buildés (Vite)
│   ├── index-*.js                 ← Code applicatif
│   ├── vendor-*.js                ← React, React DOM, React Router
│   └── tiptap-*.js                ← Éditeur Tiptap (chunk séparé)
└── uploads/                       ← Images uploadées via l'admin

3. Backend — Architecture DDD

Principe

Chaque module suit 5 couches avec dépendances strictement vers l'intérieur :

domain ← application ← infrastructure ← interface-adapters

Structure d'un module

src/Modules/{Nom}/
├── domain/
│   ├── entities/           ← Entité avec comportements + versObjetPlainPublic()
│   ├── interfaces/
│   │   └── repositories/   ← Interface du repository (I{Nom}Repository.ts)
│   ├── exceptions/         ← {Nom}Exceptions.ts — extends ErreurMetier
│   ├── schemas/            ← Constantes string (ex: export const schemaLogin = 'login')
│   └── types/              ← {Nom}DTO.ts — interfaces Donnees*, Resultat*
├── application/
│   ├── interfaces/         ← I{Action}UseCase.ts
│   └── usecases/           ← {Action}UseCase.ts — pattern 5 étapes
├── infrastructure/
│   ├── di/                 ← {Nom}Module.ts — extends DIModule
│   ├── repositories/
│   │   └── prisma/         ← {Nom}RepositoryPrisma.ts
│   ├── schemas/
│   │   └── zod/            ← Vrais schémas Zod (validation technique)
│   └── services/
│       └── validation/     ← ServiceValidation{Nom}.ts
├── interface-adapters/
│   ├── controllers/        ← {Action}Controller.ts + {Nom}ModuleController.ts
│   └── handlers/           ← {Action}Handler.ts — extends BaseActionHandler
└── index.ts

Pattern use case — 5 étapes obligatoires

async executer(donnees: DonneesXxx): Promise<ResultatXxx> {
  try {
    // ÉTAPE 1 : Validation Zod + règles métier
    const validation = await this.serviceValidation.validerAvecReglesMetier(schema, donnees);
    if (!validation.succes) throw new ErreurXxx('Données invalides', 'DONNEES_INVALIDES');

    // ÉTAPE 2 : Logique métier (méthode privée)
    const resultat = await this.executerLogique(donnees);

    // ÉTAPE 3 : Audit (non-bloquant — .catch() séparé)
    this.auditer(donnees, resultat).catch((e) => this.logger.warn('Échec audit', { error: String(e) }));

    // ÉTAPE 4 : Construction résultat
    return this.construireResultat(resultat);
  } catch (error) {
    // ÉTAPE 5 : Gestion erreurs — retourne TOUJOURS ResultatXxx, jamais re-throw
    return this.gererErreur(error, donnees);
  }
}

Exceptions

// Toutes les exceptions héritent de ErreurMetier, jamais de Error directement
export class ErreurArticle extends ErreurMetier {
  public readonly type: ErreurTypeArticle;
  constructor(message: string, type: ErreurTypeArticle) {
    super(message);
    this.name = 'ErreurArticle';   // obligatoire
    this.type = type;
    Error.captureStackTrace(this, this.constructor);  // obligatoire
  }
}

Format de réponse HTTP standard

// Succès
{ "succes": true, "message": "OK", "data": { ... }, "timestamp": "2026-..." }

// Erreur
{ "succes": false, "code": "CODE_ERREUR", "message": "Description", "timestamp": "2026-..." }

// Liste paginée (data contient items + métadonnées)
{
  "succes": true,
  "data": {
    "items": [...],
    "total": 42,
    "page": 1,
    "limite": 20,
    "totalPages": 3
  }
}

Modules existants

Module Auth (src/Modules/Auth/)

  • Use case : LoginUseCase — vérifie email + mot de passe bcrypt, génère JWT 7 jours
  • Repository : AdminUserRepositoryPrismatrouverParEmail(email)
  • Route : POST /api/v1/auth/login
  • Corps : { email: string, motDePasse: string }
  • Réponse : { data: { token: string, email: string } }

Module Categories (src/Modules/Categories/)

  • 4 use cases : Creer, Lister, Modifier, Supprimer
  • Routes :
  • GET /api/v1/categories — public
  • POST /api/v1/categories — protégé (JWT requis)
  • PUT /api/v1/categories/:id — protégé
  • DELETE /api/v1/categories/:id — protégé

Module Articles (src/Modules/Articles/)

  • 5 use cases : Creer, Lister, ObtenirParSlug, Modifier, Supprimer
  • Routes :
  • GET /api/v1/articles?page=1&limite=20&categorieId=xxx&statut=PUBLIE — public
  • GET /api/v1/articles/:slug — public
  • POST /api/v1/articles — protégé
  • PUT /api/v1/articles/:id — protégé
  • DELETE /api/v1/articles/:id — protégé

Routes utilitaires (définies directement dans server.ts)

Méthode Route Auth Description
GET /api/v1/categories/avec-articles public Catégories ayant au moins 1 article publié (Prisma WHERE EXISTS)
GET /api/v1/admin/articles/:id JWT Article complet par ID (formulaire d'édition admin)
POST /api/v1/articles/:slug/like public Liker un article — 1 like par IP, atomique via $transaction
POST /api/v1/visites public Enregistrer une visite (ip, page, userAgent)
GET /api/v1/admin/stats JWT Stats : totalVisites, visitesUniques, top pages, top articles
POST /api/v1/admin/upload JWT Upload image (Multer, multipart, champ image)

4. Frontend — React + Vite

Couleurs Tailwind (tokens personnalisés)

Définis dans tailwind.config.js :

colors: {
  deep:    '#0c1827',   // fond principal
  mid:     '#112236',   // fond intermédiaire
  surface: '#1a2e45',   // cartes, panels
  accent:  '#2b7de9',   // bleu accent
  gold:    '#c89a2a',   // or
  green:   '#2bc898',   // vert
  white:   '#ffffff',
  text:    '#c0d4ea',   // texte principal
  muted:   '#6b8fad',   // texte secondaire
}

Modules frontend

modules/shared/

  • infrastructure/http/ApiHttpClient.ts — client fetch wrappé
  • infrastructure/repositories/ApiArticleRepository.ts — accès articles public
  • infrastructure/repositories/ApiCategorieRepository.ts — accès catégories public
  • infrastructure/di/DIContainer.ts — câblage repositories
  • infrastructure/di/DIProvider.tsx — contexte React + hook useDI()
  • infrastructure/hooks/useTrackVisite.ts — hook React qui enregistre une visite à chaque changement de route (useLocation)

modules/landing/

Sections de la page d'accueil : - HeroSection — titre, tagline, boutons - CompetencesSection — grille de compétences techniques - ChiffresSection — stats + engagements - ProjetsSection — projets actifs - CitationSection — citation Paul VI - ClaudeVoitSection — bloc "Ce que Claude voit" - ServicesSection — services publics + protégés

modules/about/

Page /a-propos : portrait + ClaudeVoitSection

modules/blog/

  • BlogPage — liste paginée, filtre par catégorie via useSearchParams()
  • ArticlePage — affichage HTML via dangerouslySetInnerHTML + BoutonLike
  • CarteArticle — carte article avec tag catégorie coloré + BoutonLike
  • FiltreCategories — pills de filtrage (n'affiche que les catégories avec articles publiés)
  • BoutonLike — bouton cœur SVG avec compteur ; localStorage pour l'état UI, backend pour la contrainte réelle (1 like/IP)

modules/admin/

Voir section 10.

Code splitting Vite

// vite.config.ts
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        vendor: ['react', 'react-dom', 'react-router-dom'],
        tiptap: ['@tiptap/react', '@tiptap/starter-kit', '@tiptap/extension-image',
                 '@tiptap/extension-link', '@tiptap/extension-placeholder'],
      },
    },
  },
},

Résultat du build de production : - index-*.js : ~63 KB (code applicatif) - vendor-*.js : ~164 KB (React) - tiptap-*.js : ~383 KB (éditeur — chargé uniquement sur /admin) - index-*.css : ~21 KB


5. Base de données

Connexion

  • Hôte (depuis l'hôte) : localhost:5432
  • Hôte (depuis Docker) : shared-postgres:5432
  • Base : tekdhi_blog
  • Utilisateur : tekdhi_user
  • Mot de passe : [REDACTED]
  • Container PostgreSQL : shared-postgres (superuser : psycho_user, pas postgres)

Schéma Prisma

// prisma/schema.prisma
// Commande de migration : npx prisma db push (JAMAIS prisma migrate dev — shadow DB absente)

model Categorie {
  id               String    @id @default(cuid())
  nom              String    @unique
  slug             String    @unique
  description      String?
  couleur          String    @default("#2b7de9")
  ordreAffichage   Int       @default(0)
  createdAt        DateTime  @default(now())
  updatedAt        DateTime  @updatedAt
  articles         Article[]
  @@map("categories")
}

model Article {
  id          String         @id @default(cuid())
  titre       String
  slug        String         @unique
  contenu     String         @db.Text        // HTML produit par Tiptap
  extrait     String
  imageUrl    String?
  statut      StatutArticle  @default(BROUILLON)  // BROUILLON | PUBLIE | ARCHIVE
  likes       Int            @default(0)     // compteur dénormalisé pour performance
  categorieId String
  createdAt   DateTime       @default(now())
  updatedAt   DateTime       @updatedAt
  publieLe    DateTime?
  categorie   Categorie      @relation(fields: [categorieId], references: [id])
  artLikes    ArticleLike[]
  @@map("articles")
}

model ArticleLike {
  id        String   @id @default(cuid())
  articleId String
  ip        String
  createdAt DateTime @default(now())
  article   Article  @relation(fields: [articleId], references: [id], onDelete: Cascade)
  @@unique([articleId, ip])   // 1 like par IP par article (contrainte DB)
  @@map("article_likes")
}

model Visite {
  id        String   @id @default(cuid())
  ip        String
  page      String
  userAgent String?
  createdAt DateTime @default(now())
  @@map("visites")
}

model AdminUser {
  id            String   @id @default(cuid())
  email         String   @unique
  passwordHash  String   // bcrypt, cost 12
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
  @@map("admin_users")
}

enum StatutArticle { BROUILLON  PUBLIE  ARCHIVE }

IDs

Tous les IDs sont des CUIDs (@paralleldrive/cuid2), jamais des UUIDs. Ne jamais valider avec z.string().uuid() — utiliser z.string().min(1).


6. Authentification et sécurité

Flux de connexion

POST /api/v1/auth/login
Body: { email, motDePasse }
  → LoginUseCase
    → AdminUserRepositoryPrisma.trouverParEmail(email)
    → bcrypt.compare(motDePasse, passwordHash)
    → jwt.sign({ id, email }, JWT_SECRET, { expiresIn: '7d' })
  ← { data: { token, email } }

Middleware JWT (src/shared/infrastructure/middleware/authentifierAdmin.ts)

// Appliqué sur toutes les routes d'écriture
// Lit le header : Authorization: Bearer <token>
// Décline avec 401 si absent ou invalide
// Injecte req.utilisateur = { id, email } si valide

Routes protégées vs publiques

GET  /api/v1/categories                  → PUBLIC
GET  /api/v1/categories/avec-articles    → PUBLIC
GET  /api/v1/articles                    → PUBLIC
GET  /api/v1/articles/:slug              → PUBLIC
POST /api/v1/articles/:slug/like         → PUBLIC
POST /api/v1/visites                     → PUBLIC
POST /api/v1/auth/login                  → PUBLIC

POST   /api/v1/categories                → PROTÉGÉ (JWT)
PUT    /api/v1/categories/:id            → PROTÉGÉ
DELETE /api/v1/categories/:id            → PROTÉGÉ
POST   /api/v1/articles                  → PROTÉGÉ
PUT    /api/v1/articles/:id              → PROTÉGÉ
DELETE /api/v1/articles/:id              → PROTÉGÉ
GET    /api/v1/admin/articles/:id        → PROTÉGÉ
GET    /api/v1/admin/stats               → PROTÉGÉ
POST   /api/v1/admin/upload              → PROTÉGÉ

Compte admin

  • Email : bernardkanmdev@gmail.com
  • Mot de passe : [REDACTED]
  • Hash bcrypt cost 12 stocké en DB dans admin_users
  • JWT expire au bout de 7 jours
  • Token stocké dans localStorage clé tekdhi_admin_token

Rate limiting

Rate limiter global Express : - Fenêtre : 15 minutes - Max requêtes : 200 par IP - Headers standards (RateLimit-*) activés


7. Upload d'images

Backend (src/shared/infrastructure/middleware/uploadImage.ts)

  • Librairie : Multer
  • Stockage : disque local → /home/kanmaber/tekdhi-landing/uploads/
  • Formats acceptés : JPEG, PNG, WebP, GIF
  • Taille maximale : 5 MB
  • Nom du fichier : {timestamp}-{nomOriginal} (espaces et caractères spéciaux supprimés)
  • Route : POST /api/v1/admin/upload (multipart/form-data, champ image)
  • Réponse : { succes: true, url: "https://tekdhi.dev/uploads/filename.jpg" }

Accès public

Les images uploadées sont servies directement par Caddy depuis /home/kanmaber/tekdhi-landing/ :

Upload → /home/kanmaber/tekdhi-landing/uploads/1234567890-photo.jpg
URL    → https://tekdhi.dev/uploads/1234567890-photo.jpg

8. Docker

Container tekdhi-api

docker-compose.yml

services:
  tekdhi-api:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: tekdhi-api
    restart: unless-stopped
    environment:
      NODE_ENV: production
      DATABASE_URL: postgresql://tekdhi_user:${TEKDHI_DB_PASSWORD}@shared-postgres:5432/tekdhi_blog
      PORT: 3902
      LOG_LEVEL: info
      CORS_ORIGIN: https://tekdhi.dev
      JWT_SECRET: ${JWT_SECRET}   # à ajouter
    ports:
      - "127.0.0.1:3902:3902"    # exposé uniquement en local (Caddy fait le proxy)
    networks:
      - shared_infra

networks:
  shared_infra:
    external: true
    name: shared_infra

Important : docker-compose lit automatiquement .env dans le même dossier pour les variables ${VAR}.

Dockerfile (multi-stage)

# Stage 1 : Build TypeScript
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY prisma ./prisma/
RUN npx prisma generate
COPY tsconfig.json ./
COPY src ./src/
RUN npm run build          # tsc  dist/

# Stage 2 : Production
FROM node:22-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
RUN apk add --no-cache openssl    #  REQUIS par Prisma sur Alpine
COPY package.json package-lock.json* ./
RUN npm ci --only=production
COPY prisma ./prisma/
RUN npx prisma generate
COPY --from=builder /app/dist ./dist/
EXPOSE 3902
CMD ["node", "dist/server.js"]

Attention : openssl est obligatoire dans l'image Alpine pour que Prisma puisse se connecter à PostgreSQL. Sans ça, Prisma lève PrismaClientInitializationError au démarrage.

Lancement manuel (sans docker-compose — méthode actuelle en production)

Le container est démarré manuellement avec docker run car docker-compose ne gère pas bien la connexion au réseau shared_infra externe dans cet environnement :

docker run -d --name tekdhi-api \
  --network shared_infra \
  --restart unless-stopped \
  -p 127.0.0.1:3902:3902 \
  -e NODE_ENV=production \
  -e DATABASE_URL="postgresql://tekdhi_user:[REDACTED]@shared-postgres:5432/tekdhi_blog" \
  -e PORT=3902 \
  -e LOG_LEVEL=info \
  -e CORS_ORIGIN=https://tekdhi.dev \
  -e JWT_SECRET=[REDACTED] \
  backend-tekdhi-api:latest

Réseau Docker partagé (shared_infra)

Réseau bridge externe utilisé par tous les services du VPS :

Container IP Rôle
shared-postgres 172.21.0.5 PostgreSQL (superuser: psycho_user)
shared-redis 172.21.0.6 Redis
shared-caddy Reverse proxy
shared-prometheus 172.21.0.3 Métriques
shared-grafana 172.21.0.2 Dashboard métriques
psycho-backend 172.21.0.7 API PsychoEnLigne
psycho-frontend 172.21.0.8 Frontend PsychoEnLigne
wikijs 172.21.0.4 Wiki.js
tekdhi-blog-api attribuée dynamiquement API tekdhi.dev

9. Caddy (reverse proxy)

Fichier de configuration

/home/kanmaber/shared_infra/Caddyfile

Configuration complète pertinente pour tekdhi.dev

# --- Blog API tekdhi.dev ---
blog-api.tekdhi.dev {
    reverse_proxy 127.0.0.1:3902
}

# --- Landing tekdhi.dev ---
tekdhi.dev {
    root * /home/kanmaber/tekdhi-landing
    try_files {path} /index.html     # ← indispensable pour React Router
    file_server
}

try_files {path} /index.html est critique : sans cette directive, les routes React comme /blog, /a-propos, /admin/articles retournent 404 car Caddy cherche un fichier physique.

Recharger Caddy après modification du Caddyfile

docker restart shared-caddy

Provoque ~1 seconde d'interruption sur tous les sites hébergés. Préférer les heures creuses.

TLS automatique

Caddy gère automatiquement les certificats Let's Encrypt pour tous les domaines déclarés. Email de contact : bernardkanmdev@gmail.com (configuré en tête du Caddyfile).


10. Interface d'administration

Accès

https://tekdhi.dev/admin         → redirige vers /admin/login
https://tekdhi.dev/admin/login   → formulaire de connexion

Identifiants

  • Email : bernardkanmdev@gmail.com
  • Mot de passe : [REDACTED]

Pages disponibles

Route Description
/admin/login Connexion email + mot de passe
/admin/articles Liste de tous les articles (filtres, pagination)
/admin/articles/new Créer un article
/admin/articles/:id/edit Modifier un article
/admin/categories Gérer les catégories

Éditeur Tiptap

L'éditeur produit du HTML stocké tel quel en base. Fonctionnalités : - Gras, italique, barré - Titres H1/H2/H3 - Listes à puces et numérotées - Bloc de code, citation (blockquote) - Lien (prompt URL) - Image (upload → tekdhi.dev/uploads/ ou URL externe) - Séparateur horizontal

Gestion du token JWT

  • Stocké dans localStorage['tekdhi_admin_token']
  • Expire après 7 jours
  • À l'expiration : redirection automatique vers /admin/login

Slug auto-généré

Lors de la création d'un article ou d'une catégorie, le slug est généré automatiquement depuis le titre/nom (minuscules, accents normalisés, espaces → tirets). Modifiable manuellement.


11. Variables d'environnement

Backend — .env (développement et production)

DATABASE_URL="postgresql://tekdhi_user:[REDACTED]@shared-postgres:5432/tekdhi_blog"
PORT=3902
NODE_ENV=development
LOG_LEVEL=info
CORS_ORIGIN=https://tekdhi.dev
TEKDHI_DB_PASSWORD=[REDACTED]
JWT_SECRET=[REDACTED]
ADMIN_EMAIL=bernardkanmdev@gmail.com

Important : DATABASE_URL utilise shared-postgres (nom du container Docker) — pas localhost. localhost ne fonctionne que hors Docker ; dans le container, le réseau shared_infra résout shared-postgres.

Frontend — .env (développement)

VITE_API_URL=http://localhost:3902/api/v1

Frontend — .env.production (production)

VITE_API_URL=https://blog-api.tekdhi.dev/api/v1

Important : VITE_API_URL inclut /api/v1. Les clients HTTP (ApiHttpClient, AdminApiClient) ajoutent directement le chemin de l'endpoint sans ajouter de préfixe supplémentaire.


12. Déploiement

Déployer le frontend

cd /home/kanmaber/projets/tekdhi-site/FrontEnd

# 1. Build de production (lit .env.production)
npm run build

# 2. Copier vers le répertoire servi par Caddy
cp -r dist/. /home/kanmaber/tekdhi-landing/

Caddy détecte les nouveaux fichiers immédiatement — pas besoin de le redémarrer.

Déployer le backend (après modification du code)

cd /home/kanmaber/projets/tekdhi-site/BackEnd

# 1. Supprimer l'ancien container
docker stop tekdhi-blog-api && docker rm tekdhi-blog-api

# 2. Rebuilder l'image
docker build -t tekdhi-blog-api:latest .

# 3. Relancer le container (le .env contient déjà DATABASE_URL avec shared-postgres)
docker run -d \
  --name tekdhi-blog-api \
  --network shared_infra \
  -p 3902:3902 \
  --env-file .env \
  tekdhi-blog-api:latest

Mettre à jour le schéma Prisma

cd /home/kanmaber/projets/tekdhi-site/BackEnd

# ⚠️ Toujours db push — jamais migrate dev (shadow DB absente sur ce VPS)
npx prisma db push

# Régénérer le client (si schéma modifié)
npx prisma generate

13. Maintenance — commandes utiles

Backend

# Lancer en mode développement (avec hot reload)
cd /home/kanmaber/projets/tekdhi-site/BackEnd
npm run dev

# Vérifier TypeScript sans compiler
npx tsc --noEmit

# Créer un compte admin
ADMIN_EMAIL=email@exemple.com ADMIN_PASSWORD_SEED=MonMotDePasse npm run seed:admin

# Voir les logs du container en production
docker logs tekdhi-blog-api
docker logs tekdhi-blog-api --follow       # logs en temps réel
docker logs tekdhi-blog-api --tail 50      # 50 dernières lignes

# Statut du container
docker ps --filter name=tekdhi-blog-api

# Redémarrer le container (sans rebuild)
docker restart tekdhi-blog-api

# Stopper le container
docker stop tekdhi-blog-api

# Vérifier l'API
curl https://blog-api.tekdhi.dev/health

Base de données

# Accéder à la base (depuis l'hôte — PAS depuis Docker)
psql postgresql://tekdhi_user:[REDACTED]@localhost:5432/tekdhi_blog

# Accéder via le container shared-postgres
docker exec -it shared-postgres psql -U psycho_user -d psychoenLigne
# Puis dans psql :
\c tekdhi_blog   # se connecter à la base tekdhi_blog

# Depuis un container temporaire sur le réseau shared_infra
docker run --rm --network shared_infra alpine sh -c \
  "apk add -q postgresql-client && psql postgresql://tekdhi_user:[REDACTED]@shared-postgres:5432/tekdhi_blog"

# Lister les tables
\dt

# Compter les articles
SELECT COUNT(*) FROM articles;
SELECT COUNT(*) FROM articles WHERE statut = 'PUBLIE';

# Voir les admins
SELECT id, email, "createdAt" FROM admin_users;

Docker — commandes générales

# Lister les containers actifs
docker ps

# Lister tous les containers (y compris arrêtés)
docker ps -a

# Voir les containers sur le réseau shared_infra
docker network inspect shared_infra --format \
  '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}'

# Supprimer les images inutilisées
docker image prune -f

# Supprimer l'image tekdhi-api (pour forcer un rebuild complet)
docker rmi backend-tekdhi-api:latest

Frontend

# Lancer en développement
cd /home/kanmaber/projets/tekdhi-site/FrontEnd
npm run dev    # → http://localhost:5173

# Build de production
npm run build

# Vérifier TypeScript
npx tsc --noEmit

Caddy

# Recharger la config Caddy (redémarrage ~1s)
docker restart shared-caddy

# Voir les logs Caddy
docker logs shared-caddy
docker logs shared-caddy --follow

# Vérifier la syntaxe du Caddyfile avant de recharger
docker exec shared-caddy caddy validate --config /etc/caddy/Caddyfile

Vérifications antipatterns DDD (à exécuter après toute modification de use case)

cd /home/kanmaber/projets/tekdhi-site/BackEnd

# Antipattern 1 — validation qui retourne au lieu de throw
grep -rn "!validation.succes" src/Modules --include="*UseCase.ts" -A3 | grep "return {"
# Attendu : 0 ligne

# Antipattern 2 — audit bloquant
grep -rn "await this\.auditer" src/Modules --include="*UseCase.ts" | grep -v "\.catch"
# Attendu : 0 ligne

# Antipattern 3 — exception extends Error au lieu de ErreurMetier
grep -rn "extends Error" src/Modules --include="*Exceptions.ts"
# Attendu : 0 ligne

Changer le mot de passe admin

Il n'y a pas encore de route PATCH /admin/password. Pour changer le mot de passe :

# Option 1 : supprimer et recréer le compte
docker exec -it shared-postgres psql -U psycho_user -d psychoenLigne \
  -c "DELETE FROM tekdhi_blog.public.admin_users WHERE email = 'bernardkanmdev@gmail.com';"

# Puis recréer
cd /home/kanmaber/projets/tekdhi-site/BackEnd
ADMIN_EMAIL=bernardkanmdev@gmail.com ADMIN_PASSWORD_SEED=NouveauMotDePasse npm run seed:admin

# Option 2 : UPDATE direct en DB
# Générer d'abord le hash bcrypt :
node -e "const b=require('bcryptjs'); b.hash('NouveauMDP', 12).then(console.log)"
# Puis mettre à jour :
psql postgresql://tekdhi_user:[REDACTED]@localhost:5432/tekdhi_blog \
  -c "UPDATE admin_users SET \"passwordHash\" = 'LE_HASH' WHERE email = 'bernardkanmdev@gmail.com';"

Notes importantes

  1. prisma migrate dev est interdit sur ce VPS — toujours utiliser npx prisma db push. La shadow database n'est pas disponible.

  2. Le superuser PostgreSQL est psycho_user, pas postgres. Le container shared-postgres a été configuré ainsi.

  3. Le container tekdhi-api est démarré avec docker run (pas docker compose up) car docker-compose v2 dans cet environnement ne gère pas correctement l'attachement au réseau shared_infra externe lors du premier démarrage.

  4. openssl doit être installé dans l'image Alpine via apk add --no-cache openssl — sans ça Prisma ne peut pas établir de connexion TLS avec PostgreSQL.

  5. try_files {path} /index.html dans le Caddyfile est indispensable pour que React Router fonctionne — sans ça, toute navigation directe vers /blog, /admin, etc. retourne 404.

  6. Le contenu des articles est du HTML produit par Tiptap, stocké tel quel en base. Il est affiché via dangerouslySetInnerHTML dans ArticlePage.tsx.

  7. Les images uploadées sont stockées dans /home/kanmaber/tekdhi-landing/uploads/ et servies directement par Caddy sous tekdhi.dev/uploads/.