Loading...
Skilled Instructors

Saaz Studio training institute is the hub of professional and personal development for all Student members of the institute.

Online Classes

Saaz Studio provide one faculty one Student online classes that will teach you everything from Graphic Design and 3d Animation to content creation and conflict resolution.

Home Projects

Saaz Studio provide 2000+ assignment work in home project

Book Library

Saaz Studio provide 100+ e-books Library and video tutorial.

About Us

Welcome to the Saaz Studio

Saaz Studio is pioneers in the field of Graphics Design, animation, Gaming and VFX. We are a complete Graphics Design and Animation institute empowered by world class technology and highly rated courses to make professionals out of you.

We have experienced faculty at the help of guiding you throughout the curriculum of courses. The courses are designed keeping in mind the latest technologies and propensities of Graphics Design, Animation, Gaming and VFX industry.

Read More
Course

Popular Course

Instructors

Expert Instructors

Saaz Bokare
Director
Shraddhansh Bokare
Chief Executive Officer
Payal P Pipare
Graphic Designer
Prajwal Bule
3D Artist
Partners

Our Partners

Student Video

Students Reviews

Placement

PLACEMENT AT SAAZ STUDIO

Testimonial

Students Testimonial

$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';