v3 : liens d'équipe, publication sur git.open.us.org, huitième relecture

Liens d'équipe (Yvv, 2026-10-21) : un lien réutilisable jusqu'à une date
et pour un nombre de personnes, révocable. Chaque ouverture demande un nom
et crée sa propre identité d'invitation, jamais administratrice ; un nom
déjà pris est refusé ; une personne déjà connectée reçoit les groupes.
trustinWallet montre par quel lien chaque identité est venue, et donne un
nouveau lien de connexion à une personne invitée (autre appareil).
Migration additive 0002 (table team_links, colonne identities.team_link).

Publication : yvv/libredecision-textes sur git.open.us.org, pour
l'instant (un dépôt pour l'ouverture, puis le dépôt officiel, viendront).

Huitième relecture (c3d6bf8 : ok), points corrigés :
- une négation n'est jamais coupée ni formée par collage (« ja mais ») ;
- caractères invisibles : tout l'ensemble Unicode des ignorables et les
  contrôles autres que saut de ligne et tabulation ;
- sauts de ligne et indentation font partie de la forme (listes) ;
- un titre tient sur une ligne.

Boucle : 65 tests Rust, svelte-check, 9 vitest, e2e 13/13 (C12 ajouté).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-21 08:12:21 +02:00
co-authored by Claude Opus 5.5
parent c3d6bf88b9
commit fd19dbf4ec
29 changed files with 938 additions and 85 deletions
+7 -1
View File
@@ -34,6 +34,10 @@ pub async fn overview(State(state): State<AppState>, User(user): User) -> AppRes
.await?;
let last_seen: Vec<(String, Option<String>)> =
sqlx::query_as("SELECT id, last_seen_at FROM identities").fetch_all(&state.db).await?;
let came_by: Vec<(String, String)> =
sqlx::query_as("SELECT i.id, t.label FROM identities i JOIN team_links t ON t.token_hash = i.team_link")
.fetch_all(&state.db)
.await?;
let invitations: Vec<(String, String, bool, String, String, Option<String>)> = sqlx::query_as(
"SELECT token_hash, label, admin, groups, expires_at, identity_id FROM invitations
WHERE used_at IS NULL AND expires_at > ? ORDER BY created_at DESC",
@@ -55,7 +59,9 @@ pub async fn overview(State(state): State<AppState>, User(user): User) -> AppRes
"identities": identities.iter().map(|i| json!({
"id": i.id, "kind": i.kind, "displayName": i.display_name, "g1Member": i.g1_member, "admin": i.admin,
"lastSeenAt": last_seen.iter().find(|(id, _)| id == &i.id).and_then(|(_, at)| at.clone()),
"teamLink": came_by.iter().find(|(id, _)| id == &i.id).map(|(_, label)| label.clone()),
})).collect::<Vec<_>>(),
"teamLinks": crate::team::list(&state).await?,
"invitations": invitations.iter().map(|(hash, label, admin, groups, expires, identity)| json!({
"id": hash, "label": label, "admin": admin,
"groups": serde_json::from_str::<Vec<String>>(groups).unwrap_or_default(),
@@ -126,7 +132,7 @@ pub async fn invite(
) -> AppResult<Json<serde_json::Value>> {
admin_only(&user)?;
let label = b.label.trim();
if label.is_empty() || label.chars().count() > 60 {
if label.is_empty() || label.chars().count() > 60 || ld_engine::edit::has_hidden_controls(label) {
return Err(AppError::Invalid("label"));
}
// A new link for an existing person is for invitation identities only: a link