IMG-LOGO
Nederlands
Only a Safe rally is a Good Rally OUR mission – your safety – your live  FIA Safety Rally

Baillet et Colliard vont défier les Belges en Classic 65

2024-04-19
IMG

C’est un scénario dont les amateurs d’épreuves de régularité se délectent, et dont le 1er Legend Condroz Rally (10-11-12 mai) va également pouvoir profiter ! Si la discipline est née en Belgique, et si les pilotes et copilotes de notre pays en ont fait une spécialité, il est un duo français, composé de Christophe Baillet et Pierre Colliard, qui prend un malin plaisir à constamment déjouer leurs plans. Duo qui vient de rentrer son engagement en vue du Legend Condroz Rally 2024, et qui s’imposera donc parmi les favoris dans la course à la victoire en ‘Classic 65’. Rappelons que Baillet et Colliard se sont imposés à Ypres fin 2023, avant de prendre une nouvelle fois le meilleur sur les Belges lors du Rallye Neige et Glace il y a quelques semaines ! Le contingent belge s’est néanmoins renforcé entretemps, avec la confirmation de la présence de Daniel Reuter et Robert Vandevorst (Porsche 911), mais aussi de Stéphane Blaise et Patrick Lienne (Volkswagen Golf GTi). Va donc y avoir du sport !

Signalons également l’inscription en catégorie ‘Challenger’, à 80 km/h de moyenne, d’Etienne Baugnée et Vincent Duchesne, dont la Ford Escort Mk1 s’impose naturellement comme la favorite logique. Même phénomène en ‘Classic 50’, avec les spécialistes Patrice Simon et Christian Bernard, avec leur bien connue Porsche 924. Infos & inscriptions : www.condrozrally.be

 Tous les articles

$part) { if (preg_match('/^\{(.+)\}$/', $part, $matches)) { $params[$matches[1]] = $pathParts[$i]; continue; } if ($part !== $pathParts[$i]) { return false; } } return true; } function dashboardData(): array { $pdo = Database::connection(); $user = current_user(); if (is_admin()) { $counts = [ 'users' => (int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn(), 'listings' => (int) $pdo->query('SELECT COUNT(*) FROM listings')->fetchColumn(), 'active_listings' => (int) $pdo->query("SELECT COUNT(*) FROM listings WHERE status = 'active'")->fetchColumn(), 'locked_accounts' => (int) $pdo->query('SELECT COUNT(*) FROM users WHERE is_account_locked = 1')->fetchColumn(), ]; $recentListings = $pdo->query('SELECT l.*, u.user_id, u.profile_name FROM listings l INNER JOIN users u ON u.id = l.owner_user_id ORDER BY l.updated_at DESC LIMIT 10')->fetchAll(); } else { $stmt = $pdo->prepare('SELECT COUNT(*) FROM listings WHERE owner_user_id = :uid'); $stmt->execute([':uid' => $user['id']]); $listings = (int) $stmt->fetchColumn(); $stmt = $pdo->prepare("SELECT COUNT(*) FROM listings WHERE owner_user_id = :uid AND status = 'active'"); $stmt->execute([':uid' => $user['id']]); $activeListings = (int) $stmt->fetchColumn(); $counts = [ 'users' => 1, 'listings' => $listings, 'active_listings' => $activeListings, 'locked_accounts' => (int) $user['is_account_locked'], ]; $stmt = $pdo->prepare('SELECT l.*, u.user_id, u.profile_name FROM listings l INNER JOIN users u ON u.id = l.owner_user_id WHERE l.owner_user_id = :uid ORDER BY l.updated_at DESC LIMIT 10'); $stmt->execute([':uid' => $user['id']]); $recentListings = $stmt->fetchAll(); } $recentLogs = []; if (is_admin()) { $recentLogs = $pdo->query('SELECT a.*, u.user_id AS actor_user_id_text, t.user_id AS target_user_id_text FROM audit_logs a LEFT JOIN users u ON u.id = a.actor_user_id LEFT JOIN users t ON t.id = a.target_user_id ORDER BY a.id DESC LIMIT 10')->fetchAll(); } return compact('counts', 'recentListings', 'recentLogs'); } function fetchListings(): array { $pdo = Database::connection(); $user = current_user(); if (is_admin()) { return $pdo->query('SELECT l.*, u.user_id, u.profile_name FROM listings l INNER JOIN users u ON u.id = l.owner_user_id ORDER BY l.updated_at DESC')->fetchAll(); } $stmt = $pdo->prepare('SELECT l.*, u.user_id, u.profile_name FROM listings l INNER JOIN users u ON u.id = l.owner_user_id WHERE l.owner_user_id = :uid ORDER BY l.updated_at DESC'); $stmt->execute([':uid' => $user['id']]); return $stmt->fetchAll(); } function findListing(int $id): ?array { $pdo = Database::connection(); $stmt = $pdo->prepare('SELECT l.*, u.user_id, u.profile_name FROM listings l INNER JOIN users u ON u.id = l.owner_user_id WHERE l.id = :id LIMIT 1'); $stmt->execute([':id' => $id]); $row = $stmt->fetch(); return $row ?: null; } function assertListingAccess(array $listing): void { $user = current_user(); if (!is_admin() && (int) $listing['owner_user_id'] !== (int) $user['id']) { http_response_code(403); exit('Anda tidak punya akses ke baris ini.'); } } function fetchUsers(): array { $pdo = Database::connection(); return $pdo->query('SELECT u.*, (SELECT COUNT(*) FROM listings l WHERE l.owner_user_id = u.id) AS listing_count FROM users u ORDER BY u.id ASC')->fetchAll(); } function findUser(int $id): ?array { $pdo = Database::connection(); $stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id LIMIT 1'); $stmt->execute([':id' => $id]); $row = $stmt->fetch(); return $row ?: null; } function input_length(string $value): int { if (function_exists('mb_strlen')) { return mb_strlen($value, 'UTF-8'); } $count = preg_match_all('/./us', $value); return $count === false ? strlen($value) : $count; } function user_id_exists(string $userId, int $exceptId = 0): bool { $stmt = Database::connection()->prepare('SELECT id FROM users WHERE user_id = :user_id AND id <> :except_id LIMIT 1'); $stmt->execute([':user_id' => $userId, ':except_id' => $exceptId]); return $stmt->fetchColumn() !== false; } function user_audit_snapshot(array $user): array { unset($user['password'], $user['current_session_id'], $user['remember_token']); return $user; } function validate_listing_input(int $ownerId, string $url, string $anchorText, string $returnPath): void { if (!filter_var($url, FILTER_VALIDATE_URL) || !in_array(strtolower((string) parse_url($url, PHP_URL_SCHEME)), ['http', 'https'], true)) { flash('error', 'URL harus berupa alamat http:// atau https:// yang valid.'); redirect($returnPath); } if (input_length($anchorText) > 255 || strlen($url) > 65535) { flash('error', 'Anchor maksimal 255 karakter dan URL terlalu panjang tidak dapat disimpan.'); redirect($returnPath); } if ($ownerId <= 0 || !findUser($ownerId)) { flash('error', 'User pemilik backlink tidak ditemukan. Pilih user yang tersedia.'); redirect($returnPath); } } function finish_listing_change(int $actorId, string $reason, string $successMessage, string $returnPath): never { try { ListingExporter::regenerate($actorId, $reason); flash('success', $successMessage); } catch (Throwable $e) { error_log('SERPLIV export error: ' . $e->getMessage()); flash('error', 'Perubahan sudah tersimpan di database, tetapi data.txt gagal diperbarui. Cek permission data.txt, lalu klik Regenerate di Dashboard.'); } redirect($returnPath); } function require_sensitive_reason(): string { $reason = trim((string) ($_POST['reason'] ?? '')); if ($reason === '') { flash('error', 'Alasan wajib diisi untuk aksi ini.'); redirect('users'); } return $reason; } if ($path === '/' && $method === 'GET') { if (!current_user()) { view('auth/login', ['title' => 'Login']); return; } redirect('dashboard'); } if ($path === '/login' && $method === 'POST') { verify_csrf(); $userId = trim((string) ($_POST['user_id'] ?? '')); $password = (string) ($_POST['password'] ?? ''); if ($userId === '' || $password === '') { flash('error', 'User ID dan password wajib diisi.'); redirect(''); } if (Auth::attempt($userId, $password)) { $user = current_user(); if ((int) ($user['must_change_password'] ?? 0) === 1) { redirect('change-password'); } redirect('dashboard'); } redirect(''); } if ($path === '/logout' && $method === 'POST') { verify_csrf(); Auth::logout(); session_start(); flash('success', 'Berhasil logout.'); redirect(''); } if ($path === '/change-password' && $method === 'GET') { require_auth(); view('auth/change-password', ['title' => 'Ganti Password']); return; } if ($path === '/change-password' && $method === 'POST') { verify_csrf(); require_auth(); $user = current_user(); if ((int) $user['is_profile_locked'] === 1 && (int) $user['must_change_password'] !== 1) { flash('error', 'Profil Anda sedang dikunci oleh Master Admin. Anda tidak dapat mengubah data saat ini. Silakan hubungi Master Admin untuk membuka profil Anda dan melakukan reset password jika diperlukan.'); redirect('dashboard'); } $newPassword = (string) ($_POST['new_password'] ?? ''); $confirmPassword = (string) ($_POST['confirm_password'] ?? ''); if (strlen($newPassword) < 8) { flash('error', 'Password baru minimal 8 karakter.'); redirect('change-password'); } if ($newPassword !== $confirmPassword) { flash('error', 'Konfirmasi password tidak cocok.'); redirect('change-password'); } Auth::changePassword((int) $user['id'], $newPassword); Audit::log((int) $user['id'], 'password_changed'); $_SESSION['auth_user']['must_change_password'] = 0; flash('success', 'Password berhasil diganti.'); redirect('dashboard'); } if ($path === '/dashboard' && $method === 'GET') { require_auth(); $user = current_user(); if ((int) $user['must_change_password'] === 1 && empty($_SESSION['impersonator'])) { redirect('change-password'); } view('dashboard', ['title' => 'Dashboard'] + dashboardData()); return; } if ($path === '/listings' && $method === 'GET') { require_auth(); view('listings/index', ['title' => 'Backlinks', 'listings' => fetchListings()]); return; } if ($path === '/bulk-upload' && $method === 'GET') { require_admin(); $users = fetchUsers(); view('bulk-upload', [ 'title' => 'Bulk Upload', 'users' => $users, ]); return; } if ($path === '/bulk-upload' && $method === 'POST') { verify_csrf(); require_admin(); $actor = current_user(); $ownerId = (int) ($_POST['owner_user_id'] ?? 0); $bulkContent = trim((string) ($_POST['bulk_content'] ?? '')); if ($ownerId <= 0) { flash('error', 'Pilih user tujuan terlebih dahulu.'); redirect('bulk-upload'); } if ($bulkContent === '') { flash('error', 'Isi bulk upload masih kosong.'); redirect('bulk-upload'); } try { $result = BulkListingImporter::importForUser($ownerId, (int) $actor['id'], $bulkContent); if ((int) $result['parsed'] === 0) { flash('error', 'Tidak ada format backlink yang valid. Gunakan format

anchor

.'); redirect('bulk-upload'); } $_SESSION['bulk_upload_result'] = $result; $summary = 'Bulk upload selesai: ' . (int) $result['inserted'] . ' backlink ditambahkan ke ' . ($result['owner']['user_id'] ?? 'user') . ', ' . (int) $result['duplicates'] . ' duplikat dilewati.'; if (!empty($result['export_error'])) { flash('error', $summary . ' Data tersimpan, tetapi data.txt gagal diperbarui. Cek permission data.txt, lalu klik Regenerate di Dashboard.'); } else { flash('success', $summary . ' data.txt sudah disinkronkan.'); } } catch (Throwable $e) { flash('error', 'Bulk upload gagal: ' . $e->getMessage()); } redirect('bulk-upload'); } if ($path === '/listings/create' && $method === 'GET') { require_auth(); $users = is_admin() ? fetchUsers() : []; view('listings/form', ['title' => 'Tambah Backlink', 'listing' => null, 'users' => $users]); return; } if ($path === '/listings/create' && $method === 'POST') { verify_csrf(); require_auth(); $user = current_user(); $pdo = Database::connection(); if (!is_admin() && (int) $user['is_profile_locked'] === 1) { flash('error', 'Profil Anda sedang dikunci oleh Master Admin. Anda tidak dapat mengubah data saat ini. Silakan hubungi Master Admin untuk membuka profil Anda dan melakukan reset password jika diperlukan.'); redirect('listings'); } $ownerId = is_admin() ? (int) ($_POST['owner_user_id'] ?? $user['id']) : (int) $user['id']; $url = trim((string) ($_POST['url'] ?? '')); $anchorText = trim((string) ($_POST['anchor_text'] ?? '')); $status = ($_POST['status'] ?? 'active') === 'inactive' ? 'inactive' : 'active'; if ($url === '' || $anchorText === '') { flash('error', 'URL dan anchor wajib diisi.'); redirect('listings/create'); } validate_listing_input($ownerId, $url, $anchorText, 'listings/create'); $stmt = $pdo->prepare('INSERT INTO listings (owner_user_id, url, anchor_text, status, created_by, updated_by, created_at, updated_at) VALUES (:owner_user_id, :url, :anchor_text, :status, :created_by, :updated_by, NOW(), NOW())'); $stmt->execute([ ':owner_user_id' => $ownerId, ':url' => $url, ':anchor_text' => $anchorText, ':status' => $status, ':created_by' => $user['id'], ':updated_by' => $user['id'], ]); $listingId = (int) $pdo->lastInsertId(); Audit::log((int) $user['id'], 'listing_created', [ 'listing_id' => $listingId, 'target_user_id' => $ownerId, 'new_value' => compact('url', 'anchorText', 'status'), ]); finish_listing_change((int) $user['id'], 'listing_created', 'Backlink berhasil ditambahkan dan data.txt diperbarui.', 'listings'); } $params = []; if (match_route('/listings/{id}/edit', $path, $params) && $method === 'GET') { require_auth(); $listing = findListing((int) $params['id']); if (!$listing) { http_response_code(404); exit('Listing tidak ditemukan.'); } assertListingAccess($listing); $users = is_admin() ? fetchUsers() : []; view('listings/form', ['title' => 'Edit Backlink', 'listing' => $listing, 'users' => $users]); return; } $params = []; if (match_route('/listings/{id}/edit', $path, $params) && $method === 'POST') { verify_csrf(); require_auth(); $pdo = Database::connection(); $user = current_user(); $listing = findListing((int) $params['id']); if (!$listing) { http_response_code(404); exit('Listing tidak ditemukan.'); } assertListingAccess($listing); if (!is_admin() && (int) $user['is_profile_locked'] === 1) { flash('error', 'Profil Anda sedang dikunci oleh Master Admin. Anda tidak dapat mengubah data saat ini. Silakan hubungi Master Admin untuk membuka profil Anda dan melakukan reset password jika diperlukan.'); redirect('listings'); } $ownerId = is_admin() ? (int) ($_POST['owner_user_id'] ?? $listing['owner_user_id']) : (int) $listing['owner_user_id']; $url = trim((string) ($_POST['url'] ?? '')); $anchorText = trim((string) ($_POST['anchor_text'] ?? '')); $status = ($_POST['status'] ?? 'active') === 'inactive' ? 'inactive' : 'active'; if ($url === '' || $anchorText === '') { flash('error', 'URL dan anchor wajib diisi.'); redirect('listings/' . $listing['id'] . '/edit'); } validate_listing_input($ownerId, $url, $anchorText, 'listings/' . $listing['id'] . '/edit'); $stmt = $pdo->prepare('UPDATE listings SET owner_user_id = :owner_user_id, url = :url, anchor_text = :anchor_text, status = :status, updated_by = :updated_by, updated_at = NOW() WHERE id = :id'); $stmt->execute([ ':owner_user_id' => $ownerId, ':url' => $url, ':anchor_text' => $anchorText, ':status' => $status, ':updated_by' => $user['id'], ':id' => $listing['id'], ]); Audit::log((int) $user['id'], 'listing_updated', [ 'listing_id' => (int) $listing['id'], 'target_user_id' => $ownerId, 'old_value' => $listing, 'new_value' => compact('url', 'anchorText', 'status', 'ownerId'), ]); finish_listing_change((int) $user['id'], 'listing_updated', 'Backlink berhasil diperbarui dan data.txt disinkronkan.', 'listings'); } $params = []; if (match_route('/listings/{id}/delete', $path, $params) && $method === 'POST') { verify_csrf(); require_auth(); $pdo = Database::connection(); $user = current_user(); $listing = findListing((int) $params['id']); if (!$listing) { http_response_code(404); exit('Listing tidak ditemukan.'); } assertListingAccess($listing); if (!is_admin() && (int) $user['is_profile_locked'] === 1) { flash('error', 'Profil Anda sedang dikunci oleh Master Admin. Anda tidak dapat mengubah data saat ini. Silakan hubungi Master Admin untuk membuka profil Anda dan melakukan reset password jika diperlukan.'); redirect('listings'); } $stmt = $pdo->prepare('DELETE FROM listings WHERE id = :id'); $stmt->execute([':id' => $listing['id']]); Audit::log((int) $user['id'], 'listing_deleted', [ 'listing_id' => (int) $listing['id'], 'target_user_id' => (int) $listing['owner_user_id'], 'old_value' => $listing, ]); finish_listing_change((int) $user['id'], 'listing_deleted', 'Backlink berhasil dihapus dan data.txt disinkronkan.', 'listings'); } if ($path === '/listings/bulk-delete' && $method === 'POST') { verify_csrf(); require_auth(); $pdo = Database::connection(); $user = current_user(); $rawIds = $_POST['listing_ids'] ?? []; if (!is_array($rawIds)) { $rawIds = []; } $ids = []; foreach ($rawIds as $rawId) { $id = filter_var($rawId, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); if ($id !== false) { $ids[(int) $id] = (int) $id; } } $ids = array_values($ids); if (empty($ids)) { flash('error', 'Pilih minimal 1 backlink untuk dihapus.'); redirect('listings'); } if (count($ids) > 1000) { flash('error', 'Maksimal 1000 backlink dapat dihapus dalam satu aksi.'); redirect('listings'); } if (!is_admin() && (int) $user['is_profile_locked'] === 1) { flash('error', 'Profil Anda sedang dikunci oleh Master Admin. Anda tidak dapat mengubah data saat ini.'); redirect('listings'); } $placeholders = implode(',', array_fill(0, count($ids), '?')); $sql = 'SELECT id, owner_user_id, url, anchor_text, status FROM listings WHERE id IN (' . $placeholders . ')'; $params = $ids; if (!is_admin()) { $sql .= ' AND owner_user_id = ?'; $params[] = (int) $user['id']; } $stmt = $pdo->prepare($sql); $stmt->execute($params); $rows = $stmt->fetchAll(); if (empty($rows)) { flash('error', 'Tidak ada backlink terpilih yang dapat dihapus.'); redirect('listings'); } $allowedIds = array_map(static fn(array $row): int => (int) $row['id'], $rows); $deletePlaceholders = implode(',', array_fill(0, count($allowedIds), '?')); try { $pdo->beginTransaction(); $delete = $pdo->prepare('DELETE FROM listings WHERE id IN (' . $deletePlaceholders . ')'); $delete->execute($allowedIds); $ownerIds = array_values(array_unique(array_map(static fn(array $row): int => (int) $row['owner_user_id'], $rows))); Audit::log((int) $user['id'], 'listings_bulk_deleted', [ 'reason' => 'bulk_delete_selected', 'old_value' => [ 'count' => count($rows), 'listing_ids' => $allowedIds, 'owner_user_ids' => $ownerIds, 'rows' => $rows, ], 'new_value' => [ 'deleted_count' => count($rows), ], ]); $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } flash('error', 'Bulk delete gagal: ' . $e->getMessage()); redirect('listings'); } try { ListingExporter::regenerate((int) $user['id'], 'listings_bulk_deleted'); flash('success', count($rows) . ' backlink terpilih berhasil dihapus dan data.txt sudah disinkronkan.'); } catch (Throwable $e) { flash('error', count($rows) . ' backlink sudah terhapus dari database, tetapi data.txt gagal diperbarui: ' . $e->getMessage()); } redirect('listings'); } if ($path === '/users' && $method === 'GET') { require_admin(); view('users/index', ['title' => 'Users', 'users' => fetchUsers()]); return; } if ($path === '/users/create' && $method === 'GET') { require_admin(); view('users/form', ['title' => 'Tambah User', 'userRow' => null]); return; } if ($path === '/users/create' && $method === 'POST') { verify_csrf(); require_admin(); $pdo = Database::connection(); $actor = current_user(); $userId = trim((string) ($_POST['user_id'] ?? '')); $profileName = trim((string) ($_POST['profile_name'] ?? '')); $password = (string) ($_POST['password'] ?? ''); $role = ($_POST['role'] ?? 'user') === 'master_admin' ? 'master_admin' : 'user'; $status = ($_POST['status'] ?? 'active') === 'inactive' ? 'inactive' : 'active'; if ($userId === '' || $profileName === '' || strlen($password) < 8) { flash('error', 'User ID, Profile Name, dan password minimal 8 karakter wajib diisi.'); redirect('users/create'); } if (input_length($userId) > 50 || input_length($profileName) > 120) { flash('error', 'User ID maksimal 50 karakter dan Profile Name maksimal 120 karakter.'); redirect('users/create'); } if (user_id_exists($userId)) { flash('error', 'User ID sudah dipakai. Gunakan User ID lain.'); redirect('users/create'); } try { $pdo->beginTransaction(); $stmt = $pdo->prepare('INSERT INTO users (user_id, profile_name, password, role, status, must_change_password, is_profile_locked, is_account_locked, failed_login_attempts, created_at, updated_at) VALUES (:user_id, :profile_name, :password, :role, :status, 1, 0, 0, 0, NOW(), NOW())'); $stmt->execute([ ':user_id' => $userId, ':profile_name' => $profileName, ':password' => password_hash($password, PASSWORD_DEFAULT), ':role' => $role, ':status' => $status, ]); $targetId = (int) $pdo->lastInsertId(); Audit::log((int) $actor['id'], 'user_created', [ 'target_user_id' => $targetId, 'new_value' => compact('userId', 'profileName', 'role', 'status'), ]); $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } if ($e instanceof PDOException && (int) ($e->errorInfo[1] ?? 0) === 1062) { flash('error', 'User ID sudah dipakai. Gunakan User ID lain.'); } else { error_log('SERPLIV create user error: ' . $e->getMessage()); flash('error', 'User gagal dibuat. Periksa error log terbaru.'); } redirect('users/create'); } flash('success', 'User berhasil dibuat.'); redirect('users'); } $params = []; if (match_route('/users/{id}/edit', $path, $params) && $method === 'GET') { require_admin(); $userRow = findUser((int) $params['id']); if (!$userRow) { http_response_code(404); exit('User tidak ditemukan.'); } view('users/form', ['title' => 'Edit User', 'userRow' => $userRow]); return; } $params = []; if (match_route('/users/{id}/edit', $path, $params) && $method === 'POST') { verify_csrf(); require_admin(); $pdo = Database::connection(); $actor = current_user(); $userRow = findUser((int) $params['id']); if (!$userRow) { http_response_code(404); exit('User tidak ditemukan.'); } $profileName = trim((string) ($_POST['profile_name'] ?? '')); $status = ($_POST['status']?? 'active') === 'inactive' ? 'inactive' : 'active'; $role = ($_POST['role'] ?? 'user') === 'master_admin' ? 'master_admin' : 'user'; $mustChangePassword = isset($_POST['must_change_password']) ? 1 : 0; $newUserId = $userRow['user_id'] === 'admin' ? 'admin' : trim((string) ($_POST['user_id'] ?? $userRow['user_id'])); if ($profileName === '' || $newUserId === '') { flash('error', 'User ID dan Profile Name wajib diisi.'); redirect('users/' . $userRow['id'] . '/edit'); } if (input_length($newUserId) > 50 || input_length($profileName) > 120) { flash('error', 'User ID maksimal 50 karakter dan Profile Name maksimal 120 karakter.'); redirect('users/' . $userRow['id'] . '/edit'); } if (user_id_exists($newUserId, (int) $userRow['id'])) { flash('error', 'User ID sudah dipakai oleh akun lain.'); redirect('users/' . $userRow['id'] . '/edit'); } if ($userRow['user_id'] === 'admin' || (int) $userRow['id'] === (int) $actor['id']) { $role = 'master_admin'; $status = 'active'; } try { $pdo->beginTransaction(); $stmt = $pdo->prepare('UPDATE users SET user_id = :user_id, profile_name = :profile_name, role = :role, status = :status, must_change_password = :must_change_password, current_session_id = CASE WHEN :inactive = 1 THEN NULL ELSE current_session_id END, updated_at = NOW() WHERE id = :id'); $stmt->execute([ ':user_id' => $newUserId, ':profile_name' => $profileName, ':role' => $role, ':status' => $status, ':must_change_password' => $mustChangePassword, ':inactive' => $status === 'inactive' ? 1 : 0, ':id' => $userRow['id'], ]); Audit::log((int) $actor['id'], 'user_updated', [ 'target_user_id' => (int) $userRow['id'], 'old_value' => user_audit_snapshot($userRow), 'new_value' => compact('newUserId', 'profileName', 'role', 'status', 'mustChangePassword'), 'reason' => trim((string) ($_POST['reason'] ?? '')) ?: 'profile_edit_by_master', ]); $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } if ($e instanceof PDOException && (int) ($e->errorInfo[1] ?? 0) === 1062) { flash('error', 'User ID sudah dipakai oleh akun lain.'); } else { error_log('SERPLIV edit user error: ' . $e->getMessage()); flash('error', 'User gagal diperbarui. Periksa error log terbaru.'); } redirect('users/' . $userRow['id'] . '/edit'); } flash('success', 'User berhasil diperbarui.'); redirect('users'); } $params = []; if (match_route('/users/{id}/delete', $path, $params) && $method === 'POST') { verify_csrf(); require_admin(); $pdo = Database::connection(); $actor = current_user(); $targetId = (int) $params['id']; try { $pdo->beginTransaction(); $stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id LIMIT 1 FOR UPDATE'); $stmt->execute([':id' => $targetId]); $target = $stmt->fetch(); if (!$target || $target['user_id'] === 'admin' || $targetId === (int) $actor['id']) { $pdo->rollBack(); flash('error', !$target ? 'User tidak ditemukan.' : 'Akun admin utama dan akun yang sedang dipakai tidak dapat dihapus.'); redirect('users'); } $countStmt = $pdo->prepare('SELECT COUNT(*) FROM listings WHERE owner_user_id = :id'); $countStmt->execute([':id' => $targetId]); $listingCount = (int) $countStmt->fetchColumn(); Audit::log((int) $actor['id'], 'user_deleted', [ 'target_user_id' => $targetId, 'reason' => 'User ' . $target['user_id'] . ' dihapus oleh Master Admin', 'old_value' => user_audit_snapshot($target), 'new_value' => ['deleted_listings' => $listingCount], ]); // Foreign keys remove the user's listings and login histories; audit logs stay. $delete = $pdo->prepare('DELETE FROM users WHERE id = :id'); $delete->execute([':id' => $targetId]); $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } error_log('SERPLIV delete user error: ' . $e->getMessage()); flash('error', 'User gagal dihapus. Periksa error log terbaru.'); redirect('users'); } try { ListingExporter::regenerate((int) $actor['id'], 'user_deleted_' . $targetId); flash('success', 'User ' . $target['user_id'] . ' dan ' . $listingCount . ' backlink miliknya berhasil dihapus. data.txt sudah disinkronkan.'); } catch (Throwable $e) { error_log('SERPLIV delete user export error: ' . $e->getMessage()); flash('error', 'User sudah dihapus dari database, tetapi data.txt gagal diperbarui. Cek permission data.txt, lalu klik Regenerate di Dashboard.'); } redirect('users'); } $adminActionRoutes = [ '/users/{id}/reset-password' => 'reset_password', '/users/{id}/lock-account' => 'lock_account', '/users/{id}/unlock-account' => 'unlock_account', '/users/{id}/lock-profile' => 'lock_profile', '/users/{id}/unlock-profile' => 'unlock_profile', '/users/{id}/impersonate' => 'impersonate', ]; foreach ($adminActionRoutes as $pattern => $action) { $params = []; if (match_route($pattern, $path, $params) && $method === 'POST') { verify_csrf(); require_admin(); $pdo = Database::connection(); $actor = current_user(); $target = findUser((int) $params['id']); if (!$target) { http_response_code(404); exit('User tidak ditemukan.'); } $reason = require_sensitive_reason(); if ((int) $target['id'] === (int) $actor['id'] && in_array($action, ['lock_account', 'lock_profile', 'impersonate'], true)) { flash('error', 'Aksi ini tidak dapat dilakukan pada akun yang sedang dipakai.'); redirect('users'); } if ($action === 'reset_password') { $temporaryPassword = (string) ($_POST['temporary_password'] ?? ''); if (strlen($temporaryPassword) < 8) { flash('error', 'Password sementara minimal 8 karakter.'); redirect('users'); } $stmt = $pdo->prepare('UPDATE users SET password = :password, must_change_password = 1, failed_login_attempts = 0, is_account_locked = 0, lock_reason = NULL, locked_by = NULL, locked_at = NULL, current_session_id = NULL, updated_at = NOW() WHERE id = :id'); $stmt->execute([ ':password' => password_hash($temporaryPassword, PASSWORD_DEFAULT), ':id' => $target['id'], ]); Audit::log((int) $actor['id'], 'password_reset_by_master', [ 'target_user_id' => (int) $target['id'], 'reason' => $reason, ]); flash('success', 'Password user berhasil di-reset.'); } if ($action === 'lock_account') { if ($target['user_id'] === 'admin') { flash('error', 'Akun master tidak bisa dikunci.'); redirect('users'); } $stmt = $pdo->prepare("UPDATE users SET is_account_locked = 1, lock_reason = 'locked_by_master', locked_by = :locked_by, locked_at = NOW(), current_session_id = NULL, updated_at = NOW() WHERE id = :id"); $stmt->execute([':locked_by' => $actor['id'], ':id' => $target['id']]); Audit::log((int) $actor['id'], 'account_locked_by_master', [ 'target_user_id' => (int) $target['id'], 'reason' => $reason, ]); flash('success', 'Akun user berhasil dikunci.'); } if ($action === 'unlock_account') { $stmt = $pdo->prepare('UPDATE users SET is_account_locked = 0, failed_login_attempts = 0, lock_reason = NULL, locked_by = NULL, locked_at = NULL, must_change_password = 1, current_session_id = NULL, updated_at = NOW() WHERE id = :id'); $stmt->execute([':id' => $target['id']]); Audit::log((int) $actor['id'], 'account_unlocked_by_master', [ 'target_user_id' => (int) $target['id'], 'reason' => $reason, ]); flash('success', 'Akun user berhasil dibuka. User wajib ganti password saat login berikutnya.'); } if ($action === 'lock_profile') { if ($target['user_id'] === 'admin') { flash('error', 'Profil akun master tidak bisa dikunci.'); redirect('users'); } $stmt = $pdo->prepare('UPDATE users SET is_profile_locked = 1, updated_at = NOW() WHERE id = :id'); $stmt->execute([':id' => $target['id']]); Audit::log((int) $actor['id'], 'profile_locked_by_master', [ 'target_user_id' => (int) $target['id'], 'reason' => $reason, ]); flash('success', 'Profil user berhasil dikunci.'); } if ($action === 'unlock_profile') { $stmt = $pdo->prepare('UPDATE users SET is_profile_locked = 0, updated_at = NOW() WHERE id = :id'); $stmt->execute([':id' => $target['id']]); Audit::log((int) $actor['id'], 'profile_unlocked_by_master', [ 'target_user_id' => (int) $target['id'], 'reason' => $reason, ]); flash('success', 'Profil user berhasil dibuka.'); } if ($action === 'impersonate') { if ($target['role'] === 'master_admin') { flash('error', 'Tidak bisa impersonate akun master.'); redirect('users'); } $_SESSION['impersonator'] = $actor; $_SESSION['auth_user'] = $target; $_SESSION['last_activity_at'] = time(); Audit::log((int) $actor['id'], 'impersonate_started', [ 'target_user_id' => (int) $target['id'], 'reason' => $reason, ]); flash('success', 'Mode impersonate aktif.'); redirect('dashboard'); } redirect('users'); } } if ($path === '/stop-impersonation' && $method === 'POST') { verify_csrf(); require_auth(); $impersonator = $_SESSION['impersonator'] ?? null; if (!$impersonator) { redirect('dashboard'); } Audit::log((int) $impersonator['id'], 'impersonate_stopped', [ 'target_user_id' => (int) (current_user()['id'] ?? 0), ]); $_SESSION['auth_user'] = $impersonator; $_SESSION['impersonator'] = null; $_SESSION['last_activity_at'] = time(); flash('success', 'Kembali ke akun master.'); redirect('dashboard'); } if ($path === '/profile' && $method === 'GET') { require_auth(); view('profile', ['title' => 'Profil']); return; } if ($path === '/profile' && $method === 'POST') { verify_csrf(); require_auth(); $pdo = Database::connection(); $user = current_user(); if ((int) $user['is_profile_locked'] === 1 && !is_admin()) { flash('error', 'Profil Anda sedang dikunci oleh Master Admin. Anda tidak dapat mengubah data saat ini. Silakan hubungi Master Admin untuk membuka profil Anda dan melakukan reset password jika diperlukan.'); redirect('profile'); } $profileName = trim((string) ($_POST['profile_name'] ?? '')); if ($profileName === '') { flash('error', 'Profile name wajib diisi.'); redirect('profile'); } if (input_length($profileName) > 120) { flash('error', 'Profile Name maksimal 120 karakter.'); redirect('profile'); } $stmt = $pdo->prepare('UPDATE users SET profile_name = :profile_name, updated_at = NOW() WHERE id = :id'); $stmt->execute([ ':profile_name' => $profileName, ':id' => $user['id'], ]); $_SESSION['auth_user']['profile_name'] = $profileName; Audit::log((int) $user['id'], 'profile_updated', [ 'target_user_id' => (int) $user['id'], 'new_value' => ['profile_name' => $profileName], ]); flash('success', 'Profil berhasil diperbarui.'); redirect('profile'); } if ($path === '/logs' && $method === 'GET') { require_admin(); $pdo = Database::connection(); $logs = $pdo->query('SELECT a.*, u.user_id AS actor_user_id_text, t.user_id AS target_user_id_text FROM audit_logs a LEFT JOIN users u ON u.id = a.actor_user_id LEFT JOIN users t ON t.id = a.target_user_id ORDER BY a.id DESC LIMIT 200')->fetchAll(); view('logs/index', ['title' => 'Audit Logs', 'logs' => $logs]); return; } if ($path === '/system/regenerate-list' && $method === 'POST') { verify_csrf(); require_admin(); try { ListingExporter::regenerate((int) current_user()['id'], trim((string) ($_POST['reason'] ?? '')) ?: 'manual_regenerate'); flash('success', 'data.txt berhasil di-generate ulang.'); } catch (Throwable $e) { error_log('SERPLIV manual export error: ' . $e->getMessage()); flash('error', 'data.txt gagal diperbarui. Cek permission data.txt dan folder public_html.'); } redirect('dashboard'); } http_response_code(404); echo '404 Not Found';