Versions et sécurité

4.0.0 - non publiée

Sécurité

  • « Mot de passe oublié » répondait plus vite pour une adresse inconnue : la page écrivait le jeton de reset, et l'attendait, seulement quand l'adresse correspondait à un compte ; son temps de réponse révélait donc si un compte existait. La recherche du compte, le jeton et l'email partent désormais tous en tâche de fond : la page répond pareil, et dans le même temps, pour toutes les adresses.

  • Admin — refus sur le compte d'un superutilisateur : le message affiché (« Seul un superutilisateur peut modifier le compte d'un superutilisateur ») confirmait à un membre du staff que le compte visé était un superutilisateur. Il devient générique (« Action non autorisée », 9 langues), sans changer la règle.

  • Jeton CSRF envoyé à d'autres sites par csrf.js : le script remplace fetch pour ajouter X-CSRF-Token aux requêtes POST/PUT/PATCH/DELETE, sans regarder leur destination. Un fetch en POST vers un autre domaine, lancé depuis une page qui charge le script (script tiers, code du projet), emportait le jeton de session vers ce domaine, et la réponse d'un autre site pouvait remplacer le jeton de la page. Le jeton n'est désormais ajouté, et sa rotation acceptée, que pour une requête vers l'origine de la page. Redémarrez l'application après la mise à jour : le fichier statique change.

  • Un extend!{} de la table des comptes mettait password et is_active dans ses formulaires : le formulaire généré d'un extend! de eihwaz_users (un profil, par exemple) les reprenait des colonnes du compte. Un admin pouvait choisir le mot de passe d'un utilisateur à la création ou à l'édition — et donc le connaître — et désactiver n'importe quel compte, le sien compris, sans passer par les règles des comptes. Aucun des deux n'est plus un champ de formulaire, comme les rangs : un nouveau compte reçoit un mot de passe aléatoire que personne ne connaît (son propriétaire choisit le sien via le lien reçu par mail) et reste inactif jusqu'à ce que son propriétaire l'active ; une édition n'y touche pas, quoi que contienne la requête. Intégré aux formulaires du ModelAdmin typé — pas d'étape de génération.

  • La page d'erreur de debug pouvait afficher un fichier hors de templates/ : sur une erreur de rendu de template, la page de debug lisait et affichait templates/{nom} sans contrôler le nom. Un template choisi à partir des données de la requête (un slug) faisait afficher ../../.env, avec les secrets du projet. Seul un chemin relatif ordinaire sous templates/ est désormais lu ; .. et les chemins absolus sont refusés. La page n'existe qu'avec DEBUG=true : un déploiement de production sans ce réglage n'était pas exposé.

  • Erreurs globales des formulaires rendues sans échappement : les erreurs globales d'un formulaire (erreur d'un clean(), erreur de base citant la valeur saisie) passaient dans la page par un format! derrière le filtre form, déclaré is_safe() : rien ne les échappait (XSS). Elles sont désormais rendues par le template form_errors.html, sous l'échappement automatique de Tera.

  • Formulaire envoyé depuis un autre site refusé avant le handler : un handler qui lit son corps avec un extracteur Axum (Form<T>, Multipart) ne vérifie pas le jeton CSRF, et le middleware laissait passer tout formulaire sans en-tête X-CSRF-Token. Il refuse désormais (403) un formulaire dont le navigateur annonce Sec-Fetch-Site: cross-site ou same-site, toujours actif. Ne remplace pas le jeton : cet en-tête n'est envoyé qu'en HTTPS et sur localhost. Rupture possible pour un formulaire HTML posté depuis un sous-domaine ou par un service tiers : voir csrf_exempt() et la doc CSRF.

  • session_data stocké en clair : le contenu des sessions connectées, jeton CSRF compris, se lisait dans eihwaz_sessions (sauvegarde, injection SQL). Il est maintenant chiffré (XChaCha20-Poly1305, clé dérivée de SECRET_KEY, liée à la ligne). Une ligne en clair, modifiée ou copiée est refusée, et changer SECRET_KEY reconnecte les utilisateurs au redémarrage. Rupture : RuniqueSessionStore::new(db, &secret_key) ; voir le guide de migration, qui vide eihwaz_sessions.

  • Un reset laissait les autres sessions d'un store personnalisé : avec with_session_store(...), un cookie volé avant le reset restait valide, ce store ne pouvant pas être parcouru par utilisateur. La colonne eihwaz_users.session_version, augmentée dans la transaction du reset et par close_user_sessions, ferme toute session ouverte avant, quel que soit le store, sans requête en plus. Rupture : colonne à ajouter, voir le guide de migration.

  • Les scripts add_js sortaient sans hash SRI ni nonce : contrairement à {% static %}, le navigateur ne vérifiait pas leur contenu. La balise porte maintenant integrity et crossorigin quand le fichier est dans les statiques du projet, et le nonce CSP de la requête pour un formulaire obtenu par request.form("nom"). Comme pour {% static %}, redémarrez l'application après avoir modifié un de ces fichiers : son hash est calculé au démarrage.

  • Compte créé sans son activation (projet généré par runique new) : l'inscription écrivait le compte puis son activation séparément ; une erreur entre les deux laissait un compte impossible à activer. Les deux passent dans une même transaction.

  • Des envois pouvaient être validés avant d'être entièrement écrits : parse_multipart n'appelait jamais flush sur le fichier temporaire, donc la validation pouvait lire la taille ou le contenu d'un fichier partiel, et un envoi dépassant la limite d'un champ pouvait passer. Une erreur d'écriture pouvait aussi passer inaperçue et laisser un fichier tronqué. Toutes ses écritures sont maintenant terminées avant qu'il arrive au formulaire, et un échec d'écriture fait refuser la requête.

Ruptures

  • AdminTemplate::with_base et with_bulk_edit supprimés : le premier ne changeait rien (chaque page de l'admin étend elle-même admin_base.html), le second désignait une page qu'aucune route ne servait (l'édition groupée de l'ancien admin, son template et son script supprimés avec). Un layout se remplace par ses blocs (global_block), une page par les with_* ou ModelTemplates::page.
  • utils::constante::admin_key::admin_context supprimé : les clés de contexte des templates de l'ancien admin, qu'aucune page ne lit plus. Les clés d'une page sont maintenant les champs de son contexte (ListContext, DetailContext…), chaque page étant rendue par un test.
  • La production refuse une réinitialisation sans mailer : with_password_reset() sans with_mailer() / with_mailer_from_env() (avec ses SMTP_* définis) arrête le démarrage, comme une URL publique manquante : la page ne pourrait jamais envoyer son lien. Le debug démarre toujours, et la page répond alors à toutes les adresses le même message « pas disponible pour le moment ». Aucun jeton de reset n'est plus créé quand aucun email ne peut le porter.
  • AdminConfig.reset_route remplacé par reset_link : Option<ResetLinkConfig> (la route de la page de reset et la durée du lien), posé par le builder depuis with_password_reset(). None sans lui : le site ne sert pas de page de reset, l'admin n'envoie donc pas de lien au lieu d'en envoyer un qui ne mène nulle part. Les liens de reset sont fabriqués par une seule fonction, auth::password::send_reset_link, pour la page du site comme pour l'admin.
  • decimal déclare sa précision : prix: decimal [max_digits: 10, decimal_places: 2] (max_digits compte tous les chiffres, decimal_places ceux après la virgule : 99999999.99 au plus ici). Un decimal sans les deux est une erreur de compilation : en leur absence, MySQL/MariaDB créaient un DECIMAL(10, 0) et arrondissaient chaque valeur à l'entier (voir Corrigé). Les limites sont celles de MySQL/MariaDB, les plus basses des trois moteurs : 65 chiffres, dont 30 après la virgule.
  • belongs_to déclare son action ON DELETE : belongs_to: Cour via cour_id [cascade]. Sans action, erreur de compilation, comme on_delete dans Django. [no_action] garde le comportement d'avant, sans changement de schéma.
  • Nouvel admin : builder typé ModelAdmin, rangs et droits par compte : admin!{}, le code généré dans src/admins/ et runique start disparaissent. Déclaration dans src/admin.rs, câblage .with_admin(admin::config), cargo run suffit. L'accès repose sur trois rangs de compte (is_superuser, is_dirigeant, is_admin) qui remplacent is_staff sur le compte ; is_staff passe sur les groupes. Toute opération de l'admin passe par une seule porte de décision. Les colonnes de eihwaz_users, eihwaz_groupes, eihwaz_groupes_droits et eihwaz_history changent : voir le guide de migration.
  • Contexte de la liste de l'admin : has_prev, has_next, prev_page, next_page et page_size_options sont remplacés par list_pager, filter_pagers et filter_size_pager ; filter_values devient une liste de { value, active, qs } et active_filters associe chaque colonne à une liste de valeurs. Un template de liste surchargé doit être adapté : voir les clés de contexte.
  • RuniqueError::Template porte sa ligne : Template(String) devient Template { message, line }. RuniqueError::template(&tera_error) remplit les deux.
  • EnvironmentInfo.app_version remplacé par runique_version : l'ancien champ donnait en réalité la version de Runique. La version de l'application arrive dans le contexte de la page de debug (app_name, app_version).

Moteur de formulaire

Ruptures

  • request.form() prend un nom, et ValidationForm::try_new devient try_form : let form: MonForm = request.form("nom"); puis ValidationForm::try_form(form, &request). Le nom est rendu dans un champ caché runique_form (en premier, avant le CSRF). Le formulaire n'est rempli et validé que si la requête renvoie ce même nom : sur une page à plusieurs formulaires, seul celui qui a été envoyé reçoit les données, le contrôle CSRF et ses erreurs ; les autres restent vierges, is_submitted() à false, et deux formulaires peuvent avoir des champs de même nom. Un formulaire écrit à la main doit porter lui-même le champ runique_form.

  • FileField::image() remplacé par MediaField::image() : MediaField regroupe les fichiers dont le contenu est vérifié (voir Ajouté). FileField garde document() et any(). Le type image du DSL suit, sans changement dans model!{}.

  • Runique ne supprime plus le fichier remplacé par un upload : le supprimer rendait impossible de revenir en arrière si une écriture échouait ensuite. Le nettoyage de l'ancien fichier revient au projet.

  • Les id HTML des champs portent le nom du formulaire : id="id_{formulaire}-{champ}" (ainsi que le for des labels et container_{formulaire}-{champ}) : deux formulaires ayant un champ de même nom ne produisent plus d'id en double. Un CSS ou un JS qui cible #id_{champ} doit être mis à jour.

  • Le champ caché CSRF n'a plus d'id : id_csrf_token était en double sur toute page à deux formulaires. csrf.js lit le champ par son name, sans changement.

  • Un champ dont le rendu échoue est affiché à sa place (Render error: …) au lieu de disparaître en silence du rendu champ par champ.

  • RuniqueForm::build et build_with_data retirés : un formulaire construit à la main échappait à ce que request.form("nom") ajoute (honeypot, CSRF imposé sur une écriture, données envoyées). Un handler obtient ses formulaires par request.form::<T>("nom"), et en vide un avec form.clear(). Les tests sans requête utilisent runique::forms::field::build_form_for_test::<T>(tera, csrf_token, "nom") (feature test-utils).

  • RuniqueForm séparé en deux : FormAccess (champs et accesseurs) et RuniqueForm (logique métier) : #[form(schema = …)] génère maintenant lui-même le impl FormAccess, donc impl_form_access!(model) disparaît du code utilisateur — impl RuniqueForm for MonForm {} ne contient plus que clean, customize, les hooks… Un formulaire écrit à la main déplace register_fields et impl_form_access!() dans son propre impl FormAccess for MonForm { … }. FormAccess est dans le prélude ; un code qui importe RuniqueForm par son chemin et appelle get_form() / register_fields() sur un formulaire concret importe aussi FormAccess. impl_form_access!(champ) (accesseurs sur un champ qui ne s'appelle pas form) retiré : inutilisé.

  • save écrit lui-même, sur le formulaire validé ; on_save, before_save, after_save, save_with et SaveContext supprimés : on_save ne faisait rien par défaut, donc save n'écrivait rien tant que le formulaire ne l'implémentait pas. save(&db) et delete(&db) sont maintenant des méthodes du formulaire validé (ValidationForm, rendu par try_form), pour les formulaires reliés à une entité : Runique parcourt les champs et écrit la ligne, par SeaORM hors admin (ActiveModelBehavior du modèle compris), par la porte dans l'admin (droits, propriétaire, historique). La ligne vient de l'URL : {<nom>_<clé>} (nom donné à request.form), sinon {<clé>}, sinon création. Les deux rendent le formulaire : Ok(form) une fois écrit, Err(form) avec l'erreur posée dessus. Un formulaire sans entité n'a pas de save : son handler écrit après try_form. Migration : retirer on_save et les hooks ; la logique métier passe dans clean_field / clean / register_dynamic_fields, les effets extérieurs (email) et les écritures dans d'autres tables dans le handler après un save réussi. Forms::mark_validated (test-utils) supprimé.

  • #[form(schema = …)] : attribut model = … supprimé, FormEntity toujours généré : le formulaire est relié à chemin::Entity (schema = blog → blog::Entity), ce qui donne search!(@MonForm => …) et MonForm::objects sans rien déclarer. schema = doit donc désigner un module de modèle (model!{}, extend!{}) dont l'entité a un ModelMeta. FormEntity::Entity exige ModelMeta (au lieu de EntityTrait) ; model!{} et extend!{} génèrent aussi FormEntity pour leur formulaire.

  • reverse_with_parameters renvoie Result<String, UrlError> (au lieu de Option<String>) et encode chaque valeur : un /, ? ou # dans une valeur ne change plus la route atteinte. Un paramètre manquant (qui laissait {id} dans l'URL) ou inconnu est désormais une erreur au lieu d'être silencieux. {% link %} passe par le même code : un template qui donne un paramètre que sa route ne déclare pas échoue au rendu.

  • .extra_routes() de l'admin supprimé (avec ExtraRoute) : une route personnalisée de l'admin se déclare dans urlpatterns! avec le mot-clé admin (admin "/chemin" => view!{ … }, name = …, for_model = Entite, overrides = AdminView::…). Elle garde son chemin, sans préfixe admin, et le modèle est désigné par son type au lieu d'une clé en chaîne.

  • urlpatterns! renvoie un Routes (au lieu d'un axum::Router) : la déclaration garde ses métadonnées jusqu'au build(), ce qui permet d'y déclarer des routes admin. Une fonction routes() -> Router devient routes() -> Routes ; .routes() du builder accepte un Routes ou un Router, et Routes a merge ainsi que les méthodes de RouterExt (login_required, rate_limit…).

  • admin_page_context : une route admin peut s'afficher sur le layout de l'admin (feuilles de style, menu avec les droits appliqués, barre du haut), sans lui donner aucun droit.

  • Routes admin dans urlpatterns! : admin "/chemin" => view!{ … }, name = …, for_model = Entite (overrides = AdminView::Detail pour remplacer la page de détail générée ; Standalone par défaut). La route garde son chemin et passe par sa propre frontière : un anonyme est renvoyé vers / (vers la connexion de l'admin seulement si la page précédente était une page de l'admin), un compte sans accès à l'admin vers / avec un message, sans que sa session soit fermée.

  • Sentinel et GuardRules supprimés (forms::prisme::{sentinel, rules}, evaluate_rules) : le contrôle d'accès par rang ou par nom de groupe (staff, superuser, roles) passe par la frontière de l'admin et les droits (routes admin de urlpatterns!) ; login_required reste disponible via RouterExt::login_required. Hors de l'admin, un contrôle d'entrée se fait dans le handler. Le pipeline Prisme est désormais Aegis → CSRF.

  • Les erreurs de formulaire ont leur feuille de style : form_errors.html charge css/forms.css depuis les fichiers statiques de Runique quand un formulaire affiche une erreur (sélecteurs dans :where(), couleurs surchargeables par --runique-form-error-color / --runique-form-error-focus).

  • cleaned_string, cleaned_i32… cleaned_bytes supprimés, remplacés par cleaned::<T>() : form.cleaned::<i32>("age"), form.cleaned::<Decimal>("prix"), form.cleaned::<String>("nom"). Chaque helper relisait le texte du champ de son côté, avec ses propres règles, et rien ne reliait le type demandé à la colonne : un prix lu en f64 perdait son exactitude sans un mot. cleaned::<T> lit le texte comme la colonne de T le lit, par la même lecture que l'écriture : i16 refuse 40000, Decimal garde tous ses chiffres. Même liste blanche qu'avant (POST > chemin > query, champs déclarés seulement) ; un mot de passe revient toujours tel que tapé. cleaned_enum et cleaned_enums restent. Remplacez .cleaned_X(nom) par .cleaned::<T>(nom) avec le type que le helper rendait.

  • FormField::to_value() est une méthode obligatoire : un type de champ écrit hors de Runique doit dire comment sa valeur est stockée.

  • required sur une case à cocher signifie désormais « doit être cochée » : il voulait dire que la colonne est NOT NULL, ce qu'une case est toujours (elle répond true ou false), si bien qu'une case de CGU devait être imposée dans clean(). cgu: bool [required] / BooleanField::new("cgu").required() refusent maintenant une case décochée (forms.must_be_checked, 9 langues, tracé), comme le BooleanField de formulaire de Django ; required avec default: true est une erreur de compilation, une case pré-cochée n'étant pas un consentement. Pour migrer : retirer required de chaque bool qui voulait seulement dire NOT NULL (active: bool [required] → active: bool) — sinon la case deviendrait impossible à décocher, y compris dans l'admin. Les formulaires construits depuis le schéma suivent : une colonne bool NOT NULL donne une case facultative, seul un required déclaré la rend obligatoire.

  • Les champs email et phone sont désormais bornés dans le formulaire (254 et 20 caractères), comme leurs colonnes l'étaient déjà : un customize qui en remplace un sans max_length, ou avec un plus grand, panique au démarrage (loosened max_length).

Ajouté

  • Choix du nombre de lignes par page dans les listes de l'admin : un sélecteur (10 / 25 / 50 / 100) en haut à droite, conservé avec le tri, la recherche et les filtres. Seules ces valeurs sont acceptées depuis l'URL ; une taille libre est ignorée.
  • Panneau de filtres paginé : chaque groupe de filtre a ses flèches, et un seul sélecteur (10 / 25 / 50 / 100), sous le titre du panneau, règle le nombre de valeurs de tous les groupes. .filter_size(n) fixe le défaut sur AdminStaging et dans .list(|l| …), sur le modèle de page_size / per_page. Remplace le plafond fixe de 100 valeurs, au-delà duquel les suivantes n'étaient pas proposées.
  • Plusieurs valeurs par filtre, et désélection : filter_x=a&filter_x=b garde les lignes valant a ou b ; plusieurs colonnes se cumulent. Cliquer sur une valeur active la retire seule. Les valeurs restent limitées par le périmètre de la porte, et une colonne en accepte au plus 100 depuis l'URL.
  • Page de debug : la version de l'application (lue dans le Cargo.toml du projet, en debug seulement) et celle de Runique ont chacune leur carte ; pied de page traduit dans les 9 langues et date lisible. Pages d'erreur et page de debug redessinées sur la palette de l'admin.
  • Rang et groupes d'un compte visibles dans l'admin : sur la liste et le détail des comptes, en lecture seule, pour tout compte ayant accès à cette liste. Modifier l'un ou l'autre passe toujours par la porte. Deux requêtes de plus par page, quel que soit le nombre de lignes.
  • request.db_atomic() et save/delete dans une transaction : save et delete acceptent la connexion ou une transaction, pour lier l'écriture du formulaire à celles du handler ; rien n'est réel avant commit(). Un formulaire d'une route admin refuse une transaction : la porte décide sur ce qu'elle lit par la connexion.
  • MediaField::audio() et MediaField::video(), types audio et video du DSL : mêmes règles que MediaField::image(), avec contrôle de la signature du fichier en plus de son extension.
  • csrf.js chargé tout seul : request.render() ajoute une seule balise par page dès qu'elle contient un champ csrf_token, quel que soit le nombre de formulaires, et aucune si la page charge déjà le script.

Corrigé

  • Comptes créés dans l'admin sans date : created_at et updated_at n'étaient écrits que par la CLI ou le formulaire d'un projet. L'entité des comptes et un extend! de eihwaz_users les remplissent maintenant à l'enregistrement, en gardant une date écrite par le code.
  • Champs d'une fiche de l'admin dans un ordre aléatoire : la page suivait la map du template, qui ne garde pas l'ordre. Elle montre maintenant les colonnes de la liste dans leur ordre, puis celles du formulaire, avec la nouvelle clé detail_columns, et les dates et heures en dernier. Une liste automatique les met aussi à la fin ; un display déclaré garde son ordre.
  • session_version sur la fiche d'un compte dans l'admin : le compteur qui révoque les sessions s'affichait parmi les champs quand la ressource des comptes ne déclare pas de .form(). Il n'y est plus lu.
  • Dates de l'admin affichées en ISO brut : les listes et les fiches montraient 2026-11-06T14:58:32.907849. Les dates-heures et les heures s'affichent maintenant comme dans l'historique, depuis une seule fonction (2026-11-06 14:58:32), avec le nouveau filtre display_time.
  • Fichier et ligne d'un même save désaccordés : le fichier est désormais déplacé dans la transaction, entre l'écriture et le commit. Une écriture refusée ne laisse plus de fichier publié, ni un fichier manquant derrière une ligne écrite. Reste un cas : le déplacement réussi puis le commit perdu laisse un fichier orphelin, jamais une ligne sans fichier.
  • Un booléen forgé était lu comme false : un champ case à cocher acceptait n'importe quelle valeur, et tout ce qui n'était pas true/1/on était enregistré comme false — une requête envoyant is_active=banana désactivait le compte sans que personne n'ait envoyé false. Une case à cocher n'accepte plus que ce qu'elle envoie : sa propre valeur ("true" par défaut, .checked_value("on") pour un template écrit à la main dont la case envoie autre chose) ou rien (décochée, que fill() transforme toujours en false). Tout le reste est une erreur de formulaire sous le champ, tracée (forms.validate, nom du champ seulement — jamais la valeur reçue). En dessous du champ, là où une colonne ou le code que model!{} génère pour l'admin lit un booléen, six écritures sont acceptées quelle que soit la casse (true/1/on, false/0/off), le reste est une erreur (une seule fonction partagée, parse_bool).
  • Un formulaire dont une valeur ne tient pas dans sa colonne est invalide : un champ qui a reçu sa colonne (formulaire #[form(schema = …)] ou admin) lit sa valeur dans le type de cette colonne pendant is_valid() ; une valeur qui ne tient pas reçoit une erreur sous le champ, au lieu d'une erreur de base de données à l'écriture. cleaned::<T> lit par la colonne du champ quand il en a une, sinon par le type T.

Corrigé

  • Formulaires envoyés malgré preventDefault() : csrf.js interceptait chaque soumission et renvoyait toujours le formulaire. Un formulaire envoyé en fetch partait donc deux fois (la seconde requête pouvant répondre « doublon » alors que la première avait enregistré), et un contrôle ou une confirmation qui stoppait l'envoi côté navigateur n'avait aucun effet. Le script écoute maintenant sur window, après les autres, et laisse tranquille un formulaire déjà stoppé. Redémarrez l'application après la mise à jour : le fichier statique change.

  • Pages admin sans CSS après une montée de version de Runique : le ?v= des statiques ne suivait que les fichiers du projet, pas ceux de Runique, servis immutable pendant un an. Un CDN ou un navigateur gardait donc l'ancien fichier sous la même adresse, et le navigateur le bloquait parce qu'il ne correspondait plus à son empreinte SRI. Le jeton couvre désormais les deux dossiers.

  • La ligne en erreur n'était jamais surlignée sur la page de debug : le numéro était cherché par une regex (line N) dans le texte de l'erreur, que Tera 2 n'écrit plus sous cette forme. Il est lu dans le span de l'erreur.

  • Version de Rust fausse sur la page de debug : rustc --version était lancé à l'exécution, donc sur la machine qui fait tourner le binaire (souvent sans rustc). Le compilateur est désormais relevé à la compilation.

  • Le sélecteur de taille et la recherche de la liste gardaient l'ancienne URL : hx-get="" faisait ajouter les champs à l'URL courante. Changer de taille ou chercher ne revenait pas à la première page, et l'URL grossissait à chaque frappe. Les deux visent maintenant l'adresse de la liste.

  • decimal arrondi à l'entier sous MySQL/MariaDB : makemigrations créait un DECIMAL nu, que MySQL/MariaDB lisent comme DECIMAL(10, 0) : 9.99 était stocké 10, sans erreur. La colonne est désormais créée avec sa précision déclarée ; sur une table existante, la prochaine makemigrations émet le changement, signalé comme destructif (--force) puisqu'une valeur ayant plus de chiffres que la nouvelle précision serait arrondie ou refusée. Les valeurs déjà arrondies ne se récupèrent pas.

  • Entité, schema() et migration d'accord sur chaque colonne : l'entité générée par model!{} déclare maintenant le type de chaque colonne (#[sea_orm(column_type = …)]) d'après le DSL, au lieu que SeaORM le devine du type Rust : la longueur d'un texte et la précision d'un décimal atteignent l'entité. schema() déclarait en char, timestamp, json_binary et interval des colonnes que la migration crée en texte, date-heure et JSON. Une seule correspondance alimente désormais les trois, vérifiée pour chaque type du DSL.

  • Les liens de reset de l'admin ignoraient token_ttl : ils duraient toujours une heure, quel que soit le réglage de with_password_reset(|pr| pr.token_ttl(…)), et tous les emails de reset, celui du site compris, annonçaient « 1 heure ». L'admin suit désormais le réglage, et les emails affichent la vraie durée du lien en minutes (9 langues).

  • Contrôle au démarrage de l'admin sur un modèle à propriétaire : il exigeait la colonne propriétaire (meta: { owner }) dans fields, alors que la porte la pose au compte qui crée la ligne, quoi que le formulaire envoie. Un modèle à propriétaire peut maintenant déclarer son formulaire sans elle.

  • Colonnes enum sous MySQL/MariaDB : makemigrations écrivait une colonne enum en ColumnType::Enum pour tous les moteurs, que MySQL/MariaDB créent en ENUM(...) natif. Les migrations qui ajoutent ou renomment ensuite une valeur n'y changent rien : la nouvelle valeur était refusée (1265 Data truncated, ou enregistrée vide hors mode strict). La colonne est désormais un type enum Postgres sous Postgres et une chaîne ailleurs, comme l'entité la lit déjà ; les migrations écrites avant restent relues. Sur une table MySQL/MariaDB existante, passer la colonne en chaîne une fois (ALTER TABLE … MODIFY COLUMN … VARCHAR(255) NOT NULL, avec son défaut).

  • search! paniquait hors Postgres : ilike, icontains, iexact et not_ilike passaient par ILIKE, que sea-query ne rend que pour Postgres ; sous MySQL/MariaDB et SQLite, la requête paniquait. Ils deviennent LOWER(colonne) LIKE, comme search_cond!, sur tous les moteurs.

  • Le CLI ignorait DATABASE_URL de l'environnement : runique migration up et create-superuser chargeaient le .env par-dessus l'environnement : un DATABASE_URL donné par une CI, un conteneur ou la ligne de commande était remplacé par celui du .env, et la commande tournait sur cette base. L'environnement passe désormais en premier, comme partout ailleurs ; la langue fixée dans le .env (LANG) garde la priorité sur celle du système.


← Plus récent Plus ancien →