Système de gestion de traductions pensé en CMS headless : un back-office pour ceux qui traduisent, une API pour ce qui consomme. Architecture - Symfony 7.4 / API Platform 4.3 / MariaDB 11.4, SPA React 19 servie en même origine — ce qui rend viable le cookie de session plutôt qu'un jeton en localStorage. - Deux APIs séparées : Management (session ou clé) et Delivery (stateless, clé seule). Les fusionner ferait porter à chaque lecture de bundle le coût de la session. - Stockage canonique en ICU MessageFormat, sérialisation par plateforme. Le format d'une plateforme ne contamine pas la base. - Publication par releases immuables ; le déploiement est un déplacement de pointeur, donc le rollback aussi. - Isolation multi-organisation par filtre Doctrine, avec un test d'architecture qui casse la CI si une entité échappe à l'invariant. Éditeur, deux vues - Par langue : source et cible, jamais douze colonnes. Grille virtualisée, saisie sans bouton « Enregistrer », panneau de contexte permanent. - Par clé : une clé, toutes ses langues empilées et repliées. Répond à « ce libellé est-il prêt partout ? ». - Mode Focus dans les deux : une file à vider, ⌘↵ pour enchaîner. - Le traducteur ne voit jamais d'ICU : pastilles de variables, un champ par catégorie CLDR de la langue cible. Administration - Deux niveaux : projet (membres, clés API, plateformes) et organisation (annuaire des comptes, création de projets). - Invitations par e-mail, jeton 256 bits stocké haché. - Désactiver un compte coupe les sessions en cours, pas seulement les connexions suivantes. - Les plateformes s'archivent ; ni elles ni les environnements ne se suppriment — la trace explique pourquoi telle clé existe. CLI tqs - PHAR autonome de 3 Mo, autoloader généré : le dépôt client ne dépend ni de Composer ni de la disponibilité de TQ-Slator. - init / push / pull / status ; le sync est non destructif par défaut et son prune est scopé plateforme. 126 tests, PHPStan niveau 8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93 lines
4.6 KiB
YAML
93 lines
4.6 KiB
YAML
security:
|
|
password_hashers:
|
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
|
|
|
providers:
|
|
app_users:
|
|
entity:
|
|
class: App\Entity\User
|
|
# Pas de `property: email` : UserRepository implémente
|
|
# UserLoaderInterface pour maîtriser la levée du filtre
|
|
# multi-organisation au chargement comme au rechargement.
|
|
|
|
firewalls:
|
|
dev:
|
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
|
security: false
|
|
|
|
# ─── Delivery API ────────────────────────────────────────────────────
|
|
# Stateless, clé API uniquement, aucun cookie. Firewall distinct de la
|
|
# Management API : les deux n'ont ni le même mode d'authentification, ni
|
|
# la même surface, ni la même sensibilité à la latence. Les fusionner
|
|
# ferait porter à chaque lecture de bundle le coût de la session.
|
|
delivery:
|
|
pattern: ^/delivery
|
|
stateless: true
|
|
provider: app_users
|
|
entry_point: App\Security\ApiKeyAuthenticator
|
|
custom_authenticators:
|
|
- App\Security\ApiKeyAuthenticator
|
|
|
|
# ─── Management API + back-office ────────────────────────────────────
|
|
# Session cookie SameSite=Lax plutôt que JWT en localStorage : le SPA est
|
|
# servi en same-origin, le cookie HttpOnly est donc immunisé contre
|
|
# l'exfiltration par XSS, et une révocation est immédiate.
|
|
main:
|
|
lazy: true
|
|
provider: app_users
|
|
entry_point: App\Security\ApiEntryPoint
|
|
# Deux modes d'authentification cohabitent ici, et c'est nécessaire :
|
|
# le back-office ouvre une session, le CLI présente une clé API.
|
|
# ApiKeyAuthenticator::supports() ne répond que si un en-tête
|
|
# d'autorisation est présent, les deux ne se marchent donc pas dessus.
|
|
custom_authenticators:
|
|
- App\Security\ApiKeyAuthenticator
|
|
json_login:
|
|
check_path: api_auth_login
|
|
username_path: email
|
|
password_path: password
|
|
success_handler: App\Security\AuthenticationHandler
|
|
failure_handler: App\Security\AuthenticationHandler
|
|
logout:
|
|
path: api_auth_logout
|
|
login_throttling:
|
|
max_attempts: 5
|
|
interval: '15 minutes'
|
|
|
|
access_control:
|
|
# Endpoints publics : connexion et acceptation d'invitation. Un invité
|
|
# n'a par définition pas encore de compte.
|
|
- { path: ^/api/v1/auth/login$, roles: PUBLIC_ACCESS }
|
|
# Consultation ET acceptation : celui qui clique sur le lien n'a par
|
|
# définition pas encore de compte, et il doit pouvoir voir à quoi il est
|
|
# invité avant de choisir un mot de passe.
|
|
- { path: ^/api/v1/invitations/, roles: PUBLIC_ACCESS }
|
|
- { path: ^/api/v1/health$, roles: PUBLIC_ACCESS }
|
|
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
|
# ROLE_API_KEY et non PUBLIC_ACCESS : sans en-tête d'autorisation, le
|
|
# point d'entrée du firewall renvoie 401. Aucun endpoint de delivery ne
|
|
# peut ainsi oublier d'exiger une clé.
|
|
- { path: ^/delivery, roles: ROLE_API_KEY }
|
|
# L'administration globale est hors de portée d'une clé API, quelles que
|
|
# soient ses permissions : une clé vit dans un dépôt et une chaîne
|
|
# d'intégration continue, elle n'a rien à faire près des comptes. Les
|
|
# attributs #[IsGranted] des contrôleurs disent déjà la même chose ; cette
|
|
# ligne est la deuxième serrure, celle qui tient si un contrôleur futur
|
|
# oublie la première.
|
|
- { path: ^/api/v1/admin/, roles: ROLE_SUPER_ADMIN }
|
|
# IS_AUTHENTICATED_FULLY et non ROLE_USER : la Management API accepte deux
|
|
# identités, un utilisateur du back-office et une clé API. Exiger un rôle
|
|
# propre à l'une exclurait l'autre. Le tri fin appartient aux Voters, qui
|
|
# savent raisonner sur les deux.
|
|
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
|
|
|
when@test:
|
|
security:
|
|
password_hashers:
|
|
# Réduit le coût du hachage dans les tests. Ne jamais reprendre ces
|
|
# valeurs ailleurs.
|
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
|
algorithm: auto
|
|
cost: 4
|
|
time_cost: 3
|
|
memory_cost: 10
|