fix: audit round 3 — IDOR report-scoping, auth rate-limit, briefing chat scoping, DailyVolumes dedup, N+1 list routines, FE export/ask error handling

- IDOR: interrogate without report_id now scoped to user_key (LatestReportForUser); regression test TestInterrogateIDORScoped
- auth: signup/login per-IP rate limit 10/60s (new internal/api/ratelimit.go) + test
- chat unscoped grounding: build caller's own briefing instead of global LatestBriefing
- DailyVolumes: dedupe by bar date (snapshot rows hold 30-day windows) — fixes volume-anomaly skew
- GetDestination: direct (id,user_key) query instead of listing all
- ListRoutines: single LastRunsByRoutine query instead of N+1 RunHistory
- FE: exportMd/ask/HTML/PDF export now surface errors; alerts create clears channels
This commit is contained in:
asepharyana
2026-09-16 14:12:32 +07:00
parent 0b00daed39
commit 1ec860f9be
9 changed files with 270 additions and 38 deletions
+1
View File
@@ -33,6 +33,7 @@ function AlertsInner() {
setBusy(true);
try {
await api.createAlert({ name: tpl().label, rule: tpl().rule, channels: channels().split(",").map((c) => c.trim()).filter(Boolean) });
setChannels("");
refetch();
} catch (e) { setErr(String(e)); }
finally { setBusy(false); }
+16 -5
View File
@@ -28,13 +28,18 @@ function ReportInner() {
// Refetch when the ticker param changes (ReportInner stays mounted
// because Gate wraps it, but params.ticker is reactive).
createEffect(on(() => params.ticker, () => load()));
async function exportMd() { setMd(await api.reportMd(params.ticker)); }
async function exportMd() {
try { setMd(await api.reportMd(params.ticker)); }
catch (e) { setErr(String(e)); }
}
async function ask() {
if (!question().trim()) return;
setAsking(true);
try {
const r = await api.interrogate(params.ticker, question());
setAnswer(r.answer);
} catch (e) {
setErr(String(e));
} finally { setAsking(false); }
}
function dl(url: string, name: string) {
@@ -75,15 +80,21 @@ function ReportInner() {
<CardContent class="flex flex-wrap gap-2">
<Button variant="outline" onClick={exportMd}>Markdown</Button>
<Button variant="outline" onClick={async () => {
const r = await fetch(`/api/report/${encodeURIComponent(params.ticker)}?format=html`, { method: "POST", credentials: "same-origin" });
if (r.ok) dl(URL.createObjectURL(new Blob([await r.text()], { type: "text/html" })), `${params.ticker}-report.html`);
try {
const r = await fetch(`/api/report/${encodeURIComponent(params.ticker)}?format=html`, { method: "POST", credentials: "same-origin" });
if (!r.ok) { setErr(`HTML export gagal: HTTP ${r.status}`); return; }
dl(URL.createObjectURL(new Blob([await r.text()], { type: "text/html" })), `${params.ticker}-report.html`);
} catch (e) { setErr(String(e)); }
}}>HTML</Button>
<Button variant="outline" onClick={() => {
dl(URL.createObjectURL(new Blob([JSON.stringify(rep(), null, 1)], { type: "application/json" })), `${params.ticker}-report.json`);
}}>JSON</Button>
<Button variant="outline" onClick={async () => {
const r = await fetch(`/api/report/${encodeURIComponent(params.ticker)}?format=pdf`, { method: "POST", credentials: "same-origin" });
if (r.ok) dl(URL.createObjectURL(await r.blob()), `${params.ticker}-report.pdf`);
try {
const r = await fetch(`/api/report/${encodeURIComponent(params.ticker)}?format=pdf`, { method: "POST", credentials: "same-origin" });
if (!r.ok) { setErr(`PDF export gagal: HTTP ${r.status}`); return; }
dl(URL.createObjectURL(await r.blob()), `${params.ticker}-report.pdf`);
} catch (e) { setErr(String(e)); }
}}>PDF</Button>
</CardContent>
</Card>