diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 1565ad7..583c760 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -42,6 +42,7 @@ jobs: node tests/frontend/pdf-viewer.test.mjs node tests/frontend/forge-completion.test.mjs node tests/frontend/config-mobile.test.mjs + node tests/frontend/settings-order-avatar.test.mjs - name: Frontend JSDOM tests (PaneManager + Excalidraw + Plugins + AI + SW + Collab + Mobile + Semantic + Desktop + Inline edition) run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea8d8b..80f7a98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.21.0**. +> [Unreleased](#unreleased). La dernière version livrée est **2.22.0**. --- @@ -14,6 +14,32 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.22.0] — 2026-09-23 + +### Ajouté + +- **#113 — avatar utilisateur.** La section « Profil » des Configurations permet de + **choisir / importer une image** (PNG, JPG, WEBP, 8 Mo max) : elle est recadrée en + carré 256 px côté client, validée côté serveur (data-URL + octets magiques, sans SVG) + et persistée sur le compte (`avatar` dans `data/users.json`, exposé par + `GET/PATCH /api/auth/me` et la réponse de login). L'image s'affiche dans le **cercle + du profil en bas de la sidebar** (initiales en repli) et peut être supprimée. UI : + aperçu circulaire avec overlay caméra au survol, boutons Choisir / Supprimer, erreurs + en ligne. Clés i18n FR/EN. + +### Modifié + +- **#113 — ordre des sections Configurations.** La TOC et la page sont réorganisées + dans un ordre naturel et **parfaitement synchronisées** : **Profil en premier**, + puis Sécurité, Thèmes, Recherche, Historique récent, Tags, Fichiers cachés, + Synchronisation, Backend, Diagnostics, IA, Sources connectées, Clés API & MCP, + Push, Webhooks, Partages publics, Plugins et **À propos en dernier**. Les ancres + `cfg-tags` et `cfg-partages-publics` sont désormais portées par leur `
` + (cible de défilement = haut de section). Garde-fous : test statique + `tests/frontend/settings-order-avatar.test.mjs` (ordre TOC = page, pas d'ancre morte). + +--- + ## [2.21.0] — 2026-09-23 ### Ajouté diff --git a/README.fr.md b/README.fr.md index 68854c7..97858ef 100644 --- a/README.fr.md +++ b/README.fr.md @@ -4,7 +4,7 @@ **Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive. -[![Version](https://img.shields.io/badge/Version-2.21.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.22.0-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -975,8 +975,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.21.0). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.22.0). --- -*Projet : ObsiGate | Version : 2.21.0 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.22.0 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 968a565..6e81d7d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface. -[![Version](https://img.shields.io/badge/Version-2.21.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.22.0-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1150,8 +1150,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.21.0). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.22.0). --- -*Project: ObsiGate | Version: 2.21.0 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.22.0 | Last updated: September 2026* diff --git a/VERSION b/VERSION index db65e21..f48f82f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.21.0 +2.22.0 diff --git a/backend/auth/router.py b/backend/auth/router.py index 581e075..e9dfef9 100644 --- a/backend/auth/router.py +++ b/backend/auth/router.py @@ -2,6 +2,8 @@ # All /api/auth/* endpoints: login, logout, refresh, me, change-password, # and admin user CRUD. +import base64 +import binascii import logging import re @@ -109,6 +111,43 @@ class UpdateUserRequest(BaseModel): return validate_password_strength(v) +# ── Profile avatar (#113) ─────────────────────────────────────────── + +#: Avatar data-URL pattern — PNG/JPEG/WebP only (no SVG: XSS surface). +_AVATAR_DATA_URL_RE = re.compile( + r"^data:image/(?:png|jpeg|webp);base64,[A-Za-z0-9+/]+={0,2}$" +) +#: ~300 KB of base64 payload (a 256px JPEG is ~15 KB; generous headroom). +_AVATAR_MAX_CHARS = 400_000 + + +def _validate_avatar(data_url: str) -> str | None: + """Validate an avatar data-URL for storage on the user profile. + + Returns the normalized data-URL, or ``None`` when clearing the avatar + (empty string). Raises ``HTTPException(400)`` on anything else. + """ + if data_url == "": + return None + if len(data_url) > _AVATAR_MAX_CHARS: + raise HTTPException(400, "Avatar image too large") + if not _AVATAR_DATA_URL_RE.match(data_url): + raise HTTPException(400, "Avatar must be a PNG, JPEG or WebP data URL") + try: + raw = base64.b64decode(data_url.split(",", 1)[1], validate=True) + except (ValueError, binascii.Error) as exc: # pragma: no cover — regex guards + raise HTTPException(400, "Avatar payload is not valid base64") from exc + # Confirm the decoded bytes really are a supported image (magic numbers). + is_png = raw.startswith(b"\x89PNG\r\n\x1a\n") + is_jpeg = raw.startswith(b"\xff\xd8\xff") + is_webp = ( + len(raw) >= 12 and raw[:4] == b"RIFF" and raw[8:12] == b"WEBP" + ) + if not (is_png or is_jpeg or is_webp): + raise HTTPException(400, "Avatar payload is not a PNG, JPEG or WebP image") + return data_url + + # ── Public endpoints ────────────────────────────────────────────────── @router.get("/status") @@ -221,6 +260,7 @@ def _issue_tokens(user: dict, username: str, remember_me: bool, response: Respon "display_name": user["display_name"], "role": user["role"], "vaults": user["vaults"], + "avatar": user.get("avatar"), }, } @@ -343,6 +383,7 @@ async def get_me(current_user=Depends(require_auth)): "vaults": current_user["vaults"], "language": current_user.get("language", "fr"), "last_login": current_user.get("last_login"), + "avatar": current_user.get("avatar"), } @@ -350,19 +391,23 @@ class UpdateMeRequest(BaseModel): """Fields the user can update on their own profile.""" display_name: str | None = None language: str | None = None + #: Image data-URL (PNG/JPEG/WebP), or ``""`` to remove the avatar (#113). + avatar: str | None = None @router.patch("/me") async def patch_me(req: UpdateMeRequest, current_user=Depends(require_auth)): - """Update current user's profile fields (display_name, language).""" + """Update current user's profile fields (display_name, language, avatar).""" from .user_store import update_user - updates = {} + updates: dict[str, object] = {} if req.display_name is not None: updates["display_name"] = req.display_name if req.language is not None: if req.language not in ("fr", "en"): raise HTTPException(400, "language must be 'fr' or 'en'") updates["language"] = req.language + if req.avatar is not None: + updates["avatar"] = _validate_avatar(req.avatar) if not updates: raise HTTPException(400, "No fields to update") updated = update_user(current_user["username"], updates) @@ -373,6 +418,7 @@ async def patch_me(req: UpdateMeRequest, current_user=Depends(require_auth)): "vaults": updated["vaults"], "language": updated.get("language", "fr"), "last_login": updated.get("last_login"), + "avatar": updated.get("avatar"), } diff --git a/backend/auth/user_store.py b/backend/auth/user_store.py index e8da6ce..2562fad 100644 --- a/backend/auth/user_store.py +++ b/backend/auth/user_store.py @@ -96,6 +96,7 @@ def create_user( "vaults": vaults or [], "active": True, "language": "fr", # default UI language + "avatar": None, # profile picture data-URL (#113) "created_at": datetime.now(timezone.utc).isoformat(), "password_changed_at": datetime.now(timezone.utc).timestamp(), "last_login": None, diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index d5fabbb..a9acc01 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.21.0" +version = "2.22.0" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 56c371c..4ef8ea3 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.21.0" +version = "2.22.0" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 3d192c8..d79d03b 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.21.0", + "version": "2.22.0", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/GUIDES/API_REST.md b/docs/GUIDES/API_REST.md index 88292da..019cad4 100644 --- a/docs/GUIDES/API_REST.md +++ b/docs/GUIDES/API_REST.md @@ -221,6 +221,10 @@ Gestion : | `/api/auth/admin/users/{u}` | Modifier / supprimer | PATCH/DELETE | Admin | | `/api/admin/stats` · `/audit` · `/backup-stats` · `/stream` | Monitoring admin | GET | Admin | +> `PATCH /api/auth/me` accepte `{"avatar": ""}` (PNG/JPEG/WebP, 400 000 +> caractères max, octets magiques contrôlés) ; `{"avatar": ""}` supprime la photo. +> La valeur est renvoyée par `GET /api/auth/me` et par le payload `user` du login. + ### 3.10 Partage, webhooks, conflits, plugins, push | Endpoint | Description | Méthode | diff --git a/docs/GUIDES/AUTHENTIFICATION_SECURITE.md b/docs/GUIDES/AUTHENTIFICATION_SECURITE.md index 53dabf8..fd3325f 100644 --- a/docs/GUIDES/AUTHENTIFICATION_SECURITE.md +++ b/docs/GUIDES/AUTHENTIFICATION_SECURITE.md @@ -67,6 +67,11 @@ CHANGE THIS PASSWORD on first login! Changez-le immédiatement (menu profil → *Changer le mot de passe*). +Vous pouvez aussi ajouter une **photo de profil** : *Configurations → Profil → +Choisir une image* (PNG, JPG ou WEBP, 8 Mo maximum — recadrée en carré 256 px). +Elle remplace les initiales dans le cercle du compte en bas de la sidebar et peut +être supprimée à tout moment depuis la même section. + --- ## 3. Gestion des utilisateurs diff --git a/docs/GUIDES/PRISE_EN_MAIN.md b/docs/GUIDES/PRISE_EN_MAIN.md index c592262..9198642 100644 --- a/docs/GUIDES/PRISE_EN_MAIN.md +++ b/docs/GUIDES/PRISE_EN_MAIN.md @@ -142,6 +142,12 @@ Elle regroupe les vues principales via des icônes : Un champ **« Filtrer fichiers… »** restreint l'arborescence en temps réel, et le bouton **Aa** ajuste l'affichage des libellés. +En bas de la sidebar (si l'authentification est activée), la **section compte** +affiche votre avatar (ou vos initiales), votre nom et votre rôle ; un clic ouvre +le profil. La photo se choisit dans **Configurations → Profil** (*Choisir une +image* : PNG, JPG ou WEBP — recadrée en carré 256 px, affichée dans le cercle de +la sidebar) ; le bouton *Se déconnecter* est juste à côté. + ### 5.3 La zone de contenu Elle affiche l'onglet actif : tableau de bord **Statistiques**, **Bookmarks**, diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9a040c6..4b5bcae 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.21.0 | **Dernière mise à jour :** 2026-09-23 +> **Version :** 2.22.0 | **Dernière mise à jour :** 2026-09-23 > **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** @@ -185,6 +185,7 @@ | 110 | Lecteur média persistant « Now Playing » — élément partagé téléporté (inline ⇄ dock), Media Session, mini-vidéo PiP, mobile, reprise | 2.19.0 | [features/media-viewers-109.md](./features/media-viewers-109.md) | | 111 | Visionneuse d'images — navigation fluide : image ajustée au cadre, navigation en place (cache annuaire + préchargement), pellicule persistante, flèches latérales au survol | 2.20.0 | [features/image-navigation-111.md](./features/image-navigation-111.md) | | 112 | En-tête allégé & compte en sidebar — version dans le menu Options, retrait utilisateur/déconnexion du header, section compte en bas de la sidebar, pellicule d'images défilable (molette + flèches) | 2.21.0 | [features/header-user-sidebar-112.md](./features/header-user-sidebar-112.md) | +| 113 | Configuration — ordre naturel des sections (Profil 1er, À propos dernier, TOC = page) & avatar utilisateur (import PNG/JPG/WEBP, persistance serveur, cercle sidebar) | 2.22.0 | [features/settings-order-avatar-113.md](./features/settings-order-avatar-113.md) | --- @@ -192,7 +193,7 @@ | Priorité | Items | Effort total estimé | |---|---|---| -| ✅ Complété | #1 → #59, #61–72, #74–76, #78–84, #86, #88–93, #94–100, #102–112, #92 | ~130 jours réalisés | +| ✅ Complété | #1 → #59, #61–72, #74–76, #78–84, #86, #88–93, #94–100, #102–113, #92 | ~131 jours réalisés | | 🔵 P2 restant | #77 Desktop : signature de code (non retenue), 6 tests E2E **manuels** ([protocole](./DESKTOP_E2E_CHECKLIST.md)) | ~0,5-1 jour | | ⚪ P4 restant | #73 Sync (6-8j) | 6-8 jours | | ⚪ P0/P1 restant | #85, #87 Refonte architecturale, CI/CD (BUG-035 → BUG-040 corrigés, #86 livré) | ~11-17 jours | diff --git a/docs/features/settings-order-avatar-113.md b/docs/features/settings-order-avatar-113.md new file mode 100644 index 0000000..057539a --- /dev/null +++ b/docs/features/settings-order-avatar-113.md @@ -0,0 +1,94 @@ +# #113 — Ordre naturel des sections Configurations & avatar utilisateur + +> **Version livrée :** 2.22.0 · **Statut :** 🟢 · **Impact :** 🟡 +> **Zone :** frontend (`index.html`, `frontend/js/config.js`, `frontend/js/auth.js`, +> `frontend/style.css`, i18n FR/EN) + backend (`backend/auth/router.py`, +> `backend/auth/user_store.py`) + CI (`.gitea/workflows/ci.yml`). + +## Contexte + +Deux irritants sur la page **Configurations** : + +- l'ordre des sections était historique et peu naturel (Recherche en tête, Profil + noyé en position 11, À propos au milieu) ; +- la section **Profil** ne permettait pas de personnaliser l'image affichée dans le + cercle du compte en bas de la sidebar (initiales uniquement). + +## Ce qui a été livré + +### A. Ordre naturel des sections (TOC = page) + +Nouvel ordre, appliqué **à la liste `