Add audit logging, rate limiting, secret redactor, and backlinks

Implement several security and feature improvements across the backend
and frontend:
- New IP-based rate limiter for authentication endpoints
- New audit logging system for sensitive operations
- New secret redactor to mask sensitive patterns in rendered content
- Configurable token TTL and IGNORED_DIRS via environment variables
- Add backlink index and API endpoint
- Add preview tab support with single/double-click behavior in tree
- Add file backup before write/delete operations
This commit is contained in:
2026-05-26 10:27:00 -04:00
parent 5280dc7a50
commit 482937fb30
13 changed files with 1177 additions and 30 deletions
+87
View File
@@ -30,6 +30,9 @@ _index_generation: int = 0
# O(1) lookup table for wikilink resolution: {filename_lower: [{vault, path}, ...]}
_file_lookup: Dict[str, List[Dict[str, str]]] = {}
# Backlink index: {vault_name: {relative_path: [{vault, path, title}, ...]}}
_backlink_index: Dict[str, Dict[str, List[Dict[str, str]]]] = {}
# O(1) path index for tree filtering: {vault_name: [{path, name, type}, ...]}
path_index: Dict[str, List[Dict[str, str]]] = {}
@@ -49,6 +52,12 @@ SUPPORTED_EXTENSIONS = {
}
# Ignored directories (configurable via OBSIGATE_IGNORED_DIRS env var)
_DEFAULT_IGNORED = {'.obsidian', '.trash', '.git', '__pycache__', 'node_modules', '.obsigate-backup'}
_env_ignored = os.environ.get("OBSIGATE_IGNORED_DIRS", "")
IGNORED_DIRS = set(d.strip() for d in _env_ignored.split(",") if d.strip()) if _env_ignored else _DEFAULT_IGNORED.copy()
def load_vault_config() -> Dict[str, Dict[str, Any]]:
"""Read VAULT_N_* and DIR_N_* env vars and return vault configuration.
@@ -223,6 +232,10 @@ def _scan_vault(vault_name: str, vault_path: str, vault_cfg: Optional[Dict[str,
return {"files": [], "tags": {}, "path": vault_path, "paths": []}
for fpath in vault_root.rglob("*"):
# Skip ignored directories
if any(part in IGNORED_DIRS for part in fpath.relative_to(vault_root).parts):
continue
rel_path_str = str(fpath.relative_to(vault_root)).replace("\\", "/")
# Add all paths (files and directories) to path index
@@ -270,6 +283,12 @@ def _scan_vault(vault_name: str, vault_path: str, vault_cfg: Optional[Dict[str,
title = _extract_title(post, fpath)
content_preview = post.content[:200].strip()
# Extract wikilinks for backlink index
_extract_wikilinks_for_backlinks(
vault_name, str(relative).replace("\\", "/"),
title, post.content
)
files.append({
"path": str(relative).replace("\\", "/"),
"title": title,
@@ -849,3 +868,71 @@ def find_file_in_index(link_target: str, current_vault: str) -> Optional[Dict[st
if c["vault"] == current_vault:
return c
return candidates[0]
# ---------------------------------------------------------------------------
# Backlink index: tracks which files link to which targets
# ---------------------------------------------------------------------------
def _extract_wikilinks_for_backlinks(
vault_name: str, source_path: str, source_title: str, content: str
):
"""Extract wikilinks from markdown content and populate the backlink index.
For each `[[target]]` or `[[target|display]]` found in the content,
adds the source file to the backlink index of the target.
Args:
vault_name: The vault containing the source file.
source_path: Relative path of the source file in the vault.
source_title: Title of the source file.
content: The markdown content to scan for wikilinks.
"""
global _backlink_index
wikilink_pattern = re.compile(r'\[\[([^\]|#]+)(?:[|#][^\]]+)?\]\]')
targets = set()
for match in wikilink_pattern.finditer(content):
target = match.group(1).strip()
target_lower = target.lower()
if not target_lower.endswith(".md"):
target_lower += ".md"
targets.add(target_lower)
if vault_name not in _backlink_index:
_backlink_index[vault_name] = {}
for target in targets:
if target not in _backlink_index[vault_name]:
_backlink_index[vault_name][target] = []
# Avoid duplicates for same source-target pair
existing = [e for e in _backlink_index[vault_name][target] if e["path"] == source_path]
if not existing:
_backlink_index[vault_name][target].append({
"vault": vault_name,
"path": source_path,
"title": source_title,
})
def get_backlinks(vault_name: str, file_path: str) -> List[Dict[str, str]]:
"""Get all files that link to the given file via wikilinks.
Searches across all vaults for backlinks pointing to the target file.
Args:
vault_name: The vault containing the target file.
file_path: Relative path of the target file in the vault.
Returns:
List of ``{vault, path, title}`` dicts for files linking to the target.
"""
global _backlink_index
target_key = file_path.lower()
if not target_key.endswith(".md"):
target_key += ".md"
results = []
for vname, vindex in _backlink_index.items():
bl = vindex.get(target_key, [])
results.extend(bl)
return results