9683cd3165
Deploy Dashboard / deploy (push) Successful in 1m26s
- 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
71 lines
2.0 KiB
Python
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}
|