7fb22bcb29
Nouveau dossier /frontend avec un SPA complet qui couvre les écrans de la
maquette (cf. docs/Capture*.png) : public + admin token. Backend inchangé.
Stack
- Vue 3.5 + Vite 8 + TypeScript 6 + Tailwind v4 (via @tailwindcss/vite)
- Vue Router 4 en mode history, code-split par page (lazy import)
- Aucune lib UI, aucun store (pas de Pinia) — état local + fetch direct
- Bundle initial 50 kB gzip, total dist 240 kB
Pages
- / redirige vers /donut
- /dashboard 4 KPIs + mini donut + flux activité (via /api/stats + /api/activity)
- /donut viz SVG custom : 6 secteurs catégories, projets en points
colorés, anneaux plancher social / plafond écologique,
tooltip + click vers la fiche projet
- /projects grille de cartes paginée + filtres (catégorie, statut, recherche
avec debounce, query string comme source de vérité)
- /projects/new formulaire complet de création (token admin requis)
- /projects/:slug fiche : header méta + tabs Roadmap/Discussions/Ressources
+ panneau droit Progression + Équipe, modal "Rejoindre"
- /archives projets status=archived
- /categories 6 cartes coloured-top-border + count via /api/stats
- /admin/login saisie du token Bearer, validé via POST /api/members (vide)
- 404 fallback
Structure
- src/api/client.ts : wrapper fetch typé + token Bearer auto + ApiError
- src/composables/useAuth : token localStorage + reactivité Vue
- src/types/api.ts : types miroirs des réponses PHP
- src/utils/formatDate.ts : ISO/MySQL/DATE -> JJ mois AAAA, relatif, etc.
- src/components/ : Avatar, ProjectCard, StatusBadge, CategoryBadge,
ProgressBar, LoadingState, ErrorState, JoinProjectModal
- src/layouts/AppLayout : sidebar nav + main scrollable
- src/components/AppSidebar: logo donut SVG, CTA, nav, statut admin en bas
- src/pages/ : 10 pages ci-dessus
Intégration & déploiement
- vite.config.ts proxy /api/* vers http://donut-gestprojet.test (Laragon)
- caddy.conf prod : /api/* -> reverse_proxy PHP, le reste -> static dist/
avec fallback /index.html (history mode), cache long sur /assets/*
- DEPLOY.md mis à jour avec build local + rsync dist/ vers
/srv/donut/web/infolab-front/dist
- README.md mis à jour avec section "Frontend (SPA Vue 3)"
Build & types
- npm run build : vue-tsc strict (TS 6 + erasableSyntaxOnly) -> OK
- paths alias @/* configuré dans tsconfig.app.json
Vérifié localement
- npm run dev sur :5173, proxy /api -> Apache Laragon -> PHP
- Toutes les routes répondent 200 (curl http://localhost:5173/...)
- Routes API consommées correctement (categories, stats, projects, ...)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
334 lines
13 KiB
Markdown
334 lines
13 KiB
Markdown
# Le Donut Infolab — API
|
|
|
|
API JSON pour **Le Donut Infolab**, plateforme de projets citoyens marseillais
|
|
structurée autour de la vision Donut (plancher social / plafond écologique).
|
|
|
|
- **Stack** : PHP 8.3 + MariaDB 11.4 (compatible MySQL 8.x pour le dev local).
|
|
- **Routing** : router maison, pas de framework, autoload PSR-4 sans Composer.
|
|
- **Auth** : Bearer token unique (`ADMIN_TOKEN`) sur toutes les écritures.
|
|
Lectures publiques.
|
|
- **Front** : séparé (React/Vue), pas dans ce repo.
|
|
|
|
> Sous-domaine cible en prod : `https://infolab.ledonut-marseille.org`
|
|
|
|
---
|
|
|
|
## Pré-requis
|
|
|
|
### Prod (serveur Donut Marseille)
|
|
|
|
- Docker (la stack y tourne déjà : `donut-mariadb`, `donut-nextcloud`, etc.)
|
|
- Caddy natif côté hôte
|
|
- Réseau Docker externe `donut-net` (déjà créé)
|
|
- Conteneur `donut-mariadb` opérationnel et accessible sur ce réseau
|
|
|
|
### Dev local (Laragon, Windows)
|
|
|
|
- PHP 8.3+ (Laragon embarque PHP 8.3.30)
|
|
- MariaDB ou MySQL 8.x (Laragon embarque MySQL 8.4)
|
|
- Apache (Laragon) avec `mod_rewrite` (activé par défaut)
|
|
- `bash`, `curl`, `openssl` pour les scripts (Git Bash sous Windows)
|
|
|
|
---
|
|
|
|
## Démarrage rapide en dev local (Laragon)
|
|
|
|
### 1. Créer la base de données et l'utilisateur applicatif
|
|
|
|
Dans une console Laragon (ou `mysql -uroot`) :
|
|
|
|
```sql
|
|
CREATE DATABASE infolab CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
CREATE USER 'infolab'@'localhost' IDENTIFIED BY 'dev-pass-change-me';
|
|
GRANT ALL PRIVILEGES ON infolab.* TO 'infolab'@'localhost';
|
|
FLUSH PRIVILEGES;
|
|
```
|
|
|
|
### 2. Appliquer les migrations
|
|
|
|
```bash
|
|
mysql -uinfolab -p'dev-pass-change-me' infolab < migrations/001_init_schema.sql
|
|
mysql -uinfolab -p'dev-pass-change-me' infolab < migrations/002_seed_categories.sql
|
|
```
|
|
|
|
> Le script `migrations/apply.sh` est conçu pour la prod (Docker) ; en local,
|
|
> on applique les fichiers à la main comme ci-dessus.
|
|
|
|
### 3. Créer un fichier `.env` (gitignoré)
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
```
|
|
|
|
Puis éditer `.env` :
|
|
|
|
```env
|
|
DB_HOST=127.0.0.1
|
|
DB_PORT=3306
|
|
DB_NAME=infolab
|
|
DB_USER=infolab
|
|
DB_PASS=dev-pass-change-me
|
|
|
|
ADMIN_TOKEN=$(openssl rand -hex 32) # ← remplacer par la valeur générée
|
|
|
|
CORS_ORIGIN=*
|
|
APP_ENV=development
|
|
APP_DEBUG=true
|
|
```
|
|
|
|
### 4. Lancer Laragon
|
|
|
|
Laragon détecte automatiquement le dossier `public/` du projet et expose
|
|
l'URL **`http://donut-gestprojet.test`** (vhost auto).
|
|
|
|
Vérifier :
|
|
|
|
```bash
|
|
curl http://donut-gestprojet.test/api/health
|
|
# {"status":"ok","time":"2026-05-28T..."}
|
|
```
|
|
|
|
### 5. Lancer la suite de smoke tests
|
|
|
|
```bash
|
|
ADMIN_TOKEN="$(grep ^ADMIN_TOKEN= .env | cut -d= -f2-)" \
|
|
INFOLAB_URL=http://donut-gestprojet.test \
|
|
./tests/smoke.sh
|
|
```
|
|
|
|
---
|
|
|
|
## Endpoints
|
|
|
|
Tous préfixés par `/api`. Format de réponse : `application/json; charset=utf-8`.
|
|
|
|
### Conventions
|
|
|
|
- **Listes paginées** : `{ "data": [...], "meta": { page, per_page, total, total_pages } }`
|
|
- **Listes non paginées** : `{ "data": [...] }`
|
|
- **Détail d'une ressource** : objet JSON directement
|
|
- **Erreurs uniformes** : `{ "error": "code", "message"?: "...", "fields"?: {...} }`
|
|
|
|
### Codes HTTP
|
|
|
|
| Code | Quand | Body |
|
|
|------|-------|------|
|
|
| 200 | Lecture ou update OK | l'objet |
|
|
| 201 | Création OK | l'objet créé |
|
|
| 204 | Suppression OK | (vide) |
|
|
| 400 | JSON invalide ou param connu mais mal formé | `{error:"bad_request",message:...}` |
|
|
| 401 | Token absent / faux sur une route protégée | `{error:"unauthorized"}` |
|
|
| 404 | Ressource inexistante | `{error:"not_found"}` |
|
|
| 405 | Bonne URL, mauvaise méthode | `{error:"method_not_allowed",allowed:[...]}` |
|
|
| 409 | Conflit (slug pris, FK bloquée, déjà membre) | `{error:"conflict",message:...}` |
|
|
| 422 | Payload invalide | `{error:"validation_failed",fields:{...}}` |
|
|
| 500 | Bug imprévu — voir Dozzle | `{error:"internal_server_error"}` |
|
|
|
|
### Lectures publiques (pas de token)
|
|
|
|
| Méthode + Path | Description |
|
|
|---|---|
|
|
| `GET /api/health` | Sonde de vie + ping BDD |
|
|
| `GET /api/categories` | 6 catégories triées par `sort_order` |
|
|
| `GET /api/categories/:slug` | Détail + `projects_count` |
|
|
| `GET /api/members?page=1&per_page=20` | Annuaire paginé |
|
|
| `GET /api/members/:slug` | Détail + projets auxquels le membre participe |
|
|
| `GET /api/projects?category=&status=&q=&page=&per_page=` | Liste paginée |
|
|
| `GET /api/projects/:slug` | Détail complet (catégorie, équipe, roadmap, compteurs) |
|
|
| `GET /api/projects/:slug/roadmap` | Étapes triées par `sort_order` |
|
|
| `GET /api/projects/:slug/members` | Équipe avec rôle |
|
|
| `GET /api/projects/:slug/discussions` | Messages racines + réponses imbriquées |
|
|
| `GET /api/projects/:slug/resources` | Ressources triées par `sort_order` |
|
|
| `GET /api/activity?limit=20&project=slug` | Flux d'activité |
|
|
| `GET /api/stats` | KPIs du dashboard |
|
|
|
|
### Écritures protégées (`Authorization: Bearer <ADMIN_TOKEN>`)
|
|
|
|
| Méthode + Path | Notes |
|
|
|---|---|
|
|
| `POST /api/categories` | Validation : `name` requis |
|
|
| `PATCH /api/categories/:slug` | Partial update |
|
|
| `DELETE /api/categories/:slug` | **409** si projets attachés |
|
|
| `POST /api/members` | Validation : `display_name` requis ; initiales auto-générées |
|
|
| `PATCH /api/members/:slug` | Partial update |
|
|
| `DELETE /api/members/:slug` | Cascade : retrait des projets, discussions/activity nullifiées |
|
|
| `POST /api/projects` | Log `created_project` |
|
|
| `PATCH /api/projects/:slug` | Log `updated_project` (+ `completed_project` si transition `status=completed`) |
|
|
| `DELETE /api/projects/:slug` | **Soft delete** (set `deleted_at`) |
|
|
| `POST /api/projects/:slug/members` | `{member_slug, role}` ; log `joined` ; **409** si déjà membre |
|
|
| `DELETE /api/projects/:slug/members/:member_slug` | Log `left` |
|
|
| `POST /api/projects/:slug/roadmap` | Log `added_step` |
|
|
| `PATCH /api/projects/:slug/roadmap/:id` | Log `completed_step` si `is_done` passe à `true` |
|
|
| `DELETE /api/projects/:slug/roadmap/:id` | |
|
|
| `POST /api/projects/:slug/discussions` | `{author_name, body, parent_id?, author_member_id?}` ; log `posted_discussion` |
|
|
| `PATCH /api/projects/:slug/discussions/:id` | |
|
|
| `DELETE /api/projects/:slug/discussions/:id` | Cascade des réponses |
|
|
| `POST /api/projects/:slug/resources` | `{title, url, kind?}` ; log `added_resource` |
|
|
| `PATCH /api/projects/:slug/resources/:id` | |
|
|
| `DELETE /api/projects/:slug/resources/:id` | |
|
|
|
|
---
|
|
|
|
## Exemples curl
|
|
|
|
```bash
|
|
URL=http://donut-gestprojet.test
|
|
TOKEN=xxxx-votre-token
|
|
|
|
# Lecture publique
|
|
curl -s "$URL/api/categories" | head
|
|
curl -s "$URL/api/projects?category=education-ecoles&status=active&q=ecole"
|
|
curl -s "$URL/api/stats"
|
|
|
|
# Création (token requis)
|
|
curl -s -X POST "$URL/api/members" \
|
|
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
-d '{"display_name":"Jean Dupont","kind":"person","contact_email":"jean@example.org"}'
|
|
|
|
curl -s -X POST "$URL/api/projects" \
|
|
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
-d '{"title":"Cartographie des îlots de chaleur","category_id":5,"status":"active","progress_percent":10}'
|
|
|
|
# Patch
|
|
curl -s -X PATCH "$URL/api/projects/cartographie-des-ilots-de-chaleur" \
|
|
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
-d '{"status":"completed","progress_percent":100}'
|
|
|
|
# Suppression (soft)
|
|
curl -s -X DELETE "$URL/api/projects/cartographie-des-ilots-de-chaleur" \
|
|
-H "Authorization: Bearer $TOKEN"
|
|
```
|
|
|
|
---
|
|
|
|
## Frontend (SPA Vue 3)
|
|
|
|
Le SPA vit dans `/frontend/` — Vue 3 + Vite + TypeScript + Tailwind v4 + Vue Router.
|
|
Il consomme l'API JSON et propose les pages : Tableau de bord, Vision Donut (viz
|
|
SVG custom), Projets, Catégories, Fiche projet, Nouveau projet, Archives, Connexion admin.
|
|
|
|
### Dev local
|
|
|
|
```bash
|
|
# Backend PHP : Laragon sert déjà /api/* sur http://donut-gestprojet.test
|
|
cd frontend
|
|
npm install
|
|
npm run dev # http://localhost:5173 (Vite proxy /api vers Laragon)
|
|
```
|
|
|
|
### Build de prod
|
|
|
|
```bash
|
|
cd frontend
|
|
npm run build # vue-tsc + vite build -> frontend/dist/
|
|
```
|
|
|
|
Le SPA est servi par Caddy en statique depuis `frontend/dist/`, l'API par le
|
|
conteneur PHP. Détails dans `DEPLOY.md`.
|
|
|
|
### Architecture
|
|
|
|
- **Aucune lib UI** — composants maison + Tailwind v4 (utilities classes).
|
|
- **Pas de Pinia** : la source de vérité est l'API. État local via `ref`/`reactive`.
|
|
- **HTTP** : `fetch` natif + petit wrapper `src/api/client.ts` (typage + erreurs).
|
|
- **Auth admin** : token Bearer stocké dans `localStorage`, header injecté sur tous
|
|
les writes. Validé "best-effort" au login en pingant POST /api/members avec body vide.
|
|
- **Routing** : Vue Router 4 en mode `history`, code-split par page (lazy import).
|
|
- **Donut viz** : SVG natif, géométrie polaire calculée à la main dans `DonutPage.vue`.
|
|
|
|
---
|
|
|
|
## Ajouter une nouvelle entité
|
|
|
|
1. Écrire la migration SQL dans `migrations/00X_xxx.sql` (CREATE TABLE).
|
|
2. Créer le contrôleur dans `src/Controllers/XxxController.php` (un par
|
|
entité, méthodes `index/show/store/update/destroy`).
|
|
3. Brancher les routes dans `public/index.php` (les GET sont publiques,
|
|
POST/PATCH/DELETE sont protégés automatiquement par le router).
|
|
4. Suivre les patterns existants :
|
|
- Tous les accès SQL via `Database::run/fetchAll/...` (prepared statements).
|
|
- Validation via `new Validator($body)` ; finir par `$v->check()`.
|
|
- Réponses via `Response::json(...)` / `Response::noContent()`.
|
|
- Erreurs métier via `throw new \ApiException($status, $payload)` ou `api_abort(...)`.
|
|
5. Ajouter quelques `curl` dans `tests/smoke.sh` pour couvrir.
|
|
|
|
---
|
|
|
|
## Choix d'implémentation à connaître
|
|
|
|
- **Sluggification** : `Slugger::slugify` utilise `intl/Transliterator` si dispo
|
|
(translittération propre des accents), avec fallback bas niveau. Unicité
|
|
garantie par `Slugger::unique` (suffixe `-2`, `-3`...).
|
|
- **Activité** : `ActivityLogger::log` est best-effort — un échec d'écriture
|
|
ne casse pas la requête en cours, juste un `error_log` vers stderr.
|
|
- **Soft delete** : seuls les projets ont `deleted_at`. Les autres entités
|
|
sont supprimées en dur, avec cascades définies au schéma.
|
|
- **Stats** : `participants_total` = nombre total de membres dans l'annuaire
|
|
(interprétation choisie : "annuaire des contributeurs" = participants
|
|
potentiels). Si on veut "participants actifs sur un projet", il faudrait
|
|
un `COUNT(DISTINCT member_id) FROM project_members`.
|
|
- **CORS** : `Access-Control-Allow-Origin` configurable via `CORS_ORIGIN`.
|
|
Par défaut `*` en V1. À restreindre en V2 à l'origine du front.
|
|
- **Pagination** : `per_page` capé à 100 côté serveur.
|
|
- **JSON typé** : `PDO::ATTR_STRINGIFY_FETCHES=false` + casts explicites dans
|
|
les `present()` -> les `int`/`bool` sont propres dans la réponse, pas des strings.
|
|
|
|
---
|
|
|
|
## Roadmap V2
|
|
|
|
- **Auth réelle** : OIDC via Nextcloud (`nextcloud.ledonut-marseille.org`),
|
|
fin du Bearer admin partagé. Rôles utilisateur, modération par projet.
|
|
- **Uploads d'images** : `cover_image_url` géré côté serveur (drag & drop
|
|
vers `/srv/donut/web/infolab/uploads/` avec MIME check + redimensionnement).
|
|
- **Notifications email** : transactional emails sur `joined` / `posted_discussion`
|
|
via Postfix sortant (relais SMTP existant sur le serveur).
|
|
- **Webhooks** : notifier le bot Mattermost du Donut sur création/clôture de projet.
|
|
- **Snapshots de l'`activity_log`** : compaction mensuelle pour éviter
|
|
l'inflation indéfinie de la table.
|
|
|
|
---
|
|
|
|
## Arborescence
|
|
|
|
```
|
|
.
|
|
├── README.md # ce fichier
|
|
├── DEPLOY.md # déploiement serveur Donut (étape par étape)
|
|
├── .env.example # template secrets
|
|
├── .gitignore # cf. fichier (couvre .env, vendor, etc.)
|
|
├── docker-compose.yml # cible prod : conteneur donut-infolab sur donut-net
|
|
├── Dockerfile # php:8.3-apache + pdo_mysql + intl + opcache
|
|
├── caddy.conf # vhost infolab.ledonut-marseille.org
|
|
├── public/
|
|
│ ├── index.php # front controller (toutes les routes)
|
|
│ └── .htaccess # rewrite vers index.php (Apache/Laragon)
|
|
├── src/
|
|
│ ├── bootstrap.php # env, autoload PSR-4, ApiException, handlers
|
|
│ ├── Database.php # singleton PDO + helpers fetchOne/All/insert/...
|
|
│ ├── Router.php # routing :params + flag auth par route
|
|
│ ├── Request.php # parse JSON body, headers, query
|
|
│ ├── Response.php # json/error/noContent + CORS
|
|
│ ├── Auth.php # checkAdminToken (Bearer)
|
|
│ ├── Validator.php # règles fluentes -> 422 fields:{...}
|
|
│ ├── Slugger.php # slugify + unicité
|
|
│ ├── ActivityLogger.php # insert activity_log (best-effort)
|
|
│ └── Controllers/
|
|
│ ├── HealthController.php
|
|
│ ├── CategoriesController.php
|
|
│ ├── MembersController.php
|
|
│ ├── ProjectsController.php
|
|
│ ├── ProjectMembersController.php
|
|
│ ├── RoadmapController.php
|
|
│ ├── DiscussionsController.php
|
|
│ ├── ResourcesController.php
|
|
│ ├── ActivityController.php
|
|
│ └── StatsController.php
|
|
├── migrations/
|
|
│ ├── 001_init_schema.sql
|
|
│ ├── 002_seed_categories.sql
|
|
│ └── apply.sh # idempotent, suivi via schema_migrations
|
|
└── tests/
|
|
└── smoke.sh # appelle les endpoints clés, exit non-zéro si KO
|
|
```
|