Consolidate audit-fixes branch: webhooks, integrations, and deploy hardening
Batch commit of the pending working tree on security/audit-fixes-2026-07. Major areas: - Outbound webhooks / Zapier: schema + signed delivery with retries, public v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain. - Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when Spaces is unconfigured instead of silently using ephemeral disk. - Integrations & features (concurrent work): accounting (QuickBooks/Xero), e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding, expanded legal pages. - DB migrations 0006–0009. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
969d5d4c8a
commit
c9968531e4
@@ -0,0 +1,36 @@
|
||||
/* Leaflet map styling for the dark app shell. */
|
||||
|
||||
/* Our inline-SVG pin uses a divIcon; strip Leaflet's default white box. */
|
||||
.leaflet-div-icon.pmn-map-pin {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* OSM tiles are light — tone them to match the dark UI. Only the tile pane is
|
||||
filtered, so markers, popups and controls keep their real colors. */
|
||||
.pmn-map-dark .leaflet-tile-pane {
|
||||
filter: invert(1) hue-rotate(180deg) brightness(0.95) contrast(0.9);
|
||||
}
|
||||
|
||||
/* Popups on the dark surface. */
|
||||
.pmn-map-dark .leaflet-popup-content-wrapper {
|
||||
background: #16161f;
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
.pmn-map-dark .leaflet-popup-tip {
|
||||
background: #16161f;
|
||||
}
|
||||
.pmn-map-dark .leaflet-popup-content a {
|
||||
color: #818cf8;
|
||||
}
|
||||
|
||||
/* Attribution + controls readability on dark. */
|
||||
.pmn-map-dark .leaflet-control-attribution {
|
||||
background: rgba(9, 9, 11, 0.7);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.pmn-map-dark .leaflet-control-attribution a {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useRef } from "react"
|
||||
import type * as Leaflet from "leaflet"
|
||||
import "leaflet/dist/leaflet.css"
|
||||
import "./property-map.css"
|
||||
|
||||
export type MapMarker = {
|
||||
id: string
|
||||
name: string
|
||||
lat: number
|
||||
lng: number
|
||||
subtitle?: string
|
||||
href?: string
|
||||
}
|
||||
|
||||
// Indigo pin as an inline SVG divIcon — avoids Leaflet's default marker image
|
||||
// assets (which break under bundlers) and matches the app accent.
|
||||
const PIN_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="28" height="28" fill="#6366f1" stroke="#0b0b12" stroke-width="1.2"><path d="M12 0.5C6.9 0.5 2.8 4.6 2.8 9.7c0 6.6 8.2 13.9 8.6 14.2a0.9 0.9 0 0 0 1.2 0c0.4-0.3 8.6-7.6 8.6-14.2C21.2 4.6 17.1 0.5 12 0.5z"/><circle cx="12" cy="9.7" r="3.3" fill="#0b0b12"/></svg>`
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/[&<>"']/g, (c) =>
|
||||
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] as string
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaflet map (OpenStreetMap tiles) rendering property markers. Client-only:
|
||||
* Leaflet touches `window`, so it's dynamically imported inside an effect.
|
||||
*/
|
||||
export function PropertyMap({
|
||||
markers,
|
||||
className = "h-72 w-full",
|
||||
zoom = 15,
|
||||
}: {
|
||||
markers: MapMarker[]
|
||||
className?: string
|
||||
zoom?: number
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const mapRef = useRef<Leaflet.Map | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
void (async () => {
|
||||
const mod = await import("leaflet")
|
||||
const L = ((mod as unknown as { default?: typeof Leaflet }).default ?? mod) as typeof Leaflet
|
||||
if (cancelled || !containerRef.current || mapRef.current) return
|
||||
|
||||
const map = L.map(containerRef.current, {
|
||||
scrollWheelZoom: false,
|
||||
zoomControl: true,
|
||||
})
|
||||
mapRef.current = map
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
maxZoom: 19,
|
||||
}).addTo(map)
|
||||
|
||||
const icon = L.divIcon({
|
||||
html: PIN_SVG,
|
||||
className: "pmn-map-pin",
|
||||
iconSize: [28, 28],
|
||||
iconAnchor: [14, 28],
|
||||
popupAnchor: [0, -26],
|
||||
})
|
||||
|
||||
const latlngs: [number, number][] = []
|
||||
for (const m of markers) {
|
||||
if (!Number.isFinite(m.lat) || !Number.isFinite(m.lng)) continue
|
||||
const marker = L.marker([m.lat, m.lng], { icon }).addTo(map)
|
||||
const link = m.href
|
||||
? `<div style="margin-top:4px"><a href="${escapeHtml(m.href)}">View details →</a></div>`
|
||||
: ""
|
||||
const sub = m.subtitle ? `<div style="color:#9ca3af">${escapeHtml(m.subtitle)}</div>` : ""
|
||||
marker.bindPopup(`<div style="font-weight:600">${escapeHtml(m.name)}</div>${sub}${link}`)
|
||||
latlngs.push([m.lat, m.lng])
|
||||
}
|
||||
|
||||
if (latlngs.length === 1) {
|
||||
map.setView(latlngs[0], zoom)
|
||||
} else if (latlngs.length > 1) {
|
||||
map.fitBounds(latlngs, { padding: [40, 40] })
|
||||
} else {
|
||||
map.setView([39.8283, -98.5795], 4) // continental US fallback
|
||||
}
|
||||
|
||||
// Tiles can render blank if the container sized after init.
|
||||
setTimeout(() => map.invalidateSize(), 0)
|
||||
})()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
mapRef.current?.remove()
|
||||
mapRef.current = null
|
||||
}
|
||||
}, [markers, zoom])
|
||||
|
||||
return <div ref={containerRef} className={`pmn-map-dark ${className}`} />
|
||||
}
|
||||
Reference in New Issue
Block a user