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¶
- Vue d'ensemble
- Structure du projet
- Backend — Architecture DDD
- Frontend — React + Vite
- Base de données
- Authentification et sécurité
- Upload d'images
- Docker
- Caddy (reverse proxy)
- Interface d'administration
- Variables d'environnement
- Déploiement
- 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 :
AdminUserRepositoryPrisma—trouverParEmail(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— publicPOST /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— publicGET /api/v1/articles/:slug— publicPOST /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 publicinfrastructure/repositories/ApiCategorieRepository.ts— accès catégories publicinfrastructure/di/DIContainer.ts— câblage repositoriesinfrastructure/di/DIProvider.tsx— contexte React + hookuseDI()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 viauseSearchParams()ArticlePage— affichage HTML viadangerouslySetInnerHTML+BoutonLikeCarteArticle— carte article avec tag catégorie coloré +BoutonLikeFiltreCategories— 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, paspostgres)
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
localStorageclé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, champimage) - 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
.envdans 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 :
opensslest obligatoire dans l'image Alpine pour que Prisma puisse se connecter à PostgreSQL. Sans ça, Prisma lèvePrismaClientInitializationErrorau 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.htmlest critique : sans cette directive, les routes React comme/blog,/a-propos,/admin/articlesretournent 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_URLutiliseshared-postgres(nom du container Docker) — paslocalhost.localhostne fonctionne que hors Docker ; dans le container, le réseaushared_infrarésoutshared-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_URLinclut/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¶
-
prisma migrate devest interdit sur ce VPS — toujours utilisernpx prisma db push. La shadow database n'est pas disponible. -
Le superuser PostgreSQL est
psycho_user, paspostgres. Le containershared-postgresa été configuré ainsi. -
Le container
tekdhi-apiest démarré avecdocker run(pasdocker compose up) car docker-compose v2 dans cet environnement ne gère pas correctement l'attachement au réseaushared_infraexterne lors du premier démarrage. -
openssldoit être installé dans l'image Alpine viaapk add --no-cache openssl— sans ça Prisma ne peut pas établir de connexion TLS avec PostgreSQL. -
try_files {path} /index.htmldans le Caddyfile est indispensable pour que React Router fonctionne — sans ça, toute navigation directe vers/blog,/admin, etc. retourne 404. -
Le contenu des articles est du HTML produit par Tiptap, stocké tel quel en base. Il est affiché via
dangerouslySetInnerHTMLdansArticlePage.tsx. -
Les images uploadées sont stockées dans
/home/kanmaber/tekdhi-landing/uploads/et servies directement par Caddy soustekdhi.dev/uploads/.