Files
2026-06-10 20:41:42 +07:00

276 lines
8.2 KiB
PHP

<?php
// Konfigurasi database
$host = getenv('DB_HOST') ?: 'vg4so4ccs0444o4cgkssgos8';
$port = getenv('DB_PORT') ?: 3306;
$db = getenv('DB_NAME') ?: 'default';
$user = getenv('DB_USER') ?: 'mysql';
$pass = getenv('DB_PASS') ?: 'tg9lvPlr4hgUhgtT7aPq9XelkCEvvMGtsoV8eB4dD5FAkz891PwkJT340xmaEWxL';
$charset = 'utf8mb4';
// Direktori upload foto laporan
define('UPLOAD_DIR', __DIR__ . '/uploads/');
// Mulai session
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Koneksi PDO
try {
$pdo = new PDO(
"mysql:host={$host};port={$port};dbname={$db};charset={$charset}",
$user,
$pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false,
]
);
} catch (PDOException $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => false,
'data' => null,
'message' => 'Koneksi database gagal: ' . $e->getMessage()
]);
exit;
}
// ── CORS Headers ─────────────────────────────────────────────
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Handle preflight request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// ============================================================
// FUNGSI UTILITAS
// ============================================================
/**
* Kirim respon JSON standar lalu hentikan eksekusi.
* Format: {"success": true/false, "data": ..., "message": "..."}
*/
function jsonResponse($data = null, string $message = '', bool $success = true, int $httpCode = 200, array $extra = []): void
{
http_response_code($httpCode);
header('Content-Type: application/json; charset=utf-8');
$response = [
'success' => $success,
'data' => $data,
'message' => $message
];
// Gabungkan field tambahan (misal: duplikasi_warning)
if (!empty($extra)) {
$response = array_merge($response, $extra);
}
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
/**
* Bersihkan input string dari tag HTML.
*/
function clean(string $value): string
{
return htmlspecialchars(strip_tags(trim($value)), ENT_QUOTES, 'UTF-8');
}
/**
* Hitung jarak Haversine antara dua koordinat dalam kilometer.
*/
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float
{
$R = 6371; // Radius bumi dalam km
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat / 2) * sin($dLat / 2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLng / 2) * sin($dLng / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return round($R * $c, 4);
}
/**
* Auto-assign: cari rumah ibadah terdekat yang radius-nya mencakup lokasi KK.
* Mengembalikan ['rumah_ibadah_id' => int|null, 'jarak_km' => float|null]
*/
function autoAssign(PDO $pdo, float $lat, float $lng): array
{
$result = ['rumah_ibadah_id' => null, 'jarak_km' => null];
$stmt = $pdo->query("SELECT id, lat, lng, radius_km FROM rumah_ibadah");
$rows = $stmt->fetchAll();
$bestDist = PHP_FLOAT_MAX;
$bestId = null;
foreach ($rows as $row) {
$dist = haversine($lat, $lng, (float)$row['lat'], (float)$row['lng']);
// Hanya assign jika KK berada dalam radius RI dan merupakan yang terdekat
if ($dist <= (float)$row['radius_km'] && $dist < $bestDist) {
$bestDist = $dist;
$bestId = (int)$row['id'];
}
}
if ($bestId !== null) {
$result['rumah_ibadah_id'] = $bestId;
$result['jarak_km'] = $bestDist;
}
return $result;
}
/**
* Cek apakah user sudah login. Jika belum, kirim 401.
* Mengembalikan data session user.
*/
function requireLogin(): array
{
if (empty($_SESSION['user'])) {
jsonResponse(null, 'Tidak diizinkan. Silakan login terlebih dahulu.', false, 401);
}
return $_SESSION['user'];
}
/**
* Cek apakah role user termasuk dalam daftar yang diizinkan.
* Jika tidak, kirim 403.
*/
function requireRole(array $allowedRoles): array
{
$user = requireLogin();
if (!in_array($user['role'], $allowedRoles)) {
jsonResponse(null, 'Akses ditolak. Role Anda tidak memiliki izin.', false, 403);
}
return $user;
}
/**
* Blokir aksi tulis (POST/PUT/DELETE) untuk role walikota.
* Walikota hanya boleh GET.
*/
function blockWalikotaWrite(): void
{
if (!empty($_SESSION['user']) && $_SESSION['user']['role'] === 'walikota') {
$method = $_SERVER['REQUEST_METHOD'];
if (in_array($method, ['POST', 'PUT', 'DELETE'])) {
jsonResponse(null, 'Walikota hanya memiliki akses baca (read-only).', false, 403);
}
}
}
/**
* Generate kode pengurus dengan format: RI-[JENIS 3 HURUF]-[RANDOM 4 DIGIT]
* Contoh: RI-MSJ-4821, RI-GRJ-7153
*/
function generateKodePengurus(PDO $pdo, string $jenis): string
{
// Mapping jenis ke 3 huruf
$map = [
'Masjid' => 'MSJ',
'Gereja' => 'GRJ',
'Pura' => 'PRA',
'Vihara' => 'VHR',
'Klenteng' => 'KLT',
'Lainnya' => 'LNY',
];
$prefix = $map[$jenis] ?? 'LNY';
// Coba generate kode unik (maks 100 percobaan)
for ($i = 0; $i < 100; $i++) {
$angka = str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT);
$kode = "RI-{$prefix}-{$angka}";
$stmt = $pdo->prepare("SELECT id FROM rumah_ibadah WHERE kode_pengurus = ? LIMIT 1");
$stmt->execute([$kode]);
if ($stmt->rowCount() === 0) {
return $kode;
}
}
// Fallback: tambahkan timestamp
return "RI-{$prefix}-" . substr(time(), -4);
}
/**
* Cek rate limit untuk IP tertentu. Maks 5 submit per jam.
* Mengembalikan true jika masih diizinkan, false jika melebihi batas.
*/
function checkRateLimit(PDO $pdo, string $ip): bool
{
// Hapus record yang lebih dari 1 jam
$pdo->prepare("DELETE FROM rate_limit WHERE created_at < NOW() - INTERVAL 1 HOUR")->execute();
// Hitung jumlah submit dalam 1 jam terakhir
$stmt = $pdo->prepare("SELECT COUNT(*) as cnt FROM rate_limit WHERE ip_address = ? AND created_at >= NOW() - INTERVAL 1 HOUR");
$stmt->execute([$ip]);
$count = (int)$stmt->fetch()['cnt'];
if ($count >= 5) {
return false; // Sudah melebihi batas
}
// Catat submit baru
$stmt = $pdo->prepare("INSERT INTO rate_limit (ip_address) VALUES (?)");
$stmt->execute([$ip]);
return true;
}
/**
* Handle upload file foto. Mengembalikan path relatif file atau null.
*/
function handleUpload(string $fieldName): ?string
{
if (!isset($_FILES[$fieldName]) || $_FILES[$fieldName]['error'] !== UPLOAD_ERR_OK) {
return null;
}
$file = $_FILES[$fieldName];
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB
if (!in_array($file['type'], $allowedTypes)) {
jsonResponse(null, 'Tipe file tidak diizinkan. Gunakan JPG, PNG, GIF, atau WebP.', false, 400);
}
if ($file['size'] > $maxSize) {
jsonResponse(null, 'Ukuran file melebihi batas 5MB.', false, 400);
}
// Buat direktori upload jika belum ada
if (!is_dir(UPLOAD_DIR)) {
mkdir(UPLOAD_DIR, 0755, true);
}
// Generate nama file unik
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = 'laporan_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$dest = UPLOAD_DIR . $filename;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
jsonResponse(null, 'Gagal menyimpan file foto.', false, 500);
}
return 'uploads/' . $filename;
}
/**
* Dapatkan IP address client (mendukung proxy).
*/
function getClientIP(): string
{
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
}
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}