Files
nas-tools/dashboard/backend/routers/files.py
T
Gan, Jimmy be1f3b3ad6 Add security improvements: refresh tokens, passkeys, CSP, audit logging, TOTP encryption
- Shorten access token to 30min with 7-day refresh token flow
- Add WebAuthn passkey registration and login with TOTP fallback
- Add Content-Security-Policy header
- Add audit logging middleware to /volume1/docker/nas-dashboard/audit.log
- Block /volume1/docker/ in files endpoint
- Encrypt TOTP secret at rest with Fernet (derived from SECRET_KEY)
- New deps: py_webauthn, cryptography
2026-02-22 11:04:40 +08:00

71 lines
2.0 KiB
Python

import os
import shutil
from pathlib import Path
from fastapi import APIRouter, UploadFile, File, HTTPException
from fastapi.responses import FileResponse
from config import VOLUME_ROOT
router = APIRouter()
BASE = Path(VOLUME_ROOT)
BLOCKED_DIRS = {"@appstore", "@docker", "@eaDir", "@S2S", "@sharesnap", "@tmp", "docker"}
def _safe_path(path: str) -> Path:
target = (BASE / path).resolve()
if not str(target).startswith(str(BASE.resolve())):
raise ValueError("forbidden")
# Block access to sensitive directories
rel = target.relative_to(BASE.resolve())
if rel.parts and rel.parts[0] in BLOCKED_DIRS:
raise ValueError("forbidden")
return target
@router.get("/browse")
def browse(path: str = ""):
try:
target = _safe_path(path)
except ValueError:
raise HTTPException(status_code=403, detail="Access denied")
entries = []
for item in sorted(target.iterdir()):
try:
st = item.stat()
entries.append({"name": item.name, "is_dir": item.is_dir(), "size": st.st_size if item.is_file() else 0})
except PermissionError:
continue
return {"path": str(target.relative_to(BASE)), "entries": entries}
@router.get("/download")
def download(path: str):
try:
return FileResponse(_safe_path(path))
except ValueError:
raise HTTPException(status_code=403, detail="Access denied")
@router.post("/upload")
async def upload(path: str, file: UploadFile = File(...)):
try:
target = _safe_path(path) / file.filename
except ValueError:
raise HTTPException(status_code=403, detail="Access denied")
with open(target, "wb") as f:
shutil.copyfileobj(file.file, f)
return {"ok": True}
@router.delete("/delete")
def delete(path: str):
try:
target = _safe_path(path)
except ValueError:
raise HTTPException(status_code=403, detail="Access denied")
if target.is_dir():
shutil.rmtree(target)
else:
target.unlink()
return {"ok": True}