Lewati ke konten

Integrasi Frontend React / Inertia

Karena robyajo/laravel-security-monitor dirancang secara Headless (Pure REST API), Anda dapat membangun antarmuka dashboard keamanan dan penanganan error pemblokiran menggunakan React dan Inertia.js sesuai kebutuhan desain aplikasi Anda.


1. Menangani Respons HTTP 403 Terblokir (Axios Interceptor)

Bagian berjudul “1. Menangani Respons HTTP 403 Terblokir (Axios Interceptor)”

Ketika pengguna terblokir oleh WAF, setiap panggilan API atau request Inertia akan mengembalikan kode status HTTP 403 Forbidden dengan struktur JSON:

{
"success": false,
"message": "Akses ditolak. Alamat IP atau perangkat Anda diblokir...",
"reason": "Percobaan Server-Side Template Injection (SSTI)",
"reference_id": "SEC-A1B2C3D4",
"blocked": true
}

Anda dapat menangani respons ini secara global menggunakan Axios interceptor:

resources/js/lib/axios.js
import axios from "axios";
const api = axios.create({
baseURL: "/api",
});
api.interceptors.response.use(
(response) => response,
(error) => {
if (
error.response &&
error.response.status === 403 &&
error.response.data?.blocked
) {
// Simpan detail blokir ke state manager / trigger modal permohonan banding
window.dispatchEvent(
new CustomEvent("security-blocked", {
detail: error.response.data,
}),
);
}
return Promise.reject(error);
},
);
export default api;

2. Komponen Modal Pengajuan Tiket Banding (Appeal Modal)

Bagian berjudul “2. Komponen Modal Pengajuan Tiket Banding (Appeal Modal)”
resources/js/Components/AppealTicketModal.tsx
import React, { useState } from "react";
import axios from "axios";
interface BlockedData {
reference_id: string;
reason: string;
ip_address: string;
}
export const AppealTicketModal: React.FC<{
data: BlockedData;
onClose: () => void;
}> = ({ data, onClose }) => {
const [form, setForm] = useState({
name: "",
email: "",
phone: "",
reason: "",
});
const [ticketNumber, setTicketNumber] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setErrorMsg(null);
try {
const res = await axios.post(
"/api/security/unblock-tickets/submit",
{
...form,
reason: `[Ref: ${data.reference_id}] ${form.reason}`,
},
);
setTicketNumber(res.data.ticket_number);
} catch (err: any) {
setErrorMsg(
err.response?.data?.message || "Gagal mengirimkan permohonan.",
);
} finally {
setLoading(false);
}
};
if (ticketNumber) {
return (
<div className="p-6 bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-md mx-auto text-center">
<h3 className="text-xl font-bold text-green-600 mb-2">
Permohonan Berhasil Dikirim!
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Nomor tiket permohonan banding Anda adalah:
</p>
<div className="p-3 bg-gray-100 dark:bg-gray-800 rounded font-mono font-bold text-lg select-all">
{ticketNumber}
</div>
<p className="text-xs text-gray-500 mt-4">
Simpan nomor ini untuk memeriksa status pembukaan blokir
secara berkala.
</p>
<button
onClick={onClose}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
>
Tutup
</button>
</div>
);
}
return (
<form
onSubmit={handleSubmit}
className="p-6 bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-lg mx-auto space-y-4"
>
<h2 className="text-lg font-bold text-red-600">
Akses Dibatasi — Ajukan Permohonan Buka Blokir
</h2>
<p className="text-xs text-gray-500">
ID Referensi: <strong>{data.reference_id}</strong> (IP:{" "}
{data.ip_address})
</p>
{errorMsg && (
<div className="p-2 bg-red-100 text-red-700 text-xs rounded">
{errorMsg}
</div>
)}
<div>
<label className="block text-xs font-medium">
Nama Lengkap
</label>
<input
required
type="text"
className="w-full border p-2 rounded text-sm"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
</div>
<div>
<label className="block text-xs font-medium">
Email Dinas / Aktif
</label>
<input
required
type="email"
className="w-full border p-2 rounded text-sm"
value={form.email}
onChange={(e) =>
setForm({ ...form, email: e.target.value })
}
/>
</div>
<div>
<label className="block text-xs font-medium">
Alasan / Klarifikasi Aktivitas
</label>
<textarea
required
rows={3}
className="w-full border p-2 rounded text-sm"
value={form.reason}
onChange={(e) =>
setForm({ ...form, reason: e.target.value })
}
placeholder="Jelaskan aktivitas apa yang sedang Anda lakukan..."
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-2 bg-blue-600 text-white rounded font-medium hover:bg-blue-700 transition"
>
{loading ? "Mengirim..." : "Kirim Permohonan Banding"}
</button>
</form>
);
};

3. Dashboard Monitoring React Starter Kit Siap Pakai

Bagian berjudul “3. Dashboard Monitoring React Starter Kit Siap Pakai”

Jika aplikasi host Anda dibangun di atas Laravel React Starter Kit (Inertia + React), Anda tidak perlu membangun panel monitoring dari nol. Paket menyediakan enam halaman Inertia/React siap pakai berbasis Pure Vanilla CSS yang dapat dipublikasikan dengan satu perintah:

Terminal window
php artisan vendor:publish --tag=starterkit-react

Perintah tersebut menyalin berkas berikut ke aplikasi Anda:

Berkas yang Dipublikasikan Deskripsi
resources/js/pages/security/overview.tsx Statistik, tren serangan, dan top attacker.
resources/js/pages/security/logs.tsx Tabel log keamanan dengan filter dan aksi bersihkan/hapus.
resources/js/pages/security/blocked-ips.tsx Manajemen karantina IP (blokir manual, toggle, unblock).
resources/js/pages/security/server.tsx Audit integritas, pemindai webshell, baseline, dan lockout.
resources/js/pages/security/sessions.tsx Sesi pengguna aktif, riwayat login, dan IP terpercaya.
resources/js/pages/security/tickets.tsx Review tiket banding (approve/reject + auto-unblock).
resources/js/components/security/security-nav.tsx Navigasi antar modul keamanan.
resources/js/components/security/pagination.tsx Komponen paginasi tabel.
resources/js/components/security/ui.tsx Komponen UI dasar (Card, Badge, Button, Input, Modal, dll).
resources/js/components/security/security.css Desain sistem Pure Vanilla CSS responsif + dark mode.

Aktifkan melalui .env, lalu bangun ulang aset:

SECURITY_DASHBOARD_ENABLED=true
SECURITY_DASHBOARD_DRIVER=react
SECURITY_DASHBOARD_PREFIX=security
Terminal window
npm run build

Panel dapat diakses pada /security.

ℹ️ Halaman di-render server-side oleh controller Inertia bawaan paket (Internal\SecurityMonitor\Http\Controllers\Dashboard\*). Mutasi (blokir IP, hapus log, approve tiket) dikirim sebagai permintaan Inertia biasa, sehingga tidak memerlukan token API terpisah dan tidak bergantung pada REST API /api/security/*.

🔒 Wajib Login: Rute dashboard memakai middleware web + auth, dan secara bawaan juga security.admin (Gate manage-security-monitor). Untuk mengizinkan semua pengguna yang sudah login (tanpa syarat admin), kosongkan security.dashboard.admin_middleware pada config/security.php.

Desain Sistem Pure Vanilla CSS (Zero External UI Dependencies)

Bagian berjudul “Desain Sistem Pure Vanilla CSS (Zero External UI Dependencies)”

Stubs React dibuat mandiri sepenuhnya (Zero external UI dependencies):

  • Tidak butuh Shadcn UI (@/components/ui/*), @/lib/utils, ataupun library eksternal sonner.
  • Menggunakan security.css dengan CSS custom properties (--sec-*) yang rapi, responsif, dan otomatis mendukung dark mode (.dark / @media (prefers-color-scheme: dark)).
  • Anda bebas memodifikasi warna, ukuran, atau integrasi komponen di resources/js/components/security/ kapan saja.

Untuk memunculkan tautan ke panel, tambahkan item berikut ke mainNavItems pada resources/js/components/app-sidebar.tsx:

import { ShieldCheck } from 'lucide-react';
{
title: 'Security Monitor',
href: '/security',
icon: ShieldCheck,
},
  • Jalankan npm run build (atau npm run dev) setiap kali halaman dashboard baru dipublikasikan agar tercatat pada manifest Vite.