initial commit

This commit is contained in:
cygouw
2026-06-10 22:01:03 +07:00
commit a9b40ecf29
26 changed files with 14175 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
<?php
require_once __DIR__ . '/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
jsonResponse(['error' => 'Method tidak didukung'], 405);
}
$admin = requireRole('admin');
$db = getDB();
// Pagination
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = min(100, max(10, (int)($_GET['limit'] ?? 50)));
$offset = ($page - 1) * $limit;
// Filter
$filterAksi = $_GET['aksi'] ?? null;
$filterUser = $_GET['user_id'] ?? null;
$where = [];
$params = [];
if ($filterAksi) {
$where[] = "al.aksi LIKE ?";
$params[] = "%$filterAksi%";
}
if ($filterUser) {
$where[] = "al.user_id = ?";
$params[] = (int)$filterUser;
}
$whereClause = count($where) > 0 ? 'WHERE ' . implode(' AND ', $where) : '';
// Total count
$countSql = "SELECT COUNT(*) FROM audit_logs al $whereClause";
$countStmt = $db->prepare($countSql);
$countStmt->execute($params);
$total = (int)$countStmt->fetchColumn();
// Data
$sql = "
SELECT al.*, u.username, u.nama_lengkap
FROM audit_logs al
LEFT JOIN users u ON u.id = al.user_id
$whereClause
ORDER BY al.created_at DESC
LIMIT $limit OFFSET $offset
";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
jsonResponse([
'data' => $rows,
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit),
]);
+183
View File
@@ -0,0 +1,183 @@
<?php
require_once __DIR__ . '/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$db = getDB();
$action = $_GET['action'] ?? '';
// ══════════════════════════════════════════════
// LOGIN
// ══════════════════════════════════════════════
if ($action === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$username = trim($body['username'] ?? '');
$password = $body['password'] ?? '';
if (!$username || !$password) {
jsonResponse(['success' => false, 'error' => 'Username dan password wajib diisi'], 422);
}
$stmt = $db->prepare("SELECT * FROM users WHERE username = ? AND is_active = 1");
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password'])) {
jsonResponse(['success' => false, 'error' => 'Username atau password salah'], 401);
}
// Set session
$_SESSION['user'] = [
'id' => (int)$user['id'],
'username' => $user['username'],
'nama_lengkap' => $user['nama_lengkap'],
'role' => $user['role'],
];
auditLog($db, (int)$user['id'], 'LOGIN', 'users', (int)$user['id']);
jsonResponse([
'success' => true,
'user' => $_SESSION['user']
]);
}
// ══════════════════════════════════════════════
// LOGOUT
// ══════════════════════════════════════════════
if ($action === 'logout' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$user = getCurrentUser();
if ($user) {
auditLog($db, $user['id'], 'LOGOUT', 'users', $user['id']);
}
session_destroy();
jsonResponse(['success' => true]);
}
// ══════════════════════════════════════════════
// ME — cek session aktif
// ══════════════════════════════════════════════
if ($action === 'me' && $_SERVER['REQUEST_METHOD'] === 'GET') {
$user = getCurrentUser();
if (!$user) {
jsonResponse(['success' => false, 'logged_in' => false], 200);
}
jsonResponse(['success' => true, 'logged_in' => true, 'user' => $user]);
}
// ══════════════════════════════════════════════
// REGISTER — admin only
// ══════════════════════════════════════════════
if ($action === 'register' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$admin = requireRole('admin');
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$username = trim($body['username'] ?? '');
$password = $body['password'] ?? '';
$nama_lengkap = trim($body['nama_lengkap'] ?? '');
$role = $body['role'] ?? 'surveyor';
if (!$username || !$password || !$nama_lengkap) {
jsonResponse(['success' => false, 'error' => 'Semua field wajib diisi'], 422);
}
if (strlen($password) < 6) {
jsonResponse(['success' => false, 'error' => 'Password minimal 6 karakter'], 422);
}
if (!in_array($role, ['admin', 'surveyor', 'pemangku'])) {
jsonResponse(['success' => false, 'error' => 'Role tidak valid'], 422);
}
// Cek username unik
$check = $db->prepare("SELECT id FROM users WHERE username = ?");
$check->execute([$username]);
if ($check->fetch()) {
jsonResponse(['success' => false, 'error' => 'Username sudah digunakan'], 409);
}
$hashed = password_hash($password, PASSWORD_BCRYPT);
$stmt = $db->prepare("INSERT INTO users (username, password, nama_lengkap, role) VALUES (?, ?, ?, ?)");
$stmt->execute([$username, $hashed, $nama_lengkap, $role]);
$newId = (int)$db->lastInsertId();
auditLog($db, $admin['id'], 'CREATE_USER', 'users', $newId, null, [
'username' => $username, 'nama_lengkap' => $nama_lengkap, 'role' => $role
]);
jsonResponse(['success' => true, 'id' => $newId]);
}
// ══════════════════════════════════════════════
// LIST USERS — admin only
// ══════════════════════════════════════════════
if ($action === 'users' && $_SERVER['REQUEST_METHOD'] === 'GET') {
requireRole('admin');
$rows = $db->query("SELECT id, username, nama_lengkap, role, is_active, created_at FROM users ORDER BY created_at DESC")->fetchAll();
jsonResponse(['data' => $rows]);
}
// ══════════════════════════════════════════════
// UPDATE USER — admin only
// ══════════════════════════════════════════════
if ($action === 'update' && $_SERVER['REQUEST_METHOD'] === 'PUT') {
$admin = requireRole('admin');
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
// Ambil data lama
$old = $db->prepare("SELECT id, username, nama_lengkap, role, is_active FROM users WHERE id = ?");
$old->execute([$id]);
$oldData = $old->fetch();
if (!$oldData) jsonResponse(['success' => false, 'error' => 'User tidak ditemukan'], 404);
$fields = []; $vals = [];
if (!empty($body['nama_lengkap'])) {
$fields[] = "nama_lengkap = ?"; $vals[] = trim($body['nama_lengkap']);
}
if (!empty($body['role']) && in_array($body['role'], ['admin','surveyor','pemangku'])) {
$fields[] = "role = ?"; $vals[] = $body['role'];
}
if (isset($body['is_active'])) {
$fields[] = "is_active = ?"; $vals[] = $body['is_active'] ? 1 : 0;
}
if (!empty($body['password']) && strlen($body['password']) >= 6) {
$fields[] = "password = ?"; $vals[] = password_hash($body['password'], PASSWORD_BCRYPT);
}
if (empty($fields)) jsonResponse(['success' => false, 'error' => 'Tidak ada data diubah'], 422);
$vals[] = $id;
$db->prepare("UPDATE users SET " . implode(',', $fields) . " WHERE id = ?")->execute($vals);
auditLog($db, $admin['id'], 'UPDATE_USER', 'users', $id, $oldData, $body);
jsonResponse(['success' => true]);
}
// ══════════════════════════════════════════════
// DELETE (deactivate) USER — admin only
// ══════════════════════════════════════════════
if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'DELETE') {
$admin = requireRole('admin');
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
if ($id === $admin['id']) {
jsonResponse(['success' => false, 'error' => 'Tidak bisa menghapus akun sendiri'], 400);
}
$db->prepare("UPDATE users SET is_active = 0 WHERE id = ?")->execute([$id]);
auditLog($db, $admin['id'], 'DEACTIVATE_USER', 'users', $id);
jsonResponse(['success' => true]);
}
jsonResponse(['error' => 'Action tidak valid'], 400);
+106
View File
@@ -0,0 +1,106 @@
<?php
// ── Konfigurasi Database ──────────────────────
// Sesuaikan dengan pengaturan XAMPP Anda
define('DB_HOST', 'localhost');
define('DB_USER', 'root'); // default XAMPP
define('DB_PASS', ''); // default XAMPP (kosong)
define('DB_NAME', 'webgis_poverty_mapping');
define('DB_CHARSET', 'utf8mb4');
// ── Upload Config ─────────────────────────────
define('UPLOAD_DIR', __DIR__ . '/../uploads/');
define('MAX_UPLOAD_SIZE', 5 * 1024 * 1024); // 5MB
// ── Session ───────────────────────────────────
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
function getDB(): PDO {
static $pdo = null;
if ($pdo === null) {
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=%s', DB_HOST, DB_NAME, DB_CHARSET);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['success' => false, 'error' => 'Koneksi DB gagal: ' . $e->getMessage()]));
}
}
return $pdo;
}
// ── JSON response helper ──────────────────────
function jsonResponse(array $data, int $code = 200): void {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if ($origin) {
header('Access-Control-Allow-Origin: ' . $origin);
} else {
header('Access-Control-Allow-Origin: *');
}
header('Access-Control-Allow-Credentials: true');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// ── Auth Helpers ──────────────────────────────
/**
* Cek apakah user sudah login.
* Return data user atau null.
*/
function getCurrentUser(): ?array {
return $_SESSION['user'] ?? null;
}
/**
* Paksa login. Jika belum login, return 401.
*/
function requireAuth(): array {
$user = getCurrentUser();
if (!$user) {
jsonResponse(['success' => false, 'error' => 'Unauthorized — silakan login'], 401);
}
return $user;
}
/**
* Paksa role tertentu. Jika role tidak sesuai, return 403.
* @param array|string $roles Role yang diizinkan
*/
function requireRole($roles): array {
$user = requireAuth();
if (is_string($roles)) $roles = [$roles];
if (!in_array($user['role'], $roles)) {
jsonResponse(['success' => false, 'error' => 'Forbidden — Anda tidak memiliki akses'], 403);
}
return $user;
}
// ── Audit Log Helper ──────────────────────────
/**
* Catat aktivitas ke audit_logs
*/
function auditLog(PDO $db, int $userId, string $aksi, ?string $targetType = null, ?int $targetId = null, $before = null, $after = null): void {
$stmt = $db->prepare("
INSERT INTO audit_logs (user_id, aksi, target_type, target_id, data_before, data_after, ip_address)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$userId,
$aksi,
$targetType,
$targetId,
$before ? json_encode($before, JSON_UNESCAPED_UNICODE) : null,
$after ? json_encode($after, JSON_UNESCAPED_UNICODE) : null,
$_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'
]);
}
+37
View File
@@ -0,0 +1,37 @@
<?php
require_once __DIR__ . '/config.php';
// ── Haversine distance (meter) ──────────────
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
$R = 6371000;
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2)**2;
return $R * 2 * atan2(sqrt($a), sqrt(1-$a));
}
// ── Cari rumah ibadah terdekat yang menjangkau koordinat ──
// Hanya rumah ibadah yang sudah verified
function findNearestIbadah(PDO $db, float $lat, float $lng): ?int {
$ibadahs = $db->query("SELECT id, lat, lng, radius FROM rumah_ibadah WHERE status = 'verified'")->fetchAll();
$nearest = null;
$minDist = PHP_FLOAT_MAX;
foreach ($ibadahs as $ib) {
$d = haversine($lat, $lng, (float)$ib['lat'], (float)$ib['lng']);
if ($d <= $ib['radius'] && $d < $minDist) {
$nearest = (int)$ib['id'];
$minDist = $d;
}
}
return $nearest;
}
// ── Re-assign SEMUA warga verified (dipanggil saat radius/ibadah berubah) ──
function reassignAllWarga(PDO $db): void {
$wargas = $db->query("SELECT id, lat, lng FROM warga_miskin WHERE status = 'verified'")->fetchAll();
$stmt = $db->prepare("UPDATE warga_miskin SET ibadah_id = ? WHERE id = ?");
foreach ($wargas as $w) {
$ibadahId = findNearestIbadah($db, (float)$w['lat'], (float)$w['lng']);
$stmt->execute([$ibadahId, $w['id']]);
}
}
+181
View File
@@ -0,0 +1,181 @@
<?php
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/helpers.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$db = getDB();
$method = $_SERVER['REQUEST_METHOD'];
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
// ── GET: list semua / satu ───────────────────
if ($method === 'GET') {
$user = getCurrentUser();
if ($id) {
$stmt = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch();
jsonResponse($row ?: ['error' => 'Tidak ditemukan'], $row ? 200 : 404);
}
// Admin, Surveyor, & Pemangku melihat semua, lainnya hanya verified
if ($user && ($user['role'] === 'admin' || $user['role'] === 'surveyor' || $user['role'] === 'pemangku')) {
$rows = $db->query("
SELECT ri.*,
COUNT(wm.id) AS jumlah_kk,
COALESCE(SUM(wm.jumlah_anggota),0) AS jumlah_jiwa,
u.nama_lengkap AS surveyor_nama
FROM rumah_ibadah ri
LEFT JOIN warga_miskin wm ON wm.ibadah_id = ri.id AND wm.status = 'verified'
LEFT JOIN users u ON u.id = ri.surveyor_id
GROUP BY ri.id
ORDER BY ri.created_at DESC
")->fetchAll();
} else {
// Pemangku / publik: hanya verified
$rows = $db->query("
SELECT ri.*,
COUNT(wm.id) AS jumlah_kk,
COALESCE(SUM(wm.jumlah_anggota),0) AS jumlah_jiwa
FROM rumah_ibadah ri
LEFT JOIN warga_miskin wm ON wm.ibadah_id = ri.id AND wm.status = 'verified'
WHERE ri.status = 'verified'
GROUP BY ri.id
ORDER BY ri.created_at DESC
")->fetchAll();
}
jsonResponse(['data' => $rows]);
}
// ── POST: tambah ─────────────────────────────
if ($method === 'POST') {
$user = requireRole(['admin', 'surveyor']);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$required = ['nama','jenis','pic','lat','lng'];
foreach ($required as $f) {
if (empty($body[$f])) jsonResponse(['success'=>false,'error'=>"Field '$f' wajib diisi"], 422);
}
// Surveyor → pending, Admin → verified
$status = ($user['role'] === 'admin') ? 'verified' : 'pending';
$stmt = $db->prepare("
INSERT INTO rumah_ibadah (nama, jenis, alamat, pic, no_wa, radius, lat, lng, foto_path, surveyor_id, status, kecamatan)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$body['nama'],
$body['jenis'],
$body['alamat'] ?? '',
$body['pic'],
$body['no_wa'] ?? '',
(int)($body['radius'] ?? 500),
(float)$body['lat'],
(float)$body['lng'],
$body['foto_path'] ?? null,
$user['id'],
$status,
$body['kecamatan'] ?? null,
]);
$newId = (int)$db->lastInsertId();
// re-assign warga hanya jika langsung verified
if ($status === 'verified') {
reassignAllWarga($db);
}
auditLog($db, $user['id'], 'CREATE_IBADAH', 'rumah_ibadah', $newId, null, $body);
jsonResponse(['success'=>true, 'id'=>$newId, 'status'=>$status]);
}
// ── PUT: update ──────────────────────────────
if ($method === 'PUT') {
$user = requireAuth();
if (!$id) jsonResponse(['success'=>false,'error'=>'ID diperlukan'], 400);
// Cek permission
$old = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
$old->execute([$id]);
$oldData = $old->fetch();
if (!$oldData) jsonResponse(['success'=>false,'error'=>'Tidak ditemukan'], 404);
// Surveyor hanya bisa edit milik sendiri yang masih pending
if ($user['role'] === 'surveyor') {
if ((int)$oldData['surveyor_id'] !== $user['id']) {
jsonResponse(['success'=>false,'error'=>'Anda hanya bisa edit data milik Anda'], 403);
}
if ($oldData['status'] !== 'pending') {
jsonResponse(['success'=>false,'error'=>'Data yang sudah diverifikasi tidak bisa diedit'], 403);
}
}
// Pemangku tidak boleh edit
if ($user['role'] === 'pemangku') {
jsonResponse(['success'=>false,'error'=>'Anda tidak memiliki akses edit'], 403);
}
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$fields = []; $vals = [];
foreach (['nama','jenis','alamat','pic','no_wa','radius','lat','lng','foto_path','kecamatan'] as $f) {
if (array_key_exists($f, $body)) {
$fields[] = "$f = ?";
$vals[] = in_array($f,['lat','lng']) ? (float)$body[$f] : ($f==='radius' ? (int)$body[$f] : $body[$f]);
}
}
if (empty($fields)) jsonResponse(['success'=>false,'error'=>'Tidak ada data diubah'], 422);
$vals[] = $id;
$db->prepare("UPDATE rumah_ibadah SET ".implode(',',$fields)." WHERE id = ?")->execute($vals);
// re-assign ulang jika data verified
if ($oldData['status'] === 'verified') {
reassignAllWarga($db);
}
auditLog($db, $user['id'], 'UPDATE_IBADAH', 'rumah_ibadah', $id, $oldData, $body);
jsonResponse(['success'=>true]);
}
// ── DELETE ───────────────────────────────────
if ($method === 'DELETE') {
$user = requireAuth();
if (!$id) jsonResponse(['success'=>false,'error'=>'ID diperlukan'], 400);
$old = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
$old->execute([$id]);
$oldData = $old->fetch();
if (!$oldData) jsonResponse(['success'=>false,'error'=>'Tidak ditemukan'], 404);
// Surveyor bisa hapus milik sendiri
if ($user['role'] === 'surveyor') {
if ((int)$oldData['surveyor_id'] !== $user['id']) {
jsonResponse(['success'=>false,'error'=>'Anda hanya bisa hapus data milik Anda'], 403);
}
}
if ($user['role'] === 'pemangku') {
jsonResponse(['success'=>false,'error'=>'Anda tidak memiliki akses hapus'], 403);
}
// warga_miskin.ibadah_id akan SET NULL (FK ON DELETE SET NULL)
$db->prepare("DELETE FROM rumah_ibadah WHERE id = ?")->execute([$id]);
// Jika rumah ibadah terverifikasi dihapus, reassign warga sekitar
if ($oldData['status'] === 'verified') {
reassignAllWarga($db);
}
auditLog($db, $user['id'], 'DELETE_IBADAH', 'rumah_ibadah', $id, $oldData);
jsonResponse(['success'=>true]);
}
jsonResponse(['error'=>'Method tidak didukung'], 405);
+74
View File
@@ -0,0 +1,74 @@
<?php
require_once __DIR__ . '/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Credentials: true');
$db = getDB();
$user = getCurrentUser();
// Statistik utama — hanya data verified (digabung dalam 1 query)
$stats = $db->query("
SELECT
(SELECT COUNT(*) FROM rumah_ibadah WHERE status = 'verified') AS ibadah,
COUNT(*) AS kk_total,
COALESCE(SUM(CASE WHEN ibadah_id IS NOT NULL THEN 1 ELSE 0 END), 0) AS kk_terlayani,
COALESCE(SUM(jumlah_anggota), 0) AS jiwa_total,
COALESCE(SUM(CASE WHEN ibadah_id IS NOT NULL THEN jumlah_anggota ELSE 0 END), 0) AS jiwa_terlayani,
COALESCE(SUM(CASE WHEN bantuan_diterima = 'sembako' THEN 1 ELSE 0 END), 0) AS bantuan_sembako,
COALESCE(SUM(CASE WHEN bantuan_diterima = 'beasiswa' THEN 1 ELSE 0 END), 0) AS bantuan_beasiswa,
COALESCE(SUM(CASE WHEN bantuan_diterima = 'modal' THEN 1 ELSE 0 END), 0) AS bantuan_modal,
COALESCE(SUM(CASE WHEN bantuan_diterima = 'tunai' THEN 1 ELSE 0 END), 0) AS bantuan_tunai,
COALESCE(SUM(CASE WHEN bantuan_diterima = 'tidak_ada' THEN 1 ELSE 0 END), 0) AS bantuan_tidak_ada
FROM warga_miskin
WHERE status = 'verified'
")->fetch(PDO::FETCH_ASSOC);
$totalIbadah = (int)($stats['ibadah'] ?? 0);
$totalWarga = (int)($stats['kk_total'] ?? 0);
$totalCovered = (int)($stats['kk_terlayani'] ?? 0);
$totalJiwa = (int)($stats['jiwa_total'] ?? 0);
$jiwaLayan = (int)($stats['jiwa_terlayani'] ?? 0);
$response = [
'ibadah' => $totalIbadah,
'kk_total' => $totalWarga,
'kk_terlayani' => $totalCovered,
'kk_belum' => $totalWarga - $totalCovered,
'jiwa_total' => $totalJiwa,
'jiwa_terlayani' => $jiwaLayan,
'bantuan_sembako' => (int)($stats['bantuan_sembako'] ?? 0),
'bantuan_beasiswa' => (int)($stats['bantuan_beasiswa'] ?? 0),
'bantuan_modal' => (int)($stats['bantuan_modal'] ?? 0),
'bantuan_tunai' => (int)($stats['bantuan_tunai'] ?? 0),
'bantuan_tidak_ada' => (int)($stats['bantuan_tidak_ada'] ?? 0),
];
// Data pending — hanya untuk admin (digabung dalam 1 query)
if ($user && $user['role'] === 'admin') {
$adminStats = $db->query("
SELECT
(SELECT COUNT(*) FROM rumah_ibadah WHERE status = 'pending') AS pending_ibadah,
(SELECT COUNT(*) FROM warga_miskin WHERE status = 'pending') AS pending_warga,
(SELECT COUNT(*) FROM users WHERE is_active = 1) AS total_users
")->fetch(PDO::FETCH_ASSOC);
$response['pending_ibadah'] = (int)($adminStats['pending_ibadah'] ?? 0);
$response['pending_warga'] = (int)($adminStats['pending_warga'] ?? 0);
$response['total_users'] = (int)($adminStats['total_users'] ?? 0);
}
// Data pending milik surveyor (digabung dalam 1 query)
if ($user && $user['role'] === 'surveyor') {
$stmt = $db->prepare("
SELECT
(SELECT COUNT(*) FROM warga_miskin WHERE surveyor_id = ? AND status = 'pending') AS my_pending_warga,
(SELECT COUNT(*) FROM rumah_ibadah WHERE surveyor_id = ? AND status = 'pending') AS my_pending_ibadah
");
$stmt->execute([$user['id'], $user['id']]);
$surveyorStats = $stmt->fetch(PDO::FETCH_ASSOC);
$response['my_pending_warga'] = (int)($surveyorStats['my_pending_warga'] ?? 0);
$response['my_pending_ibadah'] = (int)($surveyorStats['my_pending_ibadah'] ?? 0);
}
jsonResponse($response);
+118
View File
@@ -0,0 +1,118 @@
<?php
require_once __DIR__ . '/config.php';
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method tidak didukung'], 405);
}
$user = requireRole(['admin', 'surveyor']);
// ── Validasi file ────────────────────────────
if (!isset($_FILES['foto']) || $_FILES['foto']['error'] !== UPLOAD_ERR_OK) {
jsonResponse(['success' => false, 'error' => 'File foto wajib diunggah'], 422);
}
$file = $_FILES['foto'];
// Cek ukuran
if ($file['size'] > MAX_UPLOAD_SIZE) {
jsonResponse(['success' => false, 'error' => 'Ukuran file melebihi batas 5MB'], 422);
}
// Cek tipe file
$allowedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
jsonResponse(['success' => false, 'error' => 'Hanya file JPEG dan PNG yang diizinkan'], 422);
}
// ── Buat folder uploads jika belum ada ───────
if (!is_dir(UPLOAD_DIR)) {
mkdir(UPLOAD_DIR, 0755, true);
}
// ── Generate nama file unik ──────────────────
$ext = ($mimeType === 'image/png') ? 'png' : 'jpg';
$filename = date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$destPath = UPLOAD_DIR . $filename;
$webPath = 'uploads/' . $filename;
// ── Pindahkan file ───────────────────────────
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
jsonResponse(['success' => false, 'error' => 'Gagal menyimpan file'], 500);
}
// ── Extract EXIF GPS ─────────────────────────
$lat = null;
$lng = null;
$hasGps = false;
if ($mimeType === 'image/jpeg' || $mimeType === 'image/jpg') {
$exif = @exif_read_data($destPath, 'GPS', true);
if ($exif && isset($exif['GPS']['GPSLatitude']) && isset($exif['GPS']['GPSLongitude'])) {
$lat = exifGpsToDecimal(
$exif['GPS']['GPSLatitude'],
$exif['GPS']['GPSLatitudeRef'] ?? 'N'
);
$lng = exifGpsToDecimal(
$exif['GPS']['GPSLongitude'],
$exif['GPS']['GPSLongitudeRef'] ?? 'E'
);
$hasGps = true;
}
}
auditLog(getDB(), $user['id'], 'UPLOAD_FOTO', null, null, null, [
'filename' => $filename, 'has_gps' => $hasGps
]);
jsonResponse([
'success' => true,
'foto_path' => $webPath,
'lat' => $lat,
'lng' => $lng,
'has_gps' => $hasGps,
]);
// ══════════════════════════════════════════════
// EXIF GPS Helper
// ══════════════════════════════════════════════
/**
* Konversi EXIF GPS coordinate (DMS) ke desimal
* @param array $coord Array of 3 fractions [degrees, minutes, seconds]
* @param string $ref Hemisphere reference (N/S/E/W)
*/
function exifGpsToDecimal(array $coord, string $ref): float {
$degrees = exifFractionToFloat($coord[0]);
$minutes = exifFractionToFloat($coord[1]);
$seconds = exifFractionToFloat($coord[2]);
$decimal = $degrees + ($minutes / 60) + ($seconds / 3600);
if ($ref === 'S' || $ref === 'W') {
$decimal *= -1;
}
return round($decimal, 8);
}
/**
* Konversi fraction string "num/den" ke float
*/
function exifFractionToFloat(string $fraction): float {
$parts = explode('/', $fraction);
if (count($parts) === 2 && (float)$parts[1] !== 0.0) {
return (float)$parts[0] / (float)$parts[1];
}
return (float)$parts[0];
}
+99
View File
@@ -0,0 +1,99 @@
<?php
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/helpers.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, PUT, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$db = getDB();
$method = $_SERVER['REQUEST_METHOD'];
$type = $_GET['type'] ?? ''; // 'ibadah' atau 'warga'
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
if (!in_array($type, ['ibadah', 'warga'])) {
jsonResponse(['error' => 'Parameter type harus "ibadah" atau "warga"'], 400);
}
$table = $type === 'ibadah' ? 'rumah_ibadah' : 'warga_miskin';
// ── GET: list data pending ───────────────────
if ($method === 'GET') {
$admin = requireRole('admin');
$status = $_GET['status'] ?? 'pending';
if (!in_array($status, ['pending', 'verified', 'rejected'])) $status = 'pending';
if ($type === 'ibadah') {
$stmt = $db->prepare("
SELECT ri.*, u.nama_lengkap AS surveyor_nama
FROM rumah_ibadah ri
LEFT JOIN users u ON u.id = ri.surveyor_id
WHERE ri.status = ?
ORDER BY ri.created_at DESC
");
} else {
$stmt = $db->prepare("
SELECT wm.*,
ri.nama AS ibadah_nama,
u.nama_lengkap AS surveyor_nama
FROM warga_miskin wm
LEFT JOIN rumah_ibadah ri ON ri.id = wm.ibadah_id
LEFT JOIN users u ON u.id = wm.surveyor_id
WHERE wm.status = ?
ORDER BY wm.created_at DESC
");
}
$stmt->execute([$status]);
jsonResponse(['data' => $stmt->fetchAll()]);
}
// ── PUT: approve / reject ────────────────────
if ($method === 'PUT') {
$admin = requireRole('admin');
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$newStatus = $body['status'] ?? '';
if (!in_array($newStatus, ['verified', 'rejected'])) {
jsonResponse(['success' => false, 'error' => 'Status harus "verified" atau "rejected"'], 422);
}
// Ambil data lama
$old = $db->prepare("SELECT * FROM $table WHERE id = ?");
$old->execute([$id]);
$oldData = $old->fetch();
if (!$oldData) {
jsonResponse(['success' => false, 'error' => 'Data tidak ditemukan'], 404);
}
// Update status
$alasan = ($newStatus === 'rejected') ? ($body['alasan_penolakan'] ?? '') : null;
$stmt = $db->prepare("UPDATE $table SET status = ?, alasan_penolakan = ? WHERE id = ?");
$stmt->execute([$newStatus, $alasan, $id]);
// Jika diverifikasi, proses spasial
if ($newStatus === 'verified') {
if ($type === 'warga') {
// Cari ibadah terdekat untuk warga yang baru verified
$ibadahId = findNearestIbadah($db, (float)$oldData['lat'], (float)$oldData['lng']);
$db->prepare("UPDATE warga_miskin SET ibadah_id = ? WHERE id = ?")->execute([$ibadahId, $id]);
} elseif ($type === 'ibadah') {
// Ibadah baru verified → re-assign semua warga
reassignAllWarga($db);
}
}
$aksi = ($newStatus === 'verified') ? 'VERIFY_' : 'REJECT_';
$aksi .= strtoupper($type);
auditLog($db, $admin['id'], $aksi, $table, $id, $oldData, [
'status' => $newStatus, 'alasan_penolakan' => $alasan
]);
jsonResponse(['success' => true, 'status' => $newStatus]);
}
jsonResponse(['error' => 'Method tidak didukung'], 405);
+233
View File
@@ -0,0 +1,233 @@
<?php
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/helpers.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$db = getDB();
$method = $_SERVER['REQUEST_METHOD'];
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
// ── GET ───────────────────────────────────────
if ($method === 'GET') {
$user = getCurrentUser();
// Filter tambahan: kecamatan
$filterKec = $_GET['kecamatan'] ?? null;
$where = [];
$params = [];
if ($filterKec) { $where[] = "wm.kecamatan = ?"; $params[] = $filterKec; }
if ($user && ($user['role'] === 'admin' || $user['role'] === 'surveyor' || $user['role'] === 'pemangku')) {
// Admin, Surveyor, & Pemangku lihat semua data (untuk mencegah input ganda & analisis lengkap)
$statusFilter = "";
} else {
// Publik: hanya verified
$statusFilter = "AND wm.status = 'verified'";
}
$whereClause = count($where) > 0 ? 'WHERE ' . implode(' AND ', $where) . ' ' . $statusFilter : ($statusFilter ? 'WHERE 1=1 ' . $statusFilter : '');
$sql = "
SELECT wm.*,
ri.nama AS ibadah_nama,
ri.jenis AS ibadah_jenis,
u.nama_lengkap AS surveyor_nama
FROM warga_miskin wm
LEFT JOIN rumah_ibadah ri ON ri.id = wm.ibadah_id
LEFT JOIN users u ON u.id = wm.surveyor_id
$whereClause
ORDER BY wm.created_at DESC
";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
jsonResponse(['data' => $rows]);
}
// ── POST: tambah warga ───────────────────────
if ($method === 'POST') {
$user = requireRole(['admin', 'surveyor']);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
foreach (['nama_kk','lat','lng'] as $f) {
if (empty($body[$f])) jsonResponse(['success'=>false,'error'=>"Field '$f' wajib diisi"], 422);
}
// Validasi NIK: harus 16 digit angka jika diisi
$nik = $body['nik'] ?? null;
if ($nik !== null && $nik !== '') {
$nik = trim($nik);
if (!preg_match('/^\d{16}$/', $nik)) {
jsonResponse(['success'=>false,'error'=>'NIK harus tepat 16 digit angka'], 422);
}
// Cek duplikasi NIK
$chk = $db->prepare("SELECT id FROM warga_miskin WHERE nik = ?");
$chk->execute([$nik]);
if ($chk->fetch()) {
jsonResponse(['success'=>false,'error'=>'NIK Kepala Keluarga sudah terdaftar di sistem'], 422);
}
} else {
$nik = null;
}
$lat = (float)$body['lat'];
$lng = (float)$body['lng'];
// Surveyor → pending, Admin → verified
$status = ($user['role'] === 'admin') ? 'verified' : 'pending';
// otomatis cari rumah ibadah terdekat (hanya jika verified)
$ibadahId = ($status === 'verified') ? findNearestIbadah($db, $lat, $lng) : null;
$bantuan = $body['bantuan_diterima'] ?? 'tidak_ada';
if (!in_array($bantuan, ['tidak_ada', 'sembako', 'beasiswa', 'modal', 'tunai'])) {
$bantuan = 'tidak_ada';
}
$stmt = $db->prepare("
INSERT INTO warga_miskin (nama_kk, jumlah_anggota, keterangan, lat, lng, ibadah_id, foto_path, surveyor_id, status, alamat, nik, kecamatan, bantuan_diterima)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$body['nama_kk'],
(int)($body['jumlah_anggota'] ?? 1),
$body['keterangan'] ?? '',
$lat, $lng, $ibadahId,
$body['foto_path'] ?? null,
$user['id'],
$status,
$body['alamat'] ?? null,
$nik,
$body['kecamatan'] ?? null,
$bantuan,
]);
$newId = (int)$db->lastInsertId();
// ambil nama ibadah untuk respon
$ibadahNama = null;
if ($ibadahId) {
$r = $db->prepare("SELECT nama FROM rumah_ibadah WHERE id = ?");
$r->execute([$ibadahId]);
$ibadahNama = $r->fetchColumn();
}
auditLog($db, $user['id'], 'CREATE_WARGA', 'warga_miskin', $newId, null, $body);
jsonResponse([
'success' => true,
'id' => $newId,
'status' => $status,
'ibadah_id' => $ibadahId,
'ibadah_nama' => $ibadahNama,
]);
}
// ── PUT: update warga ────────────────────────
if ($method === 'PUT') {
$user = requireAuth();
if (!$id) jsonResponse(['success'=>false,'error'=>'ID diperlukan'], 400);
$old = $db->prepare("SELECT * FROM warga_miskin WHERE id = ?");
$old->execute([$id]);
$oldData = $old->fetch();
if (!$oldData) jsonResponse(['success'=>false,'error'=>'Tidak ditemukan'], 404);
// Surveyor hanya bisa edit milik sendiri yang masih pending
if ($user['role'] === 'surveyor') {
if ((int)$oldData['surveyor_id'] !== $user['id']) {
jsonResponse(['success'=>false,'error'=>'Anda hanya bisa edit data milik Anda'], 403);
}
if ($oldData['status'] !== 'pending') {
jsonResponse(['success'=>false,'error'=>'Data yang sudah diverifikasi tidak bisa diedit'], 403);
}
}
if ($user['role'] === 'pemangku') {
jsonResponse(['success'=>false,'error'=>'Anda tidak memiliki akses edit'], 403);
}
$body = json_decode(file_get_contents('php://input'), true) ?? [];
// Validasi NIK jika diubah
if (array_key_exists('nik', $body) && $body['nik'] !== null && $body['nik'] !== '') {
$nikVal = trim($body['nik']);
if (!preg_match('/^\d{16}$/', $nikVal)) {
jsonResponse(['success'=>false,'error'=>'NIK harus tepat 16 digit angka'], 422);
}
// Cek duplikasi NIK (kecuali data sendiri)
$chk = $db->prepare("SELECT id FROM warga_miskin WHERE nik = ? AND id != ?");
$chk->execute([$nikVal, $id]);
if ($chk->fetch()) {
jsonResponse(['success'=>false,'error'=>'NIK Kepala Keluarga sudah terdaftar di sistem'], 422);
}
}
$fields = []; $vals = [];
foreach (['nama_kk','jumlah_anggota','keterangan','lat','lng','foto_path','alamat','nik','kecamatan','bantuan_diterima'] as $f) {
if (array_key_exists($f, $body)) {
$fields[] = "$f = ?";
if ($f === 'nik') {
$nikVal = $body['nik'] !== null ? trim($body['nik']) : '';
$vals[] = ($nikVal === '') ? null : $nikVal;
} else {
$vals[] = in_array($f,['lat','lng']) ? (float)$body[$f]
: ($f==='jumlah_anggota' ? (int)$body[$f] : $body[$f]);
}
}
}
if (empty($fields)) jsonResponse(['success'=>false,'error'=>'Tidak ada data diubah'], 422);
// jika koordinat berubah dan data verified, cari ulang ibadah terdekat
if ($oldData['status'] === 'verified' && (array_key_exists('lat', $body) || array_key_exists('lng', $body))) {
$lat = array_key_exists('lat',$body) ? (float)$body['lat'] : (float)$oldData['lat'];
$lng = array_key_exists('lng',$body) ? (float)$body['lng'] : (float)$oldData['lng'];
$ibadahId = findNearestIbadah($db, $lat, $lng);
$fields[] = "ibadah_id = ?";
$vals[] = $ibadahId;
}
$vals[] = $id;
$db->prepare("UPDATE warga_miskin SET ".implode(',',$fields)." WHERE id = ?")->execute($vals);
auditLog($db, $user['id'], 'UPDATE_WARGA', 'warga_miskin', $id, $oldData, $body);
jsonResponse(['success'=>true]);
}
// ── DELETE ───────────────────────────────────
if ($method === 'DELETE') {
$user = requireAuth();
if (!$id) jsonResponse(['success'=>false,'error'=>'ID diperlukan'], 400);
$old = $db->prepare("SELECT * FROM warga_miskin WHERE id = ?");
$old->execute([$id]);
$oldData = $old->fetch();
if (!$oldData) jsonResponse(['success'=>false,'error'=>'Tidak ditemukan'], 404);
// Surveyor bisa hapus milik sendiri
if ($user['role'] === 'surveyor') {
if ((int)$oldData['surveyor_id'] !== $user['id']) {
jsonResponse(['success'=>false,'error'=>'Anda hanya bisa hapus data milik Anda'], 403);
}
}
if ($user['role'] === 'pemangku') {
jsonResponse(['success'=>false,'error'=>'Anda tidak memiliki akses hapus'], 403);
}
$db->prepare("DELETE FROM warga_miskin WHERE id = ?")->execute([$id]);
auditLog($db, $user['id'], 'DELETE_WARGA', 'warga_miskin', $id, $oldData);
jsonResponse(['success'=>true]);
}
jsonResponse(['error'=>'Method tidak didukung'], 405);
+85
View File
@@ -0,0 +1,85 @@
CREATE DATABASE IF NOT EXISTS webgis_poverty_mapping
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE webgis_poverty_mapping;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
nama_lengkap VARCHAR(150) NOT NULL,
role ENUM('admin','surveyor','pemangku') NOT NULL DEFAULT 'surveyor',
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Admin default — password: admin123
-- Hash di-generate dengan password_hash('admin123', PASSWORD_BCRYPT)
INSERT INTO users (username, password, nama_lengkap, role) VALUES
('admin', '$2y$10$Xb5FU6uo67rVETXAlSYJ..wn8IudyYwGzUbyMe8yQ1TLFlvt.S9F6', 'Administrator', 'admin');
CREATE TABLE rumah_ibadah (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(200) NOT NULL,
jenis ENUM('masjid','gereja','pura','klenteng','vihara','lain') NOT NULL DEFAULT 'masjid',
alamat TEXT,
pic VARCHAR(150) NOT NULL,
no_wa VARCHAR(30) DEFAULT NULL,
radius INT NOT NULL DEFAULT 500, -- meter
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
foto_path VARCHAR(255) DEFAULT NULL,
surveyor_id INT DEFAULT NULL,
status ENUM('pending','verified','rejected') NOT NULL DEFAULT 'verified',
alasan_penolakan TEXT DEFAULT NULL,
kecamatan VARCHAR(100) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_ri_surveyor FOREIGN KEY (surveyor_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE warga_miskin (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_kk VARCHAR(200) NOT NULL,
nik VARCHAR(16) DEFAULT NULL,
jumlah_anggota INT NOT NULL DEFAULT 1,
keterangan TEXT,
alamat TEXT DEFAULT NULL,
kecamatan VARCHAR(100) DEFAULT NULL,
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
ibadah_id INT DEFAULT NULL, -- NULL = belum terjangkau
foto_path VARCHAR(255) DEFAULT NULL,
surveyor_id INT DEFAULT NULL,
status ENUM('pending','verified','rejected') NOT NULL DEFAULT 'verified',
alasan_penolakan TEXT DEFAULT NULL,
bantuan_diterima ENUM('sembako','beasiswa','modal','tunai','tidak_ada') NOT NULL DEFAULT 'tidak_ada',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uq_warga_nik UNIQUE (nik),
CONSTRAINT fk_wm_ibadah FOREIGN KEY (ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL,
CONSTRAINT fk_wm_surveyor FOREIGN KEY (surveyor_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE audit_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
aksi VARCHAR(100) NOT NULL,
target_type VARCHAR(50) DEFAULT NULL,
target_id INT DEFAULT NULL,
data_before JSON DEFAULT NULL,
data_after JSON DEFAULT NULL,
ip_address VARCHAR(45) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_audit_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE INDEX idx_ri_status ON rumah_ibadah(status);
CREATE INDEX idx_ri_kecamatan ON rumah_ibadah(kecamatan);
CREATE INDEX idx_wm_status ON warga_miskin(status);
CREATE INDEX idx_wm_kecamatan ON warga_miskin(kecamatan);
CREATE INDEX idx_wm_ibadah ON warga_miskin(ibadah_id);
CREATE INDEX idx_audit_user ON audit_logs(user_id);
CREATE INDEX idx_audit_aksi ON audit_logs(aksi);
CREATE INDEX idx_audit_created ON audit_logs(created_at);
File diff suppressed because it is too large Load Diff
+3306
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
# Hanya izinkan akses file gambar
<FilesMatch "\.(jpg|jpeg|png|gif|webp)$">
Require all granted
</FilesMatch>
# Blok semua file lain
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
# Disable directory listing
Options -Indexes
# Disable PHP execution
php_flag engine off