diff --git a/CHANGELOG.md b/CHANGELOG.md index 8deb468..d09bd4d 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.67.0**. +> [Unreleased](#unreleased). La dernière version livrée est **2.67.1**. --- @@ -14,6 +14,10 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.67.1] — 2026-10-10 + +--- + ## [2.67.0] — 2026-10-10 ### Ajouté @@ -29,6 +33,12 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). couvre les partages dirigés reçus (résultats `home-/Partage/…`, clic → page de partage, jamais l'éditeur du vault d'autrui). - Cache d'icônes partagé invalidé à la création/révocation. + - **Itération 3 :** correction « Directory not found: Partage » (le dossier + virtuel ne déclenche plus la navigation) ; un fichier reçu s'ouvre dans + l'application en onglet — arbre, recherche et dashboard — via résolution + serveur `home-/Partage/` vers le fichier source + (`/api/file`, raw, download) ; un `path` vide/null est rejeté (400) à la + création d'un partage. --- diff --git a/README.fr.md b/README.fr.md index 0e17edb..24308e5 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.67.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.67.1-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/) @@ -977,8 +977,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.67.0). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.67.1). --- -*Projet : ObsiGate | Version : 2.67.0 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.67.1 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 8b67fbb..8b14049 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.67.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.67.1-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/) @@ -1152,8 +1152,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.67.0). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.67.1). --- -*Project: ObsiGate | Version: 2.67.0 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.67.1 | Last updated: September 2026* diff --git a/VERSION b/VERSION index ed283c8..9db2e99 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.67.0 +2.67.1 diff --git a/backend/routers/files_read.py b/backend/routers/files_read.py index 18a34de..b08ad3b 100644 --- a/backend/routers/files_read.py +++ b/backend/routers/files_read.py @@ -45,9 +45,32 @@ from backend.services.files import read_raw_file from backend.services.mutations import file_revision from backend.services.paths import resolve_safe_path from backend.services.vaults import browse_directory, get_vault_root +from backend.share import list_shares logger = logging.getLogger("obsigate") + +def _resolve_shared_file(vault_name: str, path: str, username: str | None): + """#196 — resolve ``home-/Partage/`` to the real source file. + + Returns ``(real_vault, real_path)`` when *path* is a received share + mounted in the user's personal folder, else ``None``. Read-only: only a + recipient (or the share creator, whose own file is already accessible) + gets a mapping — a share directed to someone else never resolves here. + """ + if not username or not vault_name.startswith("home-") or not path.startswith("Partage/"): + return None + owner = vault_name[len("home-"):] + if owner != username: + return None + name = path.split("/", 1)[1] + for s in list_shares(user=username): + if s.get("created_by") == username: + continue + if (s.get("path") or "").split("/")[-1] == name: + return s["vault"], s["path"] + return None + # Map file extensions to highlight.js language hints EXT_TO_LANG = { ".py": "python", ".js": "javascript", ".ts": "typescript", @@ -104,6 +127,9 @@ async def api_file_raw(vault_name: str, path: str = Query(..., description="Rela Returns: ``FileRawResponse`` with vault, path, and raw text content. """ + shared = _resolve_shared_file(vault_name, path, current_user.get("username")) + if shared: + vault_name, path = shared if not check_vault_access(vault_name, current_user): raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'") return read_raw_file(vault_name, path) @@ -120,6 +146,9 @@ async def api_file_download(vault_name: str, path: str = Query(..., description= Returns: ``FileResponse`` with ``application/octet-stream`` content-type. """ + shared = _resolve_shared_file(vault_name, path, current_user.get("username")) + if shared: + vault_name, path = shared if not check_vault_access(vault_name, current_user): raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'") vault_data = get_vault_data(vault_name) @@ -305,8 +334,17 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative Returns: ``FileContentResponse`` with HTML, metadata, and tags. """ + # #196 — fichier reçu par partage dirigé : home-/Partage/ + # est résolu vers le fichier source (lecture seule, viewer standard). + # Résolu AVANT l'ACL vault : le dossier est virtuel, l'autorisation réelle + # est l'appartenance au partage (vérifiée dans le resolver). + shared = _resolve_shared_file(vault_name, path, current_user.get("username")) + if shared: + vault_name, path = shared + if not check_vault_access(vault_name, current_user): raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'") + vault_data = get_vault_data(vault_name) if not vault_data: raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found") diff --git a/backend/routers/sharing.py b/backend/routers/sharing.py index 513d775..b73cc48 100644 --- a/backend/routers/sharing.py +++ b/backend/routers/sharing.py @@ -88,7 +88,9 @@ async def api_share_create( """ if not check_vault_access(vault_name, current_user): raise HTTPException(403, f"Accès refusé à la vault '{vault_name}'") - path = body.get("path", "") + path = body.get("path") or "" + if not path: + raise HTTPException(400, "Chemin de fichier requis") expires = body.get("expires_in_hours") # #196 — directed share: validate recipients before creating anything. recipients = body.get("shared_with") or [] diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index f087db5..c691c8b 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.67.0" +version = "2.67.1" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 2956c9a..d42eb46 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.67.0" +version = "2.67.1" 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 b45de36..ade8686 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.67.0", + "version": "2.67.1", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6e4b134..72ed8c2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.67.0 | **Dernière mise à jour :** 2026-10-10 +> **Version :** 2.67.1 | **Dernière mise à jour :** 2026-10-10 > **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)** @@ -57,6 +57,13 @@ jamais l'éditeur du vault d'autrui) ; cache front invalidé à la création/révocation. +2 tests (recherche trouvée par le destinataire, aucune fuite vers un non-destinataire). + - [x] **Itération 3 (2026-10-11)** : le dossier Partage ne déclenche plus la + navigation (dossier virtuel — corrige « Directory not found: Partage ») ; + un fichier reçu s'ouvre **dans l'application en onglet** (arbre, + recherche, dashboard) via la résolution serveur + `home-/Partage/` → fichier source dans `/api/file` + (+ raw, download) ; validation `path` non-nulle à la création d'un + partage. +2 tests. ### 194. Dossier personnel par utilisateur (`/Home/`) diff --git a/docs/features/directed-shares-196.md b/docs/features/directed-shares-196.md index 656c2f7..b2d732d 100644 --- a/docs/features/directed-shares-196.md +++ b/docs/features/directed-shares-196.md @@ -105,3 +105,19 @@ Décisions : +2 tests : `test_search_finds_received_share_for_recipient`, `test_search_does_not_leak_share_to_other_user`. + + +## Itération 3 — ouverture en onglet (2026-10-11) + +- **Résolution serveur** : `_resolve_shared_file()` (`backend/routers/files_read.py`) + mappe `home-/Partage/` vers le fichier source pour les routes + `/api/file`, `/api/file/raw`, `/api/file/download`. Résolu AVANT l'ACL vault + (le home virtuel peut ne pas être dans `user.vaults`) — l'autorisation réelle + est l'appartenance au partage, vérifiée dans le resolver (destinataire seul). +- **Front** : clic dossier Partage → simple dépliage (plus de `openNav`, fix + « Directory not found ») ; clic fichier reçu → `TabManager.openPreview/openPersistent` + (arbre, recherche, dashboard) — même parcours qu'un fichier ordinaire. +- `POST /api/share` : `path` vide/null → 400 (un share `path: None` cassait + `/api/shares` en 500). ++2 tests : ouverture applicative du fichier reçu (file + raw), résolution +scopée par utilisateur (home d'autrui → 403). diff --git a/frontend/js/dashboard.js b/frontend/js/dashboard.js index f811e27..1df377c 100644 --- a/frontend/js/dashboard.js +++ b/frontend/js/dashboard.js @@ -68,8 +68,10 @@ const DashboardSharedWidget = { : ` `; + const cardVault = received ? `home-${me}` : s.vault; + const cardPath = received ? `Partage/${(s.path || "").split("/").pop()}` : s.path; return ` -
+
${escapeHtml(s.path.split("/").pop().replace(/\\.md$/i, ""))} @@ -93,10 +95,7 @@ const DashboardSharedWidget = { grid.querySelectorAll(".shared-open-btn").forEach(b => b.addEventListener("click", (e) => { e.stopPropagation(); const card = b.closest(".shared-card"); - if (card && card.dataset.received === "1" && card.dataset.token) { - window.open(`/s/${card.dataset.token}`, "_blank"); - return; - } + // #196 : onglet applicatif — /api/file résout Partage/ vers la source. if (card) TabManager.openPreview(card.dataset.vault, card.dataset.path); })); grid.querySelectorAll(".shared-revoke-btn").forEach(b => b.addEventListener("click", async (e) => { @@ -106,10 +105,6 @@ const DashboardSharedWidget = { this.load(); })); grid.querySelectorAll(".shared-card").forEach(card => card.addEventListener("click", () => { - if (card.dataset.received === "1" && card.dataset.token) { - window.open(`/s/${card.dataset.token}`, "_blank"); - return; - } TabManager.openPreview(card.dataset.vault, card.dataset.path); })); } catch (err) { if (empty) empty.style.display = ""; } diff --git a/frontend/js/search.js b/frontend/js/search.js index 60cd9ea..75e9565 100644 --- a/frontend/js/search.js +++ b/frontend/js/search.js @@ -911,9 +911,10 @@ export function renderSearchResults(data, query, tagFilter) { }); if (tagsDiv.children.length > 0) item.appendChild(tagsDiv); } - // #196 : document reçu par partage → ouvre la page de partage (lecture seule). + // #196 : document reçu par partage → onglet applicatif (/api/file résout + // Partage/ vers la source, lecture seule). Fallback /s/ si résolution KO. if (r.share_token) { - item.addEventListener("click", () => window.open(`/s/${r.share_token}`, "_blank")); + item.addEventListener("click", () => TabManager.openPreview(r.vault, r.path)); } else { item.addEventListener("click", () => TabManager.openPreview(r.vault, r.path)); item.addEventListener("dblclick", (e) => { e.preventDefault(); TabManager.openPersistent(r.vault, r.path); }); diff --git a/frontend/js/sidebar.js b/frontend/js/sidebar.js index 8d40666..f239e28 100644 --- a/frontend/js/sidebar.js +++ b/frontend/js/sidebar.js @@ -370,12 +370,19 @@ async function incrementalLoadDirectory(vaultName, dirPath, container) { fItem.title = t("dashboard.shared_by", { user: s.created_by || "" }); fItem.addEventListener("click", () => { scrollTreeItemIntoView(fItem, false); - window.open(`/s/${s.token}`, "_blank"); + // #196 : ouverture en onglet applicatif — /api/file résout Partage/ + // vers le fichier source (lecture seule). + if (window.innerWidth <= 768) { + TabManager.openPersistent(vaultName, `Partage/${(s.path || "").split("/").pop()}`); + } else { + TabManager.openPreview(vaultName, `Partage/${(s.path || "").split("/").pop()}`); + } + closeMobileSidebar(); }); shareSub.appendChild(fItem); }); shareDir.addEventListener("click", () => { - TabManager.openNav(vaultName, "Partage"); + // #196 : dossier virtuel — pas de navigation (pas de répertoire physique). const expanded = !shareSub.classList.contains("collapsed"); shareSub.classList.toggle("collapsed", expanded); const chev = shareDir.querySelector("[data-lucide]"); @@ -733,12 +740,19 @@ async function loadDirectory(vaultName, dirPath, container) { fItem.title = t("dashboard.shared_by", { user: s.created_by || "" }); fItem.addEventListener("click", () => { scrollTreeItemIntoView(fItem, false); - window.open(`/s/${s.token}`, "_blank"); + // #196 : ouverture en onglet applicatif — /api/file résout Partage/ + // vers le fichier source (lecture seule). + if (window.innerWidth <= 768) { + TabManager.openPersistent(vaultName, `Partage/${(s.path || "").split("/").pop()}`); + } else { + TabManager.openPreview(vaultName, `Partage/${(s.path || "").split("/").pop()}`); + } + closeMobileSidebar(); }); shareSub.appendChild(fItem); }); shareDir.addEventListener("click", () => { - TabManager.openNav(vaultName, "Partage"); + // #196 : dossier virtuel — pas de navigation (pas de répertoire physique). const expanded = !shareSub.classList.contains("collapsed"); shareSub.classList.toggle("collapsed", expanded); const chev = shareDir.querySelector("[data-lucide]"); diff --git a/package.json b/package.json index c59ac04..dbc2aa7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.67.0", + "version": "2.67.1", "description": "**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.", "main": "patch.js", "directories": { diff --git a/tests/test_directed_shares.py b/tests/test_directed_shares.py index aacd786..11a0989 100644 --- a/tests/test_directed_shares.py +++ b/tests/test_directed_shares.py @@ -167,3 +167,33 @@ class TestDirectedShareGate: resp = user_client.get("/api/search", params={"q": "dirigé", "vault": "all"}) assert resp.status_code == 200 assert not [r for r in resp.json()["results"] if r.get("share_token")] + + def test_recipient_opens_shared_file_in_app(self, sessions): + # #196 : /api/file résout home-/Partage/ vers la source. + admin_client, user_client = sessions + path = _ensure_shared_file() + resp = admin_client.post("/api/share/TestVault", json={ + "path": path, "shared_with": ["normaluser"], + }) + assert resp.status_code == 200 + r = user_client.get("/api/file/home-normaluser", params={"path": "Partage/share_directed.md"}) + assert r.status_code == 200, r.text + body = r.json() + assert body["is_markdown"] is True + assert "dirigé" in body["html"] + + # raw endpoint too + r = user_client.get("/api/file/home-normaluser/raw", params={"path": "Partage/share_directed.md"}) + assert r.status_code == 200 + assert "dirigé" in r.json()["raw"] + + def test_shared_file_resolution_is_user_scoped(self, sessions): + # home-admin/Partage/x.md demandé par normaluser → pas de mapping + # (le home d'un autre user lui est interdit, 403 avant tout). + admin_client, user_client = sessions + path = _ensure_shared_file() + admin_client.post("/api/share/TestVault", json={ + "path": path, "shared_with": ["normaluser"], + }) + r = user_client.get("/api/file/home-admin", params={"path": "Partage/share_directed.md"}) + assert r.status_code == 403