fix: audit round 2 — session cookie Secure kondisional, logout clear cookie, IDOR report owner-scoping (migrasi 0008), health force login-gate, SSE stream login-gate, FE: Gate undefined-flicker, auth-expired global redirect, X-User-Key demo dihapus, error handling alerts/routines/watchlist, chart label, encodeURIComponent, Screener onMount
This commit is contained in:
@@ -28,14 +28,16 @@ function AlertsInner() {
|
||||
const [dchat, setDchat] = createSignal("");
|
||||
const [dhook, setDhook] = createSignal("");
|
||||
const [derr, setDerr] = createSignal("");
|
||||
const [err, setErr] = createSignal("");
|
||||
async function create() {
|
||||
setBusy(true);
|
||||
try {
|
||||
await api.createAlert({ name: tpl().label, rule: tpl().rule, channels: channels().split(",").map((c) => c.trim()).filter(Boolean) });
|
||||
refetch();
|
||||
} finally { setBusy(false); }
|
||||
} catch (e) { setErr(String(e)); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
async function del(id: number) { await api.deleteAlert(id); refetch(); }
|
||||
async function del(id: number) { try { await api.deleteAlert(id); refetch(); } catch (e) { setErr(String(e)); } }
|
||||
async function addDest() {
|
||||
setDerr("");
|
||||
try {
|
||||
@@ -47,11 +49,12 @@ function AlertsInner() {
|
||||
refetchDests();
|
||||
} catch (e) { setDerr(String(e)); }
|
||||
}
|
||||
async function toggleDest(id: number, enabled: boolean) { await api.updateDestination(id, { enabled: !enabled }); refetchDests(); }
|
||||
async function delDest(id: number) { await api.deleteDestination(id); refetchDests(); }
|
||||
async function toggleDest(id: number, enabled: boolean) { try { await api.updateDestination(id, { enabled: !enabled }); refetchDests(); } catch (e) { setDerr(String(e)); } }
|
||||
async function delDest(id: number) { try { await api.deleteDestination(id); refetchDests(); } catch (e) { setDerr(String(e)); } }
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
<PageHead title="Notifikasi otomatis 🔔" sub="Pilih kejadian yang mau kamu dikabari — tanpa perlu paham angka." />
|
||||
<Show when={err()}><p class="text-sm text-destructive">{err()}</p></Show>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>1. Mau dikabari soal apa?</CardTitle><CardDescription>Pilih satu template <Term kata="alert" />.</CardDescription></CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createResource, createSignal, For, Show } from "solid-js";
|
||||
import { createResource, createSignal, createMemo, For, Show } from "solid-js";
|
||||
import { A, useNavigate } from "@solidjs/router";
|
||||
import { api } from "../lib/api";
|
||||
import { useAuth } from "../components/auth";
|
||||
import { verdictFor, heroSummary, konteksPasar, fmtRp, fmtHarga } from "../lib/awam";
|
||||
import { Term, VerdictBadge, AlasanBar, IstilahStrip } from "../components/Awam";
|
||||
import { Badge } from "../components/ui/badge";
|
||||
@@ -13,10 +14,12 @@ Chart.register(...registerables);
|
||||
|
||||
export default function Dashboard() {
|
||||
const navigate = useNavigate();
|
||||
const { me } = useAuth();
|
||||
// screen+alertEvents are login-gated; source = me() so login/logout refetches.
|
||||
const [flow] = createResource(() => api.flowSummary());
|
||||
const [screen] = createResource(() => api.screen({ limit: 5 }).catch(() => null));
|
||||
const [screen] = createResource(me, (user) => (user ? api.screen({ limit: 5 }).catch(() => null) : null));
|
||||
const [briefing] = createResource(() => api.briefing().catch(() => null));
|
||||
const [events] = createResource(() => api.alertEvents("2000-01-01").then((r) => r.events.slice(0, 5)).catch(() => []));
|
||||
const [events] = createResource(me, (user) => (user ? api.alertEvents("2000-01-01").then((r) => r.events.slice(0, 5)).catch(() => []) : []));
|
||||
const [chartTiker, setChartTiker] = createSignal("BBCA");
|
||||
const [foreign] = createResource(chartTiker, (t) => api.flowForeign(t).catch(() => null));
|
||||
|
||||
@@ -31,7 +34,7 @@ export default function Dashboard() {
|
||||
const chartData = () => ({
|
||||
labels: (foreign()?.dates || []) as string[],
|
||||
datasets: [{
|
||||
label: `${chartTiker()} — uang asing harian (${fmtRp(0).slice(0, 2)}7700`,
|
||||
label: `${chartTiker()} — uang asing harian (Rp juta)`,
|
||||
data: (foreign()?.nets || []) as number[],
|
||||
borderColor: "#3b82f6",
|
||||
backgroundColor: "rgba(59,130,246,.15)",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSignal, For, Show, onMount } from "solid-js";
|
||||
import { createSignal, createEffect, For, Show, on } from "solid-js";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { api, type ReportPayload } from "../lib/api";
|
||||
import { Citations } from "../components/Citations";
|
||||
@@ -19,14 +19,15 @@ function ReportInner() {
|
||||
const [busy, setBusy] = createSignal(true);
|
||||
const [asking, setAsking] = createSignal(false);
|
||||
const [err, setErr] = createSignal("");
|
||||
const authHeaders = () => ({ "X-User-Key": localStorage.getItem("fs-key") || "demo" });
|
||||
async function load() {
|
||||
setErr(""); setBusy(true);
|
||||
try { setRep(await api.report(params.ticker)); }
|
||||
catch (e) { setErr(String(e)); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
onMount(load);
|
||||
// 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 ask() {
|
||||
if (!question().trim()) return;
|
||||
@@ -39,6 +40,8 @@ function ReportInner() {
|
||||
function dl(url: string, name: string) {
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = name; a.click();
|
||||
// Revoke after a short delay to avoid blob URL leak.
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
||||
}
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
@@ -72,15 +75,15 @@ 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/${params.ticker}?format=html`, { method: "POST", headers: authHeaders() });
|
||||
dl(URL.createObjectURL(new Blob([await r.text()], { type: "text/html" })), `${params.ticker}-report.html`);
|
||||
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`);
|
||||
}}>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/${params.ticker}?format=pdf`, { method: "POST", headers: authHeaders() });
|
||||
dl(URL.createObjectURL(await r.blob()), `${params.ticker}-report.pdf`);
|
||||
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`);
|
||||
}}>PDF</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -107,5 +110,5 @@ function ReportInner() {
|
||||
import { Gate } from "../index";
|
||||
|
||||
export default function Report() {
|
||||
return <Gate fitur="Report saham">{<ReportInner />}</Gate>;
|
||||
return <Gate fitur="Report saham"><ReportInner /></Gate>;
|
||||
}
|
||||
|
||||
@@ -22,20 +22,24 @@ function RoutinesInner() {
|
||||
const [runs, { refetch: refetchRuns }] = createResource(() => api.runs().then((r) => r.runs.slice(0, 20)));
|
||||
const [type_, setType] = createSignal("morning-briefing");
|
||||
const [busy, setBusy] = createSignal(false);
|
||||
const [err, setErr] = createSignal("");
|
||||
const [briefing] = createResource(() => api.briefing().catch(() => null));
|
||||
async function subscribe() {
|
||||
setBusy(true);
|
||||
try { await api.createRoutine({ type: type_() }); refetch(); }
|
||||
catch (e) { setErr(String(e)); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
async function toggle(id: number, enabled: boolean) { await api.updateRoutine(id, { enabled: !enabled }); refetch(); }
|
||||
async function toggle(id: number, enabled: boolean) {
|
||||
try { await api.updateRoutine(id, { enabled: !enabled }); refetch(); } catch (e) { setErr(String(e)); }
|
||||
}
|
||||
async function del(id: number) {
|
||||
await fetch(`/api/routines/${id}`, { method: "DELETE", headers: { "X-User-Key": localStorage.getItem("fs-key") || "demo" } });
|
||||
refetch();
|
||||
try { await api.deleteRoutine(id); refetch(); } catch (e) { setErr(String(e)); }
|
||||
}
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
<PageHead title="Jadwal otomatis 🗓️" sub="Pilih sekali — sistem yang kerja tiap hari. Ini namanya routine." />
|
||||
<Show when={err()}><p class="text-sm text-destructive">{err()}</p></Show>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Mau dilayani apa?</CardTitle><CardDescription>Jadwal standar sudah diatur (mis. ringkasan jam 07:30) — tidak perlu isi cron.</CardDescription></CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
import { createSignal, For, Show, onMount } from "solid-js";
|
||||
import { api, type ScreenRow } from "../lib/api";
|
||||
import { verdictFor } from "../lib/awam";
|
||||
import { Term, VerdictBadge, AlasanBar, IstilahStrip } from "../components/Awam";
|
||||
@@ -33,7 +33,7 @@ function ScreenerInner() {
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
// Default = semua: auto-jalan preset "Semua" sekali saat halaman dibuka.
|
||||
if (!ran() && !busy()) void runPreset(PRESET_SEMUA);
|
||||
onMount(() => { if (!ran()) void runPreset(PRESET_SEMUA); });
|
||||
const hasil = () => rows().map((r) => ({ row: r, ...verdictFor(r) }));
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
@@ -84,5 +84,5 @@ function ScreenerInner() {
|
||||
import { Gate } from "../index";
|
||||
|
||||
export default function Screener() {
|
||||
return <Gate fitur="Cari saham">{ScreenerInner()}</Gate>;
|
||||
return <Gate fitur="Cari saham"><ScreenerInner /></Gate>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user