security: CORS hardening, path traversal fix, WebSocket auth + cleanup
- Restrict CORS to localhost origins (was allow_origins=[*])
- Require valid JWT on WebSocket /ws (anonymous no longer gets admin view)
- Fix path traversal in delete_cell(): resolve() + parent check
- Validate cell_id format in /charts/download-noaa/{cell_id}
- Exclude charts/ and Cartas/ from git (keep US1GC09M world overview)
- Add NOAA ENC Portal external link in charts catalog tab
- Untrack __pycache__/, .db, .claude/ session files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+30
-10
@@ -181,9 +181,14 @@ def noaa_nearby(west: float, south: float, east: float, north: float):
|
||||
return matches
|
||||
|
||||
|
||||
import re as _re
|
||||
_CELL_RE = _re.compile(r'^US[1-5][A-Z]{2}\d{2}M$')
|
||||
|
||||
@router.post("/download-noaa/{cell_id}")
|
||||
async def download_noaa(cell_id: str):
|
||||
cell_id = cell_id.upper()
|
||||
if not _CELL_RE.match(cell_id):
|
||||
raise HTTPException(400, f"Invalid cell_id format: {cell_id}")
|
||||
url = NOAA_BASE.format(cell=cell_id)
|
||||
log.info("Downloading NOAA ENC %s from %s", cell_id, url)
|
||||
try:
|
||||
@@ -333,35 +338,50 @@ def set_cell_region(cell_id: str, region: str):
|
||||
return {"id": cell_id.upper(), "region": region}
|
||||
|
||||
|
||||
# Chart caches are rebuilt at server startup / explicit /rebuild-cache.
|
||||
# Between rebuilds, GeoJSON content is stable, so we let the browser cache
|
||||
# the response for an hour. ETag/304 would be safer but max-age is enough
|
||||
# for typical operator sessions and avoids redownloading 350 MB of depths.
|
||||
_CHART_CACHE_HEADERS = {"Cache-Control": "private, max-age=3600"}
|
||||
|
||||
|
||||
def _bbox(w, s, e, n):
|
||||
return (w, s, e, n) if None not in (w, s, e, n) else None
|
||||
|
||||
|
||||
@router.get("/features")
|
||||
def chart_features():
|
||||
return JSONResponse(get_all_features())
|
||||
def chart_features(w: float | None = None, s: float | None = None,
|
||||
e: float | None = None, n: float | None = None):
|
||||
return JSONResponse(get_all_features(_bbox(w, s, e, n)),
|
||||
headers=_CHART_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/depths")
|
||||
def chart_depths(w: float | None = None, s: float | None = None,
|
||||
e: float | None = None, n: float | None = None):
|
||||
bbox = (w, s, e, n) if None not in (w, s, e, n) else None
|
||||
return JSONResponse(get_all_depths(bbox))
|
||||
return JSONResponse(get_all_depths(_bbox(w, s, e, n)),
|
||||
headers=_CHART_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/land")
|
||||
def chart_land(w: float | None = None, s: float | None = None,
|
||||
e: float | None = None, n: float | None = None):
|
||||
bbox = (w, s, e, n) if None not in (w, s, e, n) else None
|
||||
return JSONResponse(get_all_land(bbox))
|
||||
return JSONResponse(get_all_land(_bbox(w, s, e, n)),
|
||||
headers=_CHART_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/hazards")
|
||||
def chart_hazards():
|
||||
return JSONResponse(get_all_hazards())
|
||||
def chart_hazards(w: float | None = None, s: float | None = None,
|
||||
e: float | None = None, n: float | None = None):
|
||||
return JSONResponse(get_all_hazards(_bbox(w, s, e, n)),
|
||||
headers=_CHART_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/zones")
|
||||
def chart_zones(w: float | None = None, s: float | None = None,
|
||||
e: float | None = None, n: float | None = None):
|
||||
bbox = (w, s, e, n) if None not in (w, s, e, n) else None
|
||||
return JSONResponse(get_all_zones(bbox))
|
||||
return JSONResponse(get_all_zones(_bbox(w, s, e, n)),
|
||||
headers=_CHART_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.post("/rebuild-cache")
|
||||
|
||||
Reference in New Issue
Block a user