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:
2026-07-03 12:45:43 -04:00
parent 3e04c4113f
commit cfd94f905a
47 changed files with 1847 additions and 427 deletions
+81 -2
View File
@@ -171,14 +171,31 @@ const Modal = {
btn.textContent = 'SAVING...';
status.textContent = '';
// MMSI: empty string → unassign (sends "" so backend nulls the column).
// Non-empty → must be digits-only; backend enforces uniqueness across aids.
const mmsiVal = v('ef-mmsi');
if (mmsiVal && !/^\d{6,9}$/.test(mmsiVal)) {
status.textContent = 'MMSI must be 69 digits, or blank to unassign.';
status.className = 'save-status err';
btn.disabled = false; btn.textContent = 'SAVE CHANGES';
return;
}
const payload = {
puerto_responsable: v('ef-puerto') || null,
empresa_responsable: v('ef-empresa') || null,
caracteristica_luz: v('ef-luz') || null,
alcance_nm: flt('ef-alcance'),
radio_borneo_m: flt('ef-borneo'),
observaciones: v('ef-obs') || null,
lamp_id: v('ef-lamp') || null,
displacement_warn_m: flt('ef-drift-warn'),
displacement_alarm_m: flt('ef-drift-alarm'),
signal_loss_min: flt('ef-signal-loss') ? parseInt(v('ef-signal-loss')) : null,
din3_function: v('ef-din3') || null,
din4_function: v('ef-din4') || null,
observaciones: v('ef-obs') || null,
lamp_id: v('ef-lamp') || null,
mmsi: mmsiVal, // "" unassigns; backend handles
tipo_ais: mmsiVal ? 'ATON_21' : 'SIN_AIS',
modificado_por: Auth.session.nombre,
motivo_cambio: motivo,
};
@@ -273,6 +290,24 @@ function buildEditForm(p) {
</div>
</div>` : '';
// AIS link — when set, AIS Type 21 with this MMSI updates the buoy's
// lat_actual (ghost marker) and triggers drift/battery alerts using this
// aid's per-buoy thresholds.
const aisBlock = `
<div class="modal-section-label">AIS LINK</div>
<div style="font-size:0.68rem;color:var(--text-muted);margin-bottom:6px">
MMSI of the AtoN transponder on this buoy.
Once set, the system listens for AIS Type 21 with this MMSI and shows
the AIS-reported position as a transparent ghost over the nominal marker.
Leave blank if the buoy has no AIS.
</div>
<div class="form-field">
<label class="form-label">MMSI</label>
<input class="form-input" id="ef-mmsi" type="text" inputmode="numeric"
pattern="\\d{6,9}" maxlength="9"
value="${p.mmsi || ''}" placeholder="e.g. 993001002">
</div>`;
return `
<div class="modal-section-label">GENERAL</div>
@@ -327,12 +362,56 @@ function buildEditForm(p) {
value="${p.radio_borneo_m ?? 10}">
</div>
<div class="modal-section-label">ALERT THRESHOLDS</div>
<div style="font-size:0.68rem;color:var(--text-muted);margin-bottom:6px">
Leave blank to use global settings. Override per buoy based on anchor chain length and operating area.
</div>
<div class="field-row-modal">
<div class="form-field">
<label class="form-label">Drift warn (m)</label>
<input class="form-input" id="ef-drift-warn" type="number" step="1" min="1"
value="${p.displacement_warn_m ?? ''}" placeholder="global">
</div>
<div class="form-field">
<label class="form-label">Drift alarm (m)</label>
<input class="form-input" id="ef-drift-alarm" type="number" step="1" min="1"
value="${p.displacement_alarm_m ?? ''}" placeholder="global">
</div>
<div class="form-field">
<label class="form-label">Signal loss (min)</label>
<input class="form-input" id="ef-signal-loss" type="number" step="1" min="1"
value="${p.signal_loss_min ?? ''}" placeholder="off">
</div>
</div>
<div class="field-row-modal">
<div class="form-field">
<label class="form-label">Digital IN3 function</label>
<select class="form-input-select" id="ef-din3">
<option value="">— Not connected —</option>
<option value="WATER_INGRESS_WARN" ${p.din3_function==='WATER_INGRESS_WARN' ?'selected':''}>Water ingress (warning)</option>
<option value="WATER_INGRESS_CRITICAL" ${p.din3_function==='WATER_INGRESS_CRITICAL' ?'selected':''}>Water ingress (critical)</option>
<option value="LISTING" ${p.din3_function==='LISTING' ?'selected':''}>Listing / tilt sensor</option>
</select>
</div>
<div class="form-field">
<label class="form-label">Digital IN4 function</label>
<select class="form-input-select" id="ef-din4">
<option value="">— Not connected —</option>
<option value="WATER_INGRESS_WARN" ${p.din4_function==='WATER_INGRESS_WARN' ?'selected':''}>Water ingress (warning)</option>
<option value="WATER_INGRESS_CRITICAL" ${p.din4_function==='WATER_INGRESS_CRITICAL' ?'selected':''}>Water ingress (critical)</option>
<option value="LISTING" ${p.din4_function==='LISTING' ?'selected':''}>Listing / tilt sensor</option>
</select>
</div>
</div>
<div class="form-field">
<label class="form-label">Observations</label>
<textarea class="form-textarea" id="ef-obs"
style="height:64px">${p.observaciones || ''}</textarea>
</div>
${aisBlock}
${nominalBlock}
<div class="modal-section-label">AUDIT</div>