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 remplacefetchpour ajouterX-CSRF-Tokenaux requêtesPOST/PUT/PATCH/DELETE, sans regarder leur destination. UnfetchenPOSTvers 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 mettaitpasswordetis_activedans ses formulaires : le formulaire généré d'unextend!deeihwaz_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 duModelAdmintypé — 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 affichaittemplates/{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 soustemplates/est désormais lu ;..et les chemins absolus sont refusés. La page n'existe qu'avecDEBUG=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 unformat!derrière le filtreform, déclaréis_safe(): rien ne les échappait (XSS). Elles sont désormais rendues par le templateform_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êteX-CSRF-Token. Il refuse désormais (403) un formulaire dont le navigateur annonceSec-Fetch-Site: cross-siteousame-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 : voircsrf_exempt()et la doc CSRF. -
session_datastocké en clair : le contenu des sessions connectées, jeton CSRF compris, se lisait danseihwaz_sessions(sauvegarde, injection SQL). Il est maintenant chiffré (XChaCha20-Poly1305, clé dérivée deSECRET_KEY, liée à la ligne). Une ligne en clair, modifiée ou copiée est refusée, et changerSECRET_KEYreconnecte les utilisateurs au redémarrage. Rupture :RuniqueSessionStore::new(db, &secret_key); voir le guide de migration, qui videeihwaz_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 colonneeihwaz_users.session_version, augmentée dans la transaction du reset et parclose_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_jssortaient sans hash SRI ni nonce : contrairement à{% static %}, le navigateur ne vérifiait pas leur contenu. La balise porte maintenantintegrityetcrossoriginquand le fichier est dans les statiques du projet, et le nonce CSP de la requête pour un formulaire obtenu parrequest.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_multipartn'appelait jamaisflushsur 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_baseetwith_bulk_editsupprimés : le premier ne changeait rien (chaque page de l'admin étend elle-mêmeadmin_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 leswith_*ouModelTemplates::page.utils::constante::admin_key::admin_contextsupprimé : 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()sanswith_mailer()/with_mailer_from_env()(avec sesSMTP_*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_routeremplacé parreset_link:Option<ResetLinkConfig>(la route de la page de reset et la durée du lien), posé par le builder depuiswith_password_reset().Nonesans 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.decimaldéclare sa précision :prix: decimal [max_digits: 10, decimal_places: 2](max_digitscompte tous les chiffres,decimal_placesceux après la virgule : 99999999.99 au plus ici). Undecimalsans les deux est une erreur de compilation : en leur absence, MySQL/MariaDB créaient unDECIMAL(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_todéclare son actionON DELETE:belongs_to: Cour via cour_id [cascade]. Sans action, erreur de compilation, commeon_deletedans 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é danssrc/admins/etrunique startdisparaissent. Déclaration danssrc/admin.rs, câblage.with_admin(admin::config),cargo runsuffit. L'accès repose sur trois rangs de compte (is_superuser,is_dirigeant,is_admin) qui remplacentis_staffsur le compte ;is_staffpasse sur les groupes. Toute opération de l'admin passe par une seule porte de décision. Les colonnes deeihwaz_users,eihwaz_groupes,eihwaz_groupes_droitseteihwaz_historychangent : voir le guide de migration. - Contexte de la liste de l'admin :
has_prev,has_next,prev_page,next_pageetpage_size_optionssont remplacés parlist_pager,filter_pagersetfilter_size_pager;filter_valuesdevient une liste de{ value, active, qs }etactive_filtersassocie chaque colonne à une liste de valeurs. Un template de liste surchargé doit être adapté : voir les clés de contexte. RuniqueError::Templateporte sa ligne :Template(String)devientTemplate { message, line }.RuniqueError::template(&tera_error)remplit les deux.EnvironmentInfo.app_versionremplacé parrunique_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, etValidationForm::try_newdevienttry_form:let form: MonForm = request.form("nom");puisValidationForm::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 champrunique_form. -
FileField::image()remplacé parMediaField::image():MediaFieldregroupe les fichiers dont le contenu est vérifié (voir Ajouté).FileFieldgardedocument()etany(). Le typeimagedu DSL suit, sans changement dansmodel!{}. -
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
idHTML des champs portent le nom du formulaire :id="id_{formulaire}-{champ}"(ainsi que lefordes labels etcontainer_{formulaire}-{champ}) : deux formulaires ayant un champ de même nom ne produisent plus d'iden 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.jslit le champ par sonname, 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::buildetbuild_with_dataretirés : un formulaire construit à la main échappait à ce querequest.form("nom")ajoute (honeypot, CSRF imposé sur une écriture, données envoyées). Un handler obtient ses formulaires parrequest.form::<T>("nom"), et en vide un avecform.clear(). Les tests sans requête utilisentrunique::forms::field::build_form_for_test::<T>(tera, csrf_token, "nom")(featuretest-utils). -
RuniqueFormséparé en deux :FormAccess(champs et accesseurs) etRuniqueForm(logique métier) :#[form(schema = …)]génère maintenant lui-même leimpl FormAccess, doncimpl_form_access!(model)disparaît du code utilisateur —impl RuniqueForm for MonForm {}ne contient plus queclean,customize, les hooks… Un formulaire écrit à la main déplaceregister_fieldsetimpl_form_access!()dans son propreimpl FormAccess for MonForm { … }.FormAccessest dans le prélude ; un code qui importeRuniqueFormpar son chemin et appelleget_form()/register_fields()sur un formulaire concret importe aussiFormAccess.impl_form_access!(champ)(accesseurs sur un champ qui ne s'appelle pasform) retiré : inutilisé. -
saveécrit lui-même, sur le formulaire validé ;on_save,before_save,after_save,save_withetSaveContextsupprimés :on_savene faisait rien par défaut, doncsaven'écrivait rien tant que le formulaire ne l'implémentait pas.save(&db)etdelete(&db)sont maintenant des méthodes du formulaire validé (ValidationForm, rendu partry_form), pour les formulaires reliés à une entité : Runique parcourt les champs et écrit la ligne, par SeaORM hors admin (ActiveModelBehaviordu 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 desave: son handler écrit aprèstry_form. Migration : retireron_saveet les hooks ; la logique métier passe dansclean_field/clean/register_dynamic_fields, les effets extérieurs (email) et les écritures dans d'autres tables dans le handler après unsaveréussi.Forms::mark_validated(test-utils) supprimé. -
#[form(schema = …)]: attributmodel = …supprimé,FormEntitytoujours généré : le formulaire est relié àchemin::Entity(schema = blog→blog::Entity), ce qui donnesearch!(@MonForm => …)etMonForm::objectssans rien déclarer.schema =doit donc désigner un module de modèle (model!{},extend!{}) dont l'entité a unModelMeta.FormEntity::EntityexigeModelMeta(au lieu deEntityTrait) ;model!{}etextend!{}génèrent aussiFormEntitypour leur formulaire. -
reverse_with_parametersrenvoieResult<String, UrlError>(au lieu deOption<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é (avecExtraRoute) : une route personnalisée de l'admin se déclare dansurlpatterns!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 unRoutes(au lieu d'unaxum::Router) : la déclaration garde ses métadonnées jusqu'aubuild(), ce qui permet d'y déclarer des routesadmin. Une fonctionroutes() -> Routerdevientroutes() -> Routes;.routes()du builder accepte unRoutesou unRouter, etRoutesamergeainsi que les méthodes deRouterExt(login_required,rate_limit…). -
admin_page_context: une routeadminpeut 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
admindansurlpatterns!:admin "/chemin" => view!{ … }, name = …, for_model = Entite(overrides = AdminView::Detailpour remplacer la page de détail générée ;Standalonepar 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
GuardRulessupprimé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 (routesadmindeurlpatterns!) ;login_requiredreste disponible viaRouterExt::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.htmlchargecss/forms.cssdepuis 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_bytessupprimés, remplacés parcleaned::<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 enf64perdait son exactitude sans un mot.cleaned::<T>lit le texte comme la colonne deTle lit, par la même lecture que l'écriture :i16refuse40000,Decimalgarde 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_enumetcleaned_enumsrestent. 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. -
requiredsur 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épondtrueoufalse), si bien qu'une case de CGU devait être imposée dansclean().cgu: bool [required]/BooleanField::new("cgu").required()refusent maintenant une case décochée (forms.must_be_checked, 9 langues, tracé), comme leBooleanFieldde formulaire de Django ;requiredavecdefault: trueest une erreur de compilation, une case pré-cochée n'étant pas un consentement. Pour migrer : retirerrequiredde chaqueboolqui 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 colonneboolNOT NULL donne une case facultative, seul unrequireddéclaré la rend obligatoire. -
Les champs
emailetphonesont désormais bornés dans le formulaire (254 et 20 caractères), comme leurs colonnes l'étaient déjà : uncustomizequi en remplace un sansmax_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 surAdminStaginget dans.list(|l| …), sur le modèle depage_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=bgarde les lignes valantaoub; 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.tomldu 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()etsave/deletedans une transaction :saveetdeleteacceptent la connexion ou une transaction, pour lier l'écriture du formulaire à celles du handler ; rien n'est réel avantcommit(). Un formulaire d'une routeadminrefuse une transaction : la porte décide sur ce qu'elle lit par la connexion.MediaField::audio()etMediaField::video(), typesaudioetvideodu DSL : mêmes règles queMediaField::image(), avec contrôle de la signature du fichier en plus de son extension.csrf.jschargé tout seul :request.render()ajoute une seule balise par page dès qu'elle contient un champcsrf_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_atetupdated_atn'étaient écrits que par la CLI ou le formulaire d'un projet. L'entité des comptes et unextend!deeihwaz_usersles 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 ; undisplaydéclaré garde son ordre. session_versionsur 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 filtredisplay_time. - Fichier et ligne d'un même
savedé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 pastrue/1/onétait enregistré commefalse— une requête envoyantis_active=bananadé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, quefill()transforme toujours enfalse). 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 quemodel!{}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 pendantis_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 typeT.
Corrigé
-
Formulaires envoyés malgré
preventDefault():csrf.jsinterceptait chaque soumission et renvoyait toujours le formulaire. Un formulaire envoyé enfetchpartait 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 surwindow, 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, servisimmutablependant 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 sansrustc). 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. -
decimalarrondi à l'entier sous MySQL/MariaDB :makemigrationscréait unDECIMALnu, que MySQL/MariaDB lisent commeDECIMAL(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 prochainemakemigrationsé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 parmodel!{}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 enchar,timestamp,json_binaryetintervaldes 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 dewith_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 }) dansfields, 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 enColumnType::Enumpour tous les moteurs, que MySQL/MariaDB créent enENUM(...)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,iexactetnot_ilikepassaient parILIKE, que sea-query ne rend que pour Postgres ; sous MySQL/MariaDB et SQLite, la requête paniquait. Ils deviennentLOWER(colonne) LIKE, commesearch_cond!, sur tous les moteurs. -
Le CLI ignorait
DATABASE_URLde l'environnement :runique migration upetcreate-superuserchargeaient le.envpar-dessus l'environnement : unDATABASE_URLdonné 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.