isLockedOut comptait `username = $2 OR ip = $3` dans un seul total. Cinq échecs avec le nom d'un admin, depuis n'importe quelle adresse, verrouillaient donc ce compte un quart d'heure — à répéter indéfiniment, sans coût et sans connaître le mot de passe. Il suffisait de connaître le nom d'utilisateur pour mettre l'admin dehors en permanence. Les deux compteurs deviennent distincts et volontairement asymétriques. L'IP reste stricte à 5 : c'est elle qui freine la force brute, et s'auto-verrouiller n'a aucun intérêt pour un attaquant. Le compteur par username subsiste contre une attaque répartie, mais à 20 — il faut brûler 4 IP avant de commencer à gêner le titulaire du compte. Vérifié contre PostgreSQL : l'admin légitime a bien par_ip=0 quand un tiers martèle depuis ailleurs. Ajoute par ailleurs `Vary: Origin` aux réponses CORS. L'en-tête Allow-Origin dépend de l'origine demandée : sans Vary, un cache intermédiaire peut servir à une origine la réponse mise en cache pour une autre — ce qui revient à autoriser une origine qui ne l'est pas, ou à faire refuser une origine légitime. Posé aussi quand l'origine est refusée, pour la même raison. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| .githooks | ||
| apps | ||
| infra | ||
| scripts | ||
| .gitignore | ||
| docker-compose.dev.yml | ||
| docker-compose.test.yml | ||
| docker-compose.yml | ||
| eslint.config.js | ||
| jsconfig.json | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.js | ||
| pyproject.toml | ||
| README-CI.md | ||
| README.md | ||
| requirements-dev.txt | ||
| stryker.config.json | ||
| stryker.full.config.json | ||
| vitest.config.js | ||
| vitest.integration.config.js | ||
| vitest.mutation.config.js | ||
| vitest.mutation.full.config.js | ||
Stack metalfrom.eu
Carte des groupes de metal européens, données issues de Metal Archives (avec accord du propriétaire du site source).
Services
| Service | Rôle |
|---|---|
web |
Site public (carte Leaflet) |
admin |
Back-office (nginx + reverse-proxy vers l'API) |
api |
API Fastify (public + routes admin), migrations DB |
crawler |
Scraping incrémental/complet de Metal Archives via FlareSolverr |
flaresolverr |
Contournement Cloudflare (Chrome headless) pour le crawler |
geocoder-enqueue |
Peuple band_locations depuis bands.location_text |
geocoder-worker |
Géocodage Geoapify |
groq-worker |
Désambiguïsation LLM (Groq) des lieux non géocodés |
crawler et flaresolverr ne tournent qu'en dev pour l'instant : ils sont
absents de docker-compose.yml. La prod partage la même base, alimentée depuis
l'environnement de dev.
Déploiement
docker-compose.yml: productiondocker-compose.dev.yml: environnement de dev (branchedev, auto-déployé via webhook Forgejo → Coolify)
Base PostgreSQL + PostGIS gérée séparément par Coolify. Variables d'env : voir
infra/.env.example.
Qualité
Le déploiement se déclenche sur webhook à chaque push : rien ne s'interpose
entre git push et la mise en ligne. La porte de qualité est donc locale,
et doit être installée une fois par clone :
npm install
npm run hooks:install
| Commande | Portée |
|---|---|
npm run check |
lint + types + tests JS + ruff + pytest (~6 s) |
npm run test:e2e |
parcours Playwright du dashboard admin (~22 s) |
npm run test:integration:full |
SQL validé contre un vrai PostgreSQL+PostGIS (exige Docker) |
npm run check:full |
tout ci-dessus + mutation + audits de dépendances |
Le hook pre-push lance check puis test:e2e. .forgejo/workflows/ci.yml
existe mais reste inerte tant qu'aucun runner Forgejo n'est enregistré.
Outillage Python de test : pip install -r requirements-dev.txt.