L'admin (vaults: ["*"]) voyait le home de chaque utilisateur dans sa barre latérale : "*" ouvrait tous les vaults, home-* compris. - backend/auth/middleware.py : check_vault_access exige un octroi explicite pour tout vault home-* (nouveau is_home_vault()). - Filtres « * » en dur remplacés par check_vault_access : dashboard, conflits, liens retour, favoris, abonnements push. - /api/search : search_vaults(is_allowed=…) filtre les bruts avant pagination (total et page restent justes). - backend/user_home.py : _grant n'écarte plus les comptes « * » — l'admin reçoit son propre home-admin (auto-réparé au démarrage). - Tests : test_user_home.py +2, assertion API inversée dans test_auth_api.py (admin ne voit plus home-alice).
161 lines
6.0 KiB
Python
161 lines
6.0 KiB
Python
"""History endpoints — recent, bookmarks, saved searches (ROADMAP #85, tranche 8).
|
|
|
|
Handlers déplacés depuis :mod:`backend.main` sans changement de
|
|
comportement : mêmes chemins, mêmes modèles (``BookmarkToggleRequest``
|
|
déménagé dans :mod:`backend.schemas`), mêmes dépendances
|
|
d'authentification.
|
|
|
|
Adaptations strictement équivalentes :
|
|
- ``_resolve_safe_path`` / ``_backup_file`` → :mod:`backend.services.paths`
|
|
et :mod:`backend.services.backups` (pass-through).
|
|
- ``_load_config`` vient de :mod:`backend.routers.config`.
|
|
"""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
import frontmatter
|
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
|
|
|
from backend.auth.middleware import check_vault_access, require_auth
|
|
from backend.history import get_bookmarks, toggle_bookmark
|
|
from backend.indexer import find_file_in_index, get_vault_data, update_single_file
|
|
from backend.routers.config import _load_config
|
|
from backend.saved_searches import delete_saved, get_saved, save_search
|
|
from backend.schemas import (
|
|
BookmarksResponse,
|
|
BookmarkToggleRequest,
|
|
BookmarkToggleResponse,
|
|
RecentResponse,
|
|
SavedSearch,
|
|
StatusResponse,
|
|
)
|
|
from backend.services.backups import create_backup
|
|
from backend.services.paths import resolve_safe_path
|
|
from backend.services.recent import humanize_mtime, list_recent
|
|
|
|
logger = logging.getLogger("obsigate")
|
|
|
|
router = APIRouter(tags=["Bookmarks"])
|
|
|
|
|
|
@router.get("/api/recent", response_model=RecentResponse)
|
|
async def api_recent(limit: int | None = Query(None), vault: str | None = Query(None), mode: str | None = Query("opened"), current_user=Depends(require_auth)):
|
|
config = _load_config()
|
|
actual_limit = limit if limit is not None else config.get("recent_files_limit", 20)
|
|
|
|
username = current_user.get("username")
|
|
user_vaults = current_user.get("_token_vaults") or current_user.get("vaults", [])
|
|
|
|
return list_recent(
|
|
username,
|
|
user_vaults,
|
|
vault=vault,
|
|
limit=actual_limit,
|
|
mode=mode or "opened",
|
|
)
|
|
|
|
|
|
@router.get("/api/bookmarks", response_model=BookmarksResponse)
|
|
async def api_bookmarks(vault: str | None = Query(None), current_user=Depends(require_auth)):
|
|
username = current_user.get("username")
|
|
|
|
if not username:
|
|
return {"files": []}
|
|
|
|
history = get_bookmarks(username, vault_filter=vault)
|
|
files_resp = []
|
|
for item in history:
|
|
v_name = item["vault"]
|
|
# #194 : check_vault_access — "*" n'inclut pas les dossiers persos.
|
|
if not check_vault_access(v_name, current_user):
|
|
continue
|
|
|
|
# Find in index to get metadata
|
|
f_idx = find_file_in_index(item["path"], v_name)
|
|
if f_idx:
|
|
files_resp.append({
|
|
"path": f_idx["path"],
|
|
"title": f_idx.get("title") or item["path"].split("/")[-1],
|
|
"vault": v_name,
|
|
"mtime": item["bookmarked_at"],
|
|
"mtime_human": humanize_mtime(item["bookmarked_at"]),
|
|
"size_bytes": f_idx.get("size", 0),
|
|
"tags": [f"#{t}" for t in f_idx.get("tags", [])][:5],
|
|
"bookmarked": True
|
|
})
|
|
else:
|
|
files_resp.append({
|
|
"path": item["path"],
|
|
"title": item.get("title") or item["path"].split("/")[-1],
|
|
"vault": v_name,
|
|
"mtime": item["bookmarked_at"],
|
|
"mtime_human": humanize_mtime(item["bookmarked_at"]),
|
|
"tags": [],
|
|
"bookmarked": True
|
|
})
|
|
return {
|
|
"files": files_resp,
|
|
"total": len(files_resp)
|
|
}
|
|
|
|
|
|
@router.post("/api/bookmarks/toggle", response_model=BookmarkToggleResponse)
|
|
async def api_toggle_bookmark(req: BookmarkToggleRequest, current_user=Depends(require_auth)):
|
|
username = current_user.get("username")
|
|
if not username:
|
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
|
|
# Check vault access
|
|
if not check_vault_access(req.vault, current_user):
|
|
raise HTTPException(status_code=403, detail="Access denied to vault")
|
|
|
|
is_now_bookmarked = toggle_bookmark(username, req.vault, req.path, req.title or "")
|
|
|
|
# Update the file's YAML frontmatter: favoris: true/false
|
|
vault_data = get_vault_data(req.vault)
|
|
if vault_data:
|
|
file_path = resolve_safe_path(Path(vault_data["path"]), req.path)
|
|
if file_path.exists() and file_path.suffix == ".md":
|
|
try:
|
|
raw = file_path.read_text(encoding="utf-8", errors="replace")
|
|
post = frontmatter.loads(raw)
|
|
if is_now_bookmarked:
|
|
post.metadata["favoris"] = True
|
|
elif "favoris" in post.metadata:
|
|
del post.metadata["favoris"]
|
|
new_raw = frontmatter.dumps(post)
|
|
create_backup(file_path, req.vault, req.path)
|
|
file_path.write_text(new_raw, encoding="utf-8")
|
|
await update_single_file(req.vault, str(file_path))
|
|
except Exception as e:
|
|
logger.warning(f"Failed to update favoris metadata on {req.vault}/{req.path}: {e}")
|
|
|
|
return {"bookmarked": is_now_bookmarked}
|
|
|
|
|
|
@router.get("/api/saved-searches", response_model=list[SavedSearch])
|
|
async def api_saved_searches(current_user=Depends(require_auth)):
|
|
username = current_user.get("username")
|
|
if not username:
|
|
raise HTTPException(401)
|
|
return get_saved(username)
|
|
|
|
|
|
@router.post("/api/saved-searches", response_model=SavedSearch)
|
|
async def api_save_search(body: dict = Body(...), current_user=Depends(require_auth)):
|
|
username = current_user.get("username")
|
|
if not username:
|
|
raise HTTPException(401)
|
|
return save_search(username, body)
|
|
|
|
|
|
@router.delete("/api/saved-searches/{search_id}", response_model=StatusResponse)
|
|
async def api_delete_saved_search(search_id: str, current_user=Depends(require_auth)):
|
|
username = current_user.get("username")
|
|
if not username:
|
|
raise HTTPException(401)
|
|
if not delete_saved(username, search_id):
|
|
raise HTTPException(404, "Not found")
|
|
return {"status": "deleted"}
|