Plateforme e-commerce full-stack pour libraires de mangas — catalogue filtré, panier hybride (localStorage invité / MySQL connecté), tunnel d'achat complet, annonces seconde main, avis, i18n FR/EN. Vanilla JS frontend (no framework), Node/Express/MySQL backend, JWT auth.
→ https://abdoulrazack1.github.io/Kinka/page_accueil.html
⚠️ La version live sur GitHub Pages affiche l'UI complète (CSS, navigation, mode sombre, i18n) mais sans données dynamiques — le backend Node/Express/MySQL n'est pas déployé. Pour la version complète avec catalogue, panier et auth, suis les instructions d'installation ci-dessous.
| Use case | Comment Kinka le résout |
|---|---|
| Visiteur ajoute des articles | Panier en localStorage — pas de friction d'inscription |
| Visiteur s'inscrit après | Migration auto du panier localStorage → MySQL au login (sans perte) |
| Vendre du neuf et de l'occasion | Catalogue principal + section "Annonces" avec CRUD utilisateur |
| i18n FR/EN | Système de traduction custom, 1800+ chaînes |
| Auth solide pour DWWM | JWT + bcrypt (12 rounds) + rate-limit + auth-guard côté client |
Pas une démo — 41 pages HTML, 38 feuilles CSS, 18 scripts JS, 32 endpoints API, mode sombre + responsive complet.
Front : HTML5 · CSS3 · JavaScript ES6+ (sans framework) · Figma Back : Node.js (Express) · MySQL · JWT (jsonwebtoken) · bcryptjs Outils : Git · Live Server (VS Code, port 5503)
Catalogue
- Filtres dynamiques multi-critères (catégorie, état, éditeur, prix, auteur) sans rechargement
- Moteur de recherche instantané (titre, série, auteur, éditeur, description)
- Pages dédiées : nouveautés, promotions, coups de cœur, coffrets, occasion, par catégorie
Panier & commande
- Panier hybride : localStorage si visiteur, base MySQL si connecté
- Tunnel complet : panier → paiement (avec adresse) → confirmation → suivi
- Calcul automatique du sous-total, des frais de livraison (gratuits dès 50 €) et du TTC
- Décrémentation transactionnelle des stocks
Compte utilisateur
- Inscription, connexion, mot de passe oublié, modification du profil
- Stockage JWT en localStorage + cookie « se souvenir de moi » (30 jours)
- Pages protégées par auth-guard (panier, paiement, profil, favoris, …)
Favoris : ajout/retrait synchronisé entre localStorage (visiteur) et BDD (connecté)
Annonces seconde main : création/édition/suppression par les utilisateurs
Avis : note + commentaire (une fois par produit)
Légal & support : CGU, CGV, politique de retour, FAQ, contact (formulaire branché à l'API)
Mode sombre : toggle persistant + respect du prefers-color-scheme système
i18n : système de traduction FR/EN (1800+ chaînes)
Kinka/
├── README.md ← ce fichier
├── .gitignore ← exclut node_modules, .env, builds
├── .vscode/ ← config Live Server (port 5503)
│
├── client/ ← Front
│ ├── pages/ ← 42 pages HTML
│ │ ├── page_accueil.html ← homepage avec carrousel
│ │ ├── page_catalogue.html ← catalogue filtré
│ │ ├── page_detail_produit.html← fiche produit (+ onglet avis)
│ │ ├── page_annonces.html ← annonces entre membres
│ │ ├── page_admin.html ← back-office
│ │ └── …
│ └── assets/
│ ├── css/ ← feuilles de style (1 par page + partagées)
│ ├── images/ ← logos, bannières, visuels
│ └── js/ ← scripts frontend
│ ├── kinka-api-client.js ← client API + auth + cookies + toast
│ ├── kinka-cards.js ← rendu des cartes produit
│ ├── admin.js ← logique du back-office
│ ├── avis.js ← avis clients
│ └── …
│
├── server/ ← Backend Node.js (MVC)
│ ├── src/
│ │ ├── server.js ← point d'entrée : middlewares + montage des routes
│ │ ├── routes/ ← déclaration des URL (chemin + middlewares)
│ │ ├── controllers/ ← logique applicative
│ │ ├── models/ ← accès aux données : tout le SQL vit ici
│ │ ├── views/emails/ ← gabarits des emails envoyés par le serveur
│ │ ├── services/ ← dépendances externes (SMTP, API Jikan)
│ │ ├── middleware/ ← auth JWT, rôle admin, validation
│ │ └── config/db.js ← pool MySQL (mysql2/promise)
│ ├── scripts/
│ │ ├── seed_big.js ← jeu de données de démonstration
│ │ ├── sync_mangas_jikan.js ← import depuis MyAnimeList
│ │ ├── sync_covers_mangadex.js ← une couverture par tome
│ │ └── make_admin.js ← promotion d'un compte en administrateur
│
├── database/
│ ├── schema.sql ← structure seule
│ ├── seed.sql ← données de démonstration
│ └── migrations/ ← évolutions du schéma
│
├── docs/ ← documentation et captures d'écran
└── .env.example ← copier en .env
- Node.js ≥ 18 (pour
fetchnatif) - MySQL ≥ 8 ou MariaDB ≥ 10.5
- VS Code avec extension Live Server (recommandé pour le front)
git clone https://github.com/Abdoulrazack1/Kinka.git
cd Kinkanpm installDans MySQL :
CREATE DATABASE kinka_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Puis importer le schéma :
mysql -u root -p kinka_db < kinka_db.sqlcp .env.example .envÉditer .env avec vos valeurs :
PORT=3000
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=kinka_db
DB_USER=root
DB_PASS=ton_mot_de_passe
# Générer une clé robuste avec :
# node -e "console.log(require('crypto').randomBytes(64).toString('base64'))"
JWT_SECRET=remplacer_par_une_cle_aleatoire_longue
JWT_EXPIRES_IN=7d
CLIENT_URL=http://127.0.0.1:5503
npm run seed:big # Jeu de données de démonstration
node server/scripts/create_demo_user.js # Compte démo : demo@kinka.fr / demo1234(Optionnel : npm run sync pour ajouter ~100 mangas depuis MyAnimeList via Jikan)
npm start # Production
npm run dev # Développement (rechargement auto avec nodemon)Vérifier :
GET http://localhost:3000/api/health
→ { "success": true, "message": "Kinka API en ligne 🎌", "version": "2.0.0" }
Ouvrir le projet dans VS Code et clic-droit sur page_accueil.html → Open with Live Server.
Le front sera servi sur http://127.0.0.1:5503.
Démarrage :
npm run devsert à la fois l'API et le site sur http://localhost:3000 — front et API sur la même origine, sans configuration d'hôte virtuel. Le site reste servable par Apache : le.htaccessà la racine redirige versclient/et bloque l'accès web àserver/,database/etdocs/.
Après node server/scripts/create_demo_user.js :
| Mot de passe | Plan | |
|---|---|---|
| demo@kinka.fr | demo1234 | Premium |
| Méthode | Route | Auth | Description |
|---|---|---|---|
| GET | /api/health | — | Vérification du serveur |
| POST | /api/auth/register | — | Inscription |
| POST | /api/auth/login | — | Connexion |
| POST | /api/auth/forgot | — | Demande de réinitialisation |
| POST | /api/auth/reset | — | Enregistrer le nouveau mot de passe |
| GET | /api/auth/me | ✓ | Profil courant |
| PUT | /api/auth/me | ✓ | Modifier le profil |
| PUT | /api/auth/password | ✓ | Changer le mot de passe |
| DELETE | /api/auth/me | ✓ | Supprimer le compte |
| GET | /api/produits | — | Liste filtrée + paginée |
| GET | /api/produits/search?q= | — | Recherche (titre, série, auteur…) |
| GET | /api/produits/:id | — | Détail d'un produit |
| GET | /api/editeurs | — | Maisons d'édition + nb de titres |
| GET | /api/editeurs/:slug | — | Fiche d'une maison d'édition |
| GET | /api/panier | ✓ | Mon panier |
| POST | /api/panier | ✓ | Ajouter un article |
| PUT | /api/panier/:id | ✓ | Modifier la quantité |
| DELETE | /api/panier/:id | ✓ | Retirer un article |
| DELETE | /api/panier | ✓ | Vider le panier |
| GET | /api/favoris | ✓ | Mes favoris |
| POST | /api/favoris | ✓ | Ajouter un favori |
| DELETE | /api/favoris/:id | ✓ | Retirer un favori |
| DELETE | /api/favoris | ✓ | Vider les favoris |
| GET | /api/commandes | ✓ | Historique commandes |
| GET | /api/commandes/:id | ✓ | Détail d'une commande |
| POST | /api/commandes | ✓ | Passer une commande (transaction) |
| GET | /api/annonces | — | Liste des annonces (occasion) |
| GET | /api/annonces/mes-annonces | ✓ | Mes annonces publiées |
| GET | /api/annonces/:id | — | Détail d'une annonce |
| POST | /api/annonces | ✓ | Publier une annonce |
| PUT | /api/annonces/:id | ✓ | Modifier une annonce |
| DELETE | /api/annonces/:id | ✓ | Supprimer une annonce |
| GET | /api/avis?produit_id= | — | Avis d'un produit |
| POST | /api/avis | ✓ | Publier ou modifier un avis |
| DELETE | /api/avis/:produit_id | ✓ | Supprimer mon avis |
| GET | /api/mangas/search?q= | — | Recherche Jikan (sans BDD) |
| GET | /api/mangas/:mal_id | — | Fiche Jikan par identifiant MAL |
| POST | /api/mangas/sync | 🔒 | Sync mangas populaires |
| POST | /api/mangas/sync-one | 🔒 | Importer un manga par mal_id |
| POST | /api/newsletter | — | Inscription newsletter |
| POST | /api/contact | — | Envoyer un message de contact |
Légende : — public · ✓ connecté · 🔒 administrateur
Toutes ces routes exigent un compte administrateur.
| Méthode | Route | Description |
|---|---|---|
| GET | /api/admin/stats | Chiffres du tableau de bord |
| GET | /api/admin/produits | Catalogue (recherche, filtres) |
| POST | /api/admin/produits | Créer une fiche produit |
| PUT | /api/admin/produits/:id | Modifier une fiche produit |
| DELETE | /api/admin/produits/:id | Supprimer une fiche produit |
| GET | /api/admin/commandes | Toutes les commandes |
| GET | /api/admin/commandes/:id | Détail + historique des statuts |
| PATCH | /api/admin/commandes/:id | Statut, transporteur, numéro de suivi |
| GET | /api/admin/avis | File de modération des avis |
| PATCH | /api/admin/avis/:id | Publier ou retirer un avis |
| DELETE | /api/admin/avis/:id | Supprimer un avis |
| GET | /api/admin/annonces | Annonces entre membres |
| PATCH | /api/admin/annonces/:id | Changer le statut d'une annonce |
| DELETE | /api/admin/annonces/:id | Supprimer une annonce |
| GET | /api/admin/contact | Messages reçus |
| PATCH | /api/admin/contact/:id | Marquer traité / rouvrir |
| DELETE | /api/admin/contact/:id | Supprimer un message (spam) |
| GET | /api/admin/newsletter | Inscrits à la newsletter |
| GET | /api/admin/newsletter/export | Export CSV des inscrits |
| GET | /api/admin/utilisateurs | Comptes clients |
| PATCH | /api/admin/utilisateurs/:id | Changer le rôle d'un compte |
| GET | /api/admin/logs | Journal des actions d'administration |
Toutes les réponses suivent le format :
{ "success": true, "data": { … } }
{ "success": false, "error": "Message" }
{ "success": false, "errors": { "champ": "Message" } }Les routes protégées attendent un header :
Authorization: Bearer <token_jwt>
Toute page HTML inclut les scripts dans cet ordre (gérés par auth-guard et panier hybride) :
<script src="../assets/js/kinka-api-client.js"></script>
<script src="../assets/js/kinka-auth-guard.js"></script>
<script src="../assets/js/kinka-cards.js"></script>
<script src="../assets/js/authentification.js"></script>
<script src="../assets/js/panier.js"></script>
<script src="../assets/js/favoris.js"></script>
<script src="../assets/js/darkmode.js"></script>
<script src="../assets/js/recherche.js"></script>
<script src="../assets/js/translate.js"></script>Exemples d'utilisation :
// Connexion (avec « se souvenir de moi »)
const user = await KinkaAPI.auth.login('demo@kinka.fr', 'demo1234', true);
// Catalogue filtré
const mangas = await KinkaAPI.produits.getAll({ categorie: 'Shônen', promo: '1' });
// Panier
await KinkaAPI.panier.add('one-piece-105', 1);
// Toast (XSS-safe, défini globalement)
showToast('Ajouté au panier !', 'success');Pour pointer vers une autre URL d'API, définir avant le chargement du client :
<script>window.KINKA_API_URL = 'https://api.kinka.fr/api';</script>
<script src="../assets/js/kinka-api-client.js"></script>- Mots de passe hachés avec bcrypt (12 rounds)
- Authentification par JWT (HS256, 7 jours par défaut)
- Rate limiting : 100 req/min sur
/api, 10 req/15 min sur/api/auth, 600/min sur les couvertures. Le limiteur ne couvre volontairement pas les pages et les assets : un affichage demande une trentaine de fichiers, et un plafond global rendait le site inaccessible au bout de trois pages visitées. - Politique de mot de passe : 8 à 72 caractères, au moins deux familles de
caractères, pas de répétition d'un seul caractère. Appliquée à l'inscription,
au changement depuis le profil et à la réinitialisation — le contrôle côté
client (
kinkaFaiblesseMotDePasse) n'est qu'un confort, le serveur tranche. - Champ leurre anti-robot (
site_web) sur l'inscription et le contact : invisible et hors du parcours clavier, il fait refuser toute requête qui le remplit. Le message de contact suspect reçoit une réponse de succès ordinaire mais n'est pas enregistré. - Validation systématique des entrées (
middleware/validate.js) - Requêtes paramétrées (mysql2) → pas d'injection SQL
- XSS : échappement HTML dans toutes les fonctions de rendu (
_esc,_e) - CORS configurable via
CLIENT_URL - Bannière de consentement RGPD (cookies)
- Auth-guard côté client pour les pages sensibles (
kinka-auth-guard.js) : redirige vers la connexion en mémorisant la destination. Le panier et la page Premium restent ouverts aux visiteurs ; la connexion n'est exigée qu'au paiement.
Les deux thèmes respectent le niveau WCAG AA (contraste 4,5:1 pour le texte courant, 3:1 pour les grands titres), vérifié par mesure sur 16 pages × 2 thèmes.
Le rose de marque #e03b8b ne satisfait ce seuil dans aucun de ses deux rôles :
4,14:1 en texte sur fond sombre, 4,06:1 en aplat sous du texte blanc. Aucune
valeur unique ne convient — assombrir améliore l'aplat et dégrade le texte.
Deux déclinaisons sont donc appliquées, générées depuis les feuilles du projet :
| Rôle | Couleur | Ratio |
|---|---|---|
| le rose est le texte | #ef5a9f (sombre) / #c62372 (clair) |
5,31:1 / 4,90:1 |
| le rose porte du texte blanc | #d12d7d |
4,79:1 |
Le jeton --pink lui-même est inchangé : bordures, icônes et usages décoratifs
gardent la teinte d'origine. Les surcharges vivent dans darkmode.css et
contraste-clair.css.
La mise en page ne déborde plus horizontalement de 320 à 1280 px (18 pages
vérifiées). La cause était presque toujours la même : un élément de grille garde
min-width: auto et impose la largeur minimale de son contenu à sa piste.
npm run lint # ESLint + contrôle des scripts inline
npm run lint:inline # uniquement le JS écrit dans les pages HTML
npm run format # Prettierlint:inline existe parce qu'ESLint ne lit que les fichiers .js : les ~1600
lignes de JavaScript écrites directement dans les <script> des pages
n'étaient vérifiées par rien. C'est précisément là que se trouvaient les deux
pannes les plus visibles de l'audit (une IIFE et un setTimeout non refermés,
qui rendaient muet tout le script de la page Contact et de la page Paiement).
Le script échoue avec un code de sortie non nul, il est donc utilisable en CI.
- Remplacer
JWT_SECRETpar une clé forte générée aléatoirement (64 octets) - Restreindre
CLIENT_URLà votre domaine de production (au lieu de*) - Activer HTTPS (les cookies sont en
SameSite=Lax; passer enStrict+Securederrière HTTPS) - Mettre en place un vrai service d'envoi d'emails pour
/auth/forgot(le code actuel logge en console) - Pré-générer les indexes (déjà inclus pour
categorie,promo,nouveaute,bestseller,commandes.user_id) - Brancher un reverse proxy (nginx) devant le Node + servir le statique depuis nginx
Abdoulrazack Abdillahi Mahamoud — abdoul.abdillahi@gmail.com
Projet réalisé dans le cadre de la formation Développeur Web et Web Mobile (DWWM).

