towerops/assets/js/hooks/sites_map.ts
Graham McIntire ca7bb75472 fix: 11 critical security/correctness bugs from code audit
- C1: Replace innerHTML template literals with DOM textContent in sites_map.ts
- C2: Derive user_id from current_scope instead of client params in policy consent
- C3: Fix broken halt() in GDPR data export (orphaned _ = ... halt())
- C4: Add owner/admin authorization check to MembersController update/delete
- C5: Require HMAC signature on agent release webhook (was optional)
- C6: Fix Repo.all_by/2 (not a real Ecto function) → Repo.all with query
- C7: Move auth exemption to before_send callback in BruteForceProtection
- C8: Block </style>/<script injection in status page custom_css validation
- C9: Create secrets.example.yaml with placeholders; secrets.yaml already gitignored
- C10: Load Stripe key from STRIPE_SECRET_KEY env var instead of hardcoded value
- C13: Remove credentials from PubSub backup broadcast; channel resolves them

C11 (no force_ssl): by design — Cloudflared terminates TLS
C12 (device quota race): false positive — FOR UPDATE serializes correctly
2026-05-12 10:20:52 -05:00

158 lines
4.9 KiB
TypeScript

// Lazy-loaded sites map hook (Leaflet). Leaflet itself is loaded on
// demand via ensureLeaflet (no bundling), so this chunk is small.
import { ensureLeaflet } from "../lib/leaflet"
declare const L: any
export const SitesMap = {
map: null as any,
markers: null as any,
mounted(this: any) {
ensureLeaflet().then(() => {
const sitesData = JSON.parse(this.el.dataset.sites || '[]')
this.initializeLeaflet(sitesData)
this.handleEvent("site_clicked", (payload: any) => {
this.pushEvent("site_clicked", payload)
})
}).catch((e: any) => {
console.error('SitesMap: Leaflet failed to load', e)
})
},
updated(this: any) {
if (this.map) {
const sitesData = JSON.parse(this.el.dataset.sites || '[]')
this.updateSites(sitesData)
}
},
destroyed(this: any) {
if (this.map) {
this.map.remove()
this.map = null
this.markers = null
}
},
initializeLeaflet(this: any, sites: any[]) {
this.map = L.map(this.el, {
zoomControl: true,
scrollWheelZoom: true
})
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 18
}).addTo(this.map)
this.updateSites(sites)
},
updateSites(this: any, sites: any[]) {
if (!this.map) return
if (this.markers) {
this.map.removeLayer(this.markers)
}
this.markers = L.layerGroup()
if (sites.length === 0) {
this.map.setView([39.8283, -98.5795], 4)
this.markers.addTo(this.map)
return
}
const bounds = L.latLngBounds([])
sites.forEach((site: any) => {
if (site.latitude && site.longitude) {
const marker = L.marker([site.latitude, site.longitude])
const popupEl = document.createElement('div')
popupEl.className = 'p-2'
const h3 = popupEl.appendChild(document.createElement('h3'))
h3.className = 'font-semibold text-lg mb-1'
h3.textContent = site.name
if (site.description) {
const descP = popupEl.appendChild(document.createElement('p'))
descP.className = 'text-sm text-gray-600 mb-1'
descP.textContent = site.description
}
if (site.address) {
const addrP = popupEl.appendChild(document.createElement('p'))
addrP.className = 'text-xs text-gray-500 mb-1'
addrP.textContent = site.address
}
const coordsP = popupEl.appendChild(document.createElement('p'))
coordsP.className = 'text-xs text-gray-500 mb-2'
coordsP.textContent = `${site.latitude.toFixed(4)}, ${site.longitude.toFixed(4)}`
if (site.device_count > 0) {
const deviceP = popupEl.appendChild(document.createElement('p'))
deviceP.className = 'text-xs text-blue-600 mb-2'
deviceP.textContent = `${site.device_count} device${site.device_count !== 1 ? 's' : ''}`
}
const button = popupEl.appendChild(document.createElement('button'))
button.setAttribute('data-site-id', site.id)
button.setAttribute('data-action', 'view-site')
button.className = 'text-xs bg-blue-600 text-white px-2 py-1 rounded hover:bg-blue-700'
button.textContent = 'View Site →'
marker.bindPopup(popupEl)
marker.on('popupopen', () => {
const button = marker.getPopup()?.getElement()?.querySelector('[data-action="view-site"]') as HTMLElement
if (button) {
button.addEventListener('click', () => {
this.pushEvent("site_clicked", { site_id: button.dataset.siteId })
})
}
})
this.markers.addLayer(marker)
bounds.extend([site.latitude, site.longitude])
}
})
this.markers.addTo(this.map)
if (bounds.isValid()) {
this.map.fitBounds(bounds, { padding: [20, 20] })
}
}
}
// Single-marker map for site detail page. Loads Leaflet via ensureLeaflet
// rather than relying on a global script polled with setTimeout.
export const LeafletMap = {
map: null as any,
mounted(this: any) {
const lat = parseFloat(this.el.dataset.lat)
const lng = parseFloat(this.el.dataset.lng)
const zoom = parseInt(this.el.dataset.zoom || '14')
const title = this.el.dataset.markerTitle || ''
if (isNaN(lat) || isNaN(lng)) return
ensureLeaflet().then(() => {
this.map = L.map(this.el, { scrollWheelZoom: false, zoomControl: true })
this.map.setView([lat, lng], zoom)
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(this.map)
L.marker([lat, lng]).addTo(this.map).bindPopup(title)
setTimeout(() => this.map.invalidateSize(), 200)
}).catch((e: any) => {
console.error('LeafletMap: Leaflet failed to load', e)
})
},
destroyed(this: any) {
if (this.map) { this.map.remove(); this.map = null }
}
}