Skip to content

feat: fix frontend - #17

Open
lucas-luchack wants to merge 8 commits into
mainfrom
feat/fix-frontend
Open

feat: fix frontend#17
lucas-luchack wants to merge 8 commits into
mainfrom
feat/fix-frontend

Conversation

@lucas-luchack

Copy link
Copy Markdown
Contributor

Ajout de toutes les fonctionnalités présente dans le frontend avant le passage à AdonisJS

Le front avait été construit à partir de maquettes complètes, en avance
sur l'API : 36 migrations couvraient le métier mais seuls ~8 domaines
étaient exposés en HTTP. Plusieurs pages appelaient donc des routes
inexistantes.

Routes
- `start/routes.ts` devient un index mince ; les routes sont déclarées
  explicitement dans `start/routes/*.ts` par domaine, sans `apiOnly()`.
- Corrige `products/summary`, jusqu'ici masqué par le `products/:id` de
  `router.resource('products')` enregistré avant lui : l'URL résolvait
  vers `show()` avec `id = 'summary'`.
- Enregistre `fast-passes`, dont le contrôleur existait sans route.
- `router.route(path, ['PUT','PATCH'], …)` au lieu de `put` + `patch`
  séparés, qui font planter le boot (nom de route auto-dérivé en double).

Coordination
- Jobs, EventJobs, Assignments, Responses, Preferences : la page
  `coordination` échouait entièrement, 5 de ses 7 requêtes renvoyant 404.
- `preferenceRank` est exposé depuis la colonne pivot `rank`.

Billing
- Vouchers (nouvelle table liée à `suppliers`) et transactions en lecture.
- `GoodsController.index` expose les prix par fournisseur : ils étaient
  préchargés mais jamais sérialisés, Lucid ignorant `$extras.pivot_price`.
- Les colonnes `decimal` sont converties en number : le driver les renvoie
  en string, ce qui contaminerait toute arithmétique côté front.

Sessions
- `ip_address` / `user_agent` capturés à la connexion, liste et révocation
  des sessions. Le provider d'`@adonisjs/auth` n'accepte pas de colonnes
  arbitraires, d'où l'update direct après `accessTokens.create()`.
- `hash` et `abilities` ne sont jamais lus ni renvoyés.

Produits
- `summary()` renvoie `category`, dérivée de l'ingrédient de rang le plus
  bas : un produit n'a pas de catégorie propre, seuls ses `goods` en ont.
- `minSupplierPrice` déplacé dans `#services/pricing_service`.

Tests
- `product_summary.spec.ts` verrouille l'ordre des routes et la dérivation
  de catégorie. 9 tests passent.
`POST /v1/auth/signup` échouait systématiquement en 500 : `users.cas_id`
était `notNullable()` sans valeur par défaut, et `NewAccountController`
ne renseigne qu'`email` et `password`. Seule la factory de test
remplissait la colonne, ce qui masquait le problème aux tests existants.

`cas_id` n'est lu nulle part ailleurs que dans `UserTransformer` et
aucune intégration CAS n'existe encore dans le projet : la colonne est un
emplacement réservé pour un futur SSO. Un compte créé par inscription
directe n'a donc légitimement pas d'identité CAS.

La colonne devient nullable plutôt que remplie d'une valeur synthétique,
qui serait indiscernable d'un vrai identifiant CAS une fois le SSO
branché. L'index unique est conservé : Postgres considère les NULL comme
distincts, donc les comptes hors CAS coexistent sans collision.

Tests
- `signup.spec.ts` : création de compte, `casId` null, jeton utilisable
  sur une route protégée, e-mail dupliqué et confirmation divergente.
- `goods_pricing.spec.ts` : verrouille la sérialisation des prix du pivot
  `good_suppliers`, y compris la conversion decimal → number.

14 tests passent.
@lucas-luchack lucas-luchack self-assigned this Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Version Preview

Current version v0.6.0
Next version v0.7.0
Bump type minor

To override the bump type, add a label: major, minor, or patch
Or use conventional commits: feat: → minor, fix: → patch, BREAKING CHANGE → major

`RequestLoggerMiddleware` stockait `ctx.response.getBody()` tel quel pour
chaque requête. Les réponses de `/auth/login` et `/auth/signup` contiennent
le jeton d'accès fraîchement émis : celui-ci se retrouvait donc en clair
dans `logs.meta.response`.

`GET /v1/logs` n'est protégé que par `middleware.auth()`, sans contrôle de
rôle : n'importe quel utilisateur authentifié pouvait donc lire les jetons
des autres et usurper leur compte, y compris avec plus de privilèges.

Deux niveaux de protection :
- les routes dont la réponse est un secret par nature ne voient plus leur
  corps stocké du tout ;
- ce qui subsiste est parcouru et les clés sensibles sont masquées.

Le second niveau est une liste de refus, donc au mieux partielle : un
nouvel endpoint renvoyant un secret inédit fuiterait jusqu'à ce que son
nom y soit ajouté. Le premier niveau est la vraie garantie — c'est là
qu'il faut déclarer une route sensible.

Pagination
- `GET /v1/logs` renvoyait toute la table d'un coup, plus de 500 lignes et
  1,3 Mo, que le client traversait ensuite clé par clé dans son
  convertisseur de casse. Désormais paginé (50 par défaut, 200 au plus) et
  trié du plus récent au plus ancien.

⚠️ Les lignes écrites avant ce correctif contiennent toujours des jetons en
clair : la table demande une purge.
Ajoute un algorithme de Gale-Shapley (membre-proposant, capacités
multiples) pour affecter automatiquement les membres disponibles aux
postes d'une soirée selon leurs préférences, en classant les postes
par points normalisés par la fréquence de participation pour éviter
que les habitués dominent toujours le classement.

Ajoute aussi le verrouillage manuel d'une affectation (neutre en
points, jamais recalculé) et la restriction d'un poste à une liste de
membres éligibles (poste sans ligne = ouvert à tous).
`GET /v1/logs` n'était protégé que par `middleware.auth()` : n'importe quel
membre authentifié pouvait lire qui avait fait quoi, depuis quelle IP, ainsi
que le corps de réponse de chaque route non authentifiante.

Nouveau `middleware.can(permission)`, première brique d'autorisation du
projet. La chaîne était déjà modélisée en base et n'est que résolue :

    user → member (clé primaire partagée) → role → roles_permissions

`Member` utilise `selfAssignPrimaryKey` avec `belongsTo(User, { foreignKey:
'id' })`, donc la ligne membre d'un utilisateur est celle de même id.

Les refus sont volontairement indifférenciés : absence de ligne membre,
absence de rôle ou rôle sans la permission renvoient le même 403. Distinguer
ces cas révélerait la forme du modèle de permissions à des appelants qui
n'en ont aucune.

`log:read` est accordée à `President` et `Finance` — les deux périmètres
d'administration que l'application revendique déjà (« admin réservé
Présidence + Trésorerie »).

Deux bugs latents corrigés au passage, tous deux révélés par ce travail et
indépendants de lui :

- `Array.isArray()` est vrai pour le `ModelPaginator` de Lucid, si bien que
  `case_converter_middleware` le parcourait comme un tableau ordinaire et
  échouait sur ses propres champs internes. `API.md` documente pourtant un
  rendu `{ data, metadata }` : rien dans l'application n'avait jamais
  paginé, donc ce chemin n'avait jamais été exercé. Le paginateur est
  déballé via `.all()` et la métadonnée jointe explicitement, ce qui laisse
  `data` sous forme de tableau — la forme que le client attend déjà.

- `DateTime.isDateTime()` ne teste que le marqueur `isLuxonDateTime`, lequel
  survit à un aller-retour JSON alors que le prototype, non. Un `DateTime`
  brut stocké dans une colonne JSON se relit donc comme un objet simple qui
  se déclare DateTime sans posséder `toISO`, et l'appeler faisait planter la
  requête en 500. Les vraies instances sont converties, les imitations sont
  parcourues comme n'importe quel objet.

`GET /v1/logs` est par ailleurs paginé (50 par défaut, 200 au plus), du plus
récent au plus ancien : il renvoyait auparavant toute la table d'un seul
tenant, plus de 500 lignes et 1,3 Mo.

45 tests passent.
…ces de postes

Verrouillage
- `PUT|PATCH /v1/assignments` met à jour une affectation existante. La clé
  composite voyage dans la query string, faute d'identifiant de substitution ;
  seul `locked` est modifiable.
- Sans cette route, basculer un verrou imposait un DELETE suivi d'un POST, ce
  qui remettait `points_delta` à 0 — précisément la valeur que le moteur
  d'affectation rembourse lorsqu'il remplace une ligne, donc une corruption
  des totaux de points au lancement suivant. Un test le verrouille.
- `points_delta` reste volontairement absent du validateur : c'est une
  écriture du moteur, pas du client.

Préférences de postes
- `GET` et `PUT|PATCH /v1/account/preferences` : un membre gère son propre
  classement. L'appelant est déduit du jeton, aucun identifiant de membre ne
  circule dans le chemin, donc personne ne peut classer à la place d'un autre.
- La liste est ORDONNÉE : le rang découle de la position, si bien qu'un client
  ne peut produire ni trou, ni ex æquo, ni doublon. Une liste vide efface le
  classement. Un poste inconnu invalide toute la soumission, sans écriture
  partielle.

58 tests passent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant