Главная
О компании
История
Руководство
Цели и задачи
Кадровая политика
Информация
Новости
Плановые отключения
Текущие работы
Планы и программы
Отчёты
Дилерские услуги
Реализация ТМЦ
Аренда имущества
Результаты работы с обращениями
Потребителям
Тарифы
Тарифы на 2025 г.
Тарифы на 2026 г.
Услуги потребителям
Договоры
Образцы заявлений
Абонентский отдел
Абонентский отдел
Законодательные и нормативно-правовые акты
Общая информация
Часто задаваемые вопросы
Закупки
Контакты
Контакты
Вопросы и ответы
Главная
О компании
История
Руководство
Цели и задачи
Кадровая политика
Информация
Новости
Плановые отключения
Текущие работы
Планы и программы
Отчёты
Дилерские услуги
Реализация ТМЦ
Аренда имущества
Результаты работы с обращениями
Потребителям
Тарифы
Тарифы на 2025 г.
Тарифы на 2026 г.
Услуги потребителям
Договоры
Образцы заявлений
Абонентский отдел
Абонентский отдел
Законодательные и нормативно-правовые акты
Общая информация
Часто задаваемые вопросы
Закупки
Контакты
Контакты
Вопросы и ответы
Устранение порыва тепловой сети по ул. Котовского,151 «д» в г. Слободзея.
В связи с износом тепловых сетей в результате длительной эксплуатации,…
Информация о значениях среднесуточных температур наружного воздуха от 03.11.2026 г.
Согласно пункту 4 Главы 2. «Условия предоставления коммунальных услуг» Правил…
О начале отопительного периода 2026-2027 годов.
В связи с прогнозируемым понижением температуры наружного воздуха принято решение…
РЕКОНСТРУКЦИЯ СОЛЕХРАНИЛИЩА НА КОТЕЛЬНОЙ №№1,4 УЛ. ШЕВЧЕНКО,78 В Г. ТИРАСПОЛЬ
В связи с неудовлетворительным состоянием солевой ямы в результате длительной…
Устранение порыва магистральной тепловой сети в г. Григориополь по ул. Шевченко,1
МГУП «Тирастеплоэнерго» провело работы по устранению магистрального порыва тепловой сети…
Дилерские услуги
Договоры
Образцы заявлений
Тарифы на 2025 г.
Новости
03 ноября 2026 г.
Устранение порыва тепловой сети по ул. Котовского,151 «д» в г. Слободзея.
В связи с износом тепловых сетей в результате длительной эксплуатации, МГУП «Тирастеплоэнерго» провели работы по…
03 ноября 2026 г.
Информация о значениях среднесуточных температур наружного воздуха от 03.11.2026 г.
02 ноября 2026 г.
О начале отопительного периода 2026-2027 годов.
29 октября 2026 г.
РЕКОНСТРУКЦИЯ СОЛЕХРАНИЛИЩА НА КОТЕЛЬНОЙ №№1,4 УЛ. ШЕВЧЕНКО,78 В Г. ТИРАСПОЛЬ
28 октября 2026 г.
Устранение порыва магистральной тепловой сети в г. Григориополь по ул. Шевченко,1
Плановые отключения
11 ноября 2026 г.
Оперативная информация МГУП "Тирастеплоэнерго" по состоянию на 08.00 часов 11.11.2026
По состоянию на 11.11.2026 г. № п/п Адрес возникновения повреждения Причина отключения Срок устранения повреждения (предполагаемый срок устранения) Примечание г.…
10 ноября 2026 г.
Оперативная информация МГУП "Тирастеплоэнерго" по состоянию на 08.00 часов 10.11.2026
09 ноября 2026 г.
Оперативная информация МГУП "Тирастеплоэнерго" по состоянию на 08.00 часов 09.11.2026
06 ноября 2026 г.
Оперативная информация МГУП "Тирастеплоэнерго" по состоянию на 08.00 часов 06.11.2026
05 ноября 2026 г.
Оперативная информация МГУП "Тирастеплоэнерго" по состоянию на 08.00 часов 05.11.2026
Часто задаваемые вопросы
Почему человек должен платить за коммунальные услуги? Каким…
МГУП «Тирастеплоэнерго» вырабатывает и поставляет тепловую энергию на…
1. Какая температура горячей воды должна быть…
Кадровая политика
ОТПРАВИТЬ ОБРАЩЕНИЕ
ОТПРАВИТЬ ЗАЯВКУ НА СЛИВ
Видео
25 декабря 2024 г.
Празднование дня энергетика 2024
Популярные материалы
Информация о значениях среднесуточных температур наружного воздуха от 03.11.2026 г.
Согласно пункту 4 Главы 2.…
В Приднестровье с 21 октября начнут подавать тепло в учреждения с круглосуточным пребыванием граждан.
Соответствующий Приказ подписал Первый заместитель…
О начале отопительного периода 2026-2027 годов.
В связи с прогнозируемым понижением…
Конкурс "Признание".
За многолетний добросовестный труд, высокий…
Реконструкция ЦТП 15 «ОК» по ул. Краснодонская,36/1 в г. Тирасполь
В рамках реализации инвестиционной программы…
Отчеты
Отчет о порывах, выявленных при проведении гидравлических испытаний тепловых сетей за 2026 год.
2026 год г. Тирасполь магистральные 15 внутриквартальные 32…
$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';