39303-vm/index.php
2026-03-25 07:58:17 +00:00

464 lines
24 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
require_once __DIR__ . '/archive_bootstrap.php';
ensure_archive_schema();
if (isset($_GET['reset_otp'])) {
unset($_SESSION['pending_auth']);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = (string)($_POST['action'] ?? '');
if ($action === 'login_request') {
process_login_request();
header('Location: index.php');
exit;
}
if ($action === 'verify_otp') {
process_otp_verification();
header('Location: index.php');
exit;
}
if ($action === 'logout') {
if (verify_csrf($_POST['csrf_token'] ?? null)) {
logout_user();
} else {
set_flash('danger', 'Permintaan logout tidak valid.');
}
header('Location: index.php');
exit;
}
if ($action === 'upload_document' && is_authenticated()) {
create_document($_POST, $_FILES, current_user());
header('Location: index.php');
exit;
}
}
$meta = page_meta('Sistem Informasi Arsip Digital KBRI Harare', 'Sistem arsip digital ultra-aman untuk pengelolaan dokumen diplomatik, konsuler, dan internal KBRI Harare.');
$flashes = get_flashes();
$user = current_user();
$pendingAuth = $_SESSION['pending_auth'] ?? null;
$folderTree = archive_folder_tree();
$folderOptions = folder_options();
$categoryOptions = category_options();
$selectedFolder = isset($_GET['folder']) && in_array($_GET['folder'], $folderOptions, true) ? (string)$_GET['folder'] : ($folderOptions[0] ?? '');
$dashboardMetrics = is_authenticated() ? archive_dashboard_metrics() : ['total' => 0, 'pending' => 0, 'validated' => 0, 'today_uploads' => 0];
$recentDocuments = is_authenticated() ? get_documents(['limit' => 8]) : [];
$pendingDocuments = is_authenticated() ? get_documents(['status' => 'pending', 'limit' => 5]) : [];
$loginAudit = $_SESSION['login_audit'] ?? null;
?>
<!doctype html>
<html lang="id" data-bs-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= h($meta['title']) ?></title>
<?php if ($meta['description'] !== ''): ?>
<meta name="description" content="<?= h($meta['description']) ?>" />
<meta property="og:description" content="<?= h($meta['description']) ?>" />
<meta property="twitter:description" content="<?= h($meta['description']) ?>" />
<?php endif; ?>
<?php if ($meta['image'] !== ''): ?>
<meta property="og:image" content="<?= h($meta['image']) ?>" />
<meta property="twitter:image" content="<?= h($meta['image']) ?>" />
<?php endif; ?>
<meta property="og:title" content="<?= h($meta['title']) ?>" />
<meta property="twitter:title" content="<?= h($meta['title']) ?>" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= urlencode((string)filemtime(__DIR__ . '/assets/css/custom.css')) ?>">
</head>
<body class="archive-app <?= $user ? 'app-authenticated' : 'app-guest' ?>">
<div class="toast-container position-fixed top-0 end-0 p-3">
<?php foreach ($flashes as $flash): ?>
<div class="toast align-items-center text-bg-<?= h($flash['type']) ?> border-0 mb-2" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="4500">
<div class="d-flex">
<div class="toast-body"><?= h($flash['message']) ?></div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if (!$user): ?>
<main class="login-shell">
<section class="login-splash">
<div class="glass-city" aria-hidden="true">
<span class="tower tower-a"></span>
<span class="tower tower-b"></span>
<span class="tower tower-c"></span>
<span class="grid-line grid-a"></span>
<span class="grid-line grid-b"></span>
</div>
<div class="login-copy">
<span class="eyebrow">Sistem Informasi Arsip Digital</span>
<h1>KEDUTAAN BESAR REPUBLIK INDONESIA HARARE</h1>
<p class="lead">Selamat datang di Sistem Informasi Arsip Digital KBRI Harare.</p>
<p class="muted-copy">Mewujudkan tata kelola administrasi yang tertib, transparan, dan akuntabel untuk pelayanan publik prima.</p>
<div class="security-badges">
<span><i class="bi bi-shield-lock"></i> Bcrypt + CSRF</span>
<span><i class="bi bi-envelope-check"></i> MFA via Email OTP</span>
<span><i class="bi bi-journal-text"></i> Audit trail dokumen</span>
</div>
</div>
</section>
<section class="login-panel card border-0 shadow-sm">
<div class="card-body p-4 p-lg-5">
<div class="d-flex justify-content-between align-items-start gap-3 mb-4">
<div>
<p class="section-kicker mb-2">Akses aman staf internal</p>
<h2 class="h4 mb-1"><?= is_array($pendingAuth) ? 'Verifikasi MFA' : 'Masuk ke brankas digital' ?></h2>
<p class="text-secondary mb-0 small"><?= is_array($pendingAuth) ? 'Masukkan kode OTP yang berlaku 5 menit.' : 'Gunakan email kedinasan, password, dan verifikasi OTP.' ?></p>
</div>
<button class="btn btn-outline-secondary btn-sm theme-toggle" type="button" data-theme-toggle>
<i class="bi bi-moon-stars"></i>
</button>
</div>
<?php if (is_array($pendingAuth)): ?>
<div class="alert alert-secondary border mb-4" role="status">
Kode OTP dikirim ke <strong><?= h($pendingAuth['masked_email'] ?? '') ?></strong>.
<?php if (empty($pendingAuth['mail_sent'])): ?>
<span class="d-block small mt-2">Mode uji aktif karena SMTP belum dikonfigurasi.</span>
<?php endif; ?>
</div>
<form method="post" class="vstack gap-3">
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
<input type="hidden" name="action" value="verify_otp">
<div>
<label for="otp" class="form-label">Kode OTP</label>
<input id="otp" name="otp" inputmode="numeric" maxlength="6" class="form-control form-control-lg" placeholder="Masukkan 6 digit kode" required>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100">Verifikasi & Masuk</button>
<a href="index.php?reset_otp=1" class="btn btn-link text-decoration-none p-0">Kembali ke form login</a>
</form>
<?php else: ?>
<form method="post" class="vstack gap-3">
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
<input type="hidden" name="action" value="login_request">
<div>
<label for="email" class="form-label">Email</label>
<input id="email" name="email" type="email" class="form-control form-control-lg" placeholder="nama@kbriharare.go.id" required>
</div>
<div>
<label for="password" class="form-label">Password</label>
<input id="password" name="password" type="password" class="form-control form-control-lg" placeholder="Masukkan password" required>
</div>
<div>
<label for="mfa" class="form-label">MFA</label>
<input id="mfa" class="form-control" value="Email OTP" readonly aria-readonly="true">
</div>
<button type="submit" class="btn btn-primary btn-lg w-100">Lanjutkan ke OTP</button>
</form>
<?php endif; ?>
<div class="demo-credentials mt-4">
<p class="mb-2 fw-semibold small text-uppercase text-secondary">Akun demo internal</p>
<div class="small text-secondary">Super Admin: <code>superadmin@kbriharare.go.id</code></div>
<div class="small text-secondary">Staf: <code>staf.politik@kbriharare.go.id</code></div>
<div class="small text-secondary">Password: <code>Harare2026!</code></div>
</div>
</div>
</section>
</main>
<?php else: ?>
<div class="app-shell">
<aside class="sidebar-panel" id="sidebarPanel">
<div class="sidebar-top">
<div>
<p class="section-kicker mb-1">Navigasi arsip</p>
<h2 class="h6 mb-0">Pohon folder KBRI Harare</h2>
</div>
<button class="btn btn-outline-secondary btn-sm d-lg-none" type="button" data-sidebar-toggle>
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="folder-tree">
<?php foreach ($folderTree as $section): ?>
<details class="folder-group" open>
<summary>
<span><?= h($section['group']) ?></span>
<i class="bi bi-chevron-down"></i>
</summary>
<div class="folder-links">
<?php foreach ($section['items'] as $item): ?>
<div class="folder-item">
<button
type="button"
class="folder-select"
data-folder-select
data-folder="<?= h($item['folder']) ?>"
data-category="<?= h($item['label']) ?>"
>
<span class="folder-name"><?= h($item['label']) ?></span>
<small><?= h($item['hint']) ?></small>
</button>
<button type="button" class="btn btn-sm btn-outline-primary folder-add" data-folder-select data-folder="<?= h($item['folder']) ?>" data-category="<?= h($item['label']) ?>" aria-label="Tambah dokumen ke <?= h($item['folder']) ?>">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<?php endforeach; ?>
</div>
</details>
<?php endforeach; ?>
</div>
</aside>
<div class="app-main">
<header class="topbar">
<div class="d-flex align-items-center gap-3">
<button class="btn btn-outline-secondary d-lg-none" type="button" data-sidebar-toggle>
<i class="bi bi-list"></i>
</button>
<div>
<p class="section-kicker mb-1">Sistem Informasi Arsip Digital</p>
<h1 class="h4 mb-0">KBRI Harare Secure Vault</h1>
</div>
</div>
<div class="d-flex align-items-center gap-2 gap-lg-3">
<button class="btn btn-outline-secondary btn-sm theme-toggle" type="button" data-theme-toggle>
<i class="bi bi-moon-stars"></i>
</button>
<div class="dropdown">
<button class="btn profile-chip dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<span class="avatar"><?= h($user['avatar'] ?? 'U') ?></span>
<span class="text-start d-none d-md-inline-block">
<strong class="d-block"><?= h($user['name']) ?></strong>
<small class="text-secondary"><?= h($user['role_label'] ?? '') ?> · <?= h($user['department'] ?? '') ?></small>
</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
<li><span class="dropdown-item-text small text-secondary">Foto profil dapat diaktifkan pada iterasi berikutnya.</span></li>
<li><hr class="dropdown-divider"></li>
<li>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
<input type="hidden" name="action" value="logout">
<button type="submit" class="dropdown-item text-danger">Keluar</button>
</form>
</li>
</ul>
</div>
</div>
</header>
<main class="content-grid">
<section class="hero-panel card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex flex-column flex-xl-row justify-content-between gap-4">
<div>
<p class="section-kicker mb-2">Workflow MVP siap dipakai</p>
<h2 class="display-title">Unggah → Validasi → Pratinjau aman → Riwayat audit</h2>
<p class="text-secondary mb-4">Staf dapat menempatkan dokumen pada folder yang tepat, sementara Super Admin memvalidasi sebelum dokumen dapat dipratinjau, diunduh, atau dicetak.</p>
<div class="hero-actions d-flex flex-wrap gap-2">
<a href="#uploadCard" class="btn btn-primary"><i class="bi bi-cloud-arrow-up me-2"></i>Tambah Dokumen</a>
<a href="#documentTable" class="btn btn-outline-secondary"><i class="bi bi-journal-text me-2"></i>Lihat Arsip</a>
</div>
</div>
<div class="session-panel">
<div class="metric-box">
<span>Total arsip</span>
<strong><?= number_format($dashboardMetrics['total']) ?></strong>
</div>
<div class="metric-box">
<span>Menunggu validasi</span>
<strong><?= number_format($dashboardMetrics['pending']) ?></strong>
</div>
<div class="metric-box">
<span>Tervalidasi</span>
<strong><?= number_format($dashboardMetrics['validated']) ?></strong>
</div>
<div class="metric-box">
<span>Upload hari ini</span>
<strong><?= number_format($dashboardMetrics['today_uploads']) ?></strong>
</div>
</div>
</div>
</div>
</section>
<section class="card border-0 shadow-sm" id="uploadCard">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start gap-3 mb-4">
<div>
<p class="section-kicker mb-2">Input dokumen</p>
<h2 class="h5 mb-1">Tambah dokumen ke folder terpilih</h2>
<p class="text-secondary mb-0 small">Gunakan drag-and-drop, isi metadata inti, lalu sistem menyimpan arsip ke folder terkait secara otomatis.</p>
</div>
<span class="badge text-bg-secondary-subtle text-secondary-emphasis border">CSRF aktif</span>
</div>
<form method="post" enctype="multipart/form-data" class="row g-3" id="uploadForm">
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
<input type="hidden" name="action" value="upload_document">
<div class="col-12 col-xl-7">
<label class="form-label" for="title">Judul dokumen</label>
<input class="form-control" id="title" name="title" placeholder="Contoh: Nota Diplomatik Dukungan Kegiatan RI" required>
</div>
<div class="col-6 col-xl-2">
<label class="form-label" for="document_date">Tanggal</label>
<input class="form-control" id="document_date" name="document_date" type="date" value="<?= h(date('Y-m-d')) ?>" required>
</div>
<div class="col-6 col-xl-3">
<label class="form-label" for="category">Kategori</label>
<input class="form-control" id="category" name="category" list="categoryList" placeholder="Pilih / ketik kategori" required>
<datalist id="categoryList">
<?php foreach ($categoryOptions as $category): ?>
<option value="<?= h($category) ?>"></option>
<?php endforeach; ?>
</datalist>
</div>
<div class="col-12 col-lg-7">
<label class="form-label" for="folder_path">Folder tujuan</label>
<select class="form-select" id="folder_path" name="folder_path" required>
<?php foreach ($folderOptions as $folder): ?>
<option value="<?= h($folder) ?>" <?= $selectedFolder === $folder ? 'selected' : '' ?>><?= h($folder) ?></option>
<?php endforeach; ?>
</select>
<div class="form-text">Klik folder di sidebar untuk mengisi otomatis.</div>
</div>
<div class="col-12 col-lg-5">
<label class="form-label">Lampiran</label>
<div class="dropzone" data-dropzone>
<input class="visually-hidden" type="file" id="attachment" name="attachment" accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.mp4" required>
<div>
<i class="bi bi-file-earmark-arrow-up"></i>
<p class="mb-1 fw-semibold">Tarik file ke sini atau klik untuk memilih</p>
<small class="text-secondary" data-file-label>PDF, DOC, DOCX, JPG, PNG, MP4 · maksimum 10 MB</small>
</div>
</div>
</div>
<div class="col-12">
<label class="form-label" for="notes">Catatan</label>
<textarea class="form-control" id="notes" name="notes" rows="4" placeholder="Isi ringkasan, status, atau konteks diplomatik singkat."></textarea>
</div>
<div class="col-12 d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<div class="small text-secondary">Akses staf untuk pratinjau/unduh/cetak akan terbuka setelah validasi Super Admin.</div>
<button type="submit" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Simpan dokumen</button>
</div>
</form>
</div>
</section>
<section class="card border-0 shadow-sm compact-card">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div>
<p class="section-kicker mb-2">Kontrol akses</p>
<h2 class="h5 mb-1">Status sesi & keamanan</h2>
</div>
<span class="badge text-bg-dark"><?= h($user['role_label'] ?? '') ?></span>
</div>
<ul class="list-unstyled security-list mb-0">
<li><i class="bi bi-check2-circle"></i><span>Login terakhir: <?= h($loginAudit['timestamp'] ?? date('Y-m-d H:i:s')) ?></span></li>
<li><i class="bi bi-check2-circle"></i><span>MFA: <?= h($loginAudit['mfa_method'] ?? 'Email OTP') ?></span></li>
<li><i class="bi bi-check2-circle"></i><span>Hak akses: <?= is_super_admin() ? 'validasi, unggah, dan akses semua dokumen' : 'unggah dan akses dokumen setelah validasi' ?></span></li>
<li><i class="bi bi-check2-circle"></i><span>Proteksi aktif: prepared statements, CSRF token, dan rate limiting sesi.</span></li>
</ul>
</div>
</section>
<section class="card border-0 shadow-sm compact-card">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div>
<p class="section-kicker mb-2">Validasi dokumen</p>
<h2 class="h5 mb-1">Queue persetujuan</h2>
</div>
<span class="badge text-bg-warning"><?= number_format($dashboardMetrics['pending']) ?> pending</span>
</div>
<?php if ($pendingDocuments): ?>
<div class="queue-list">
<?php foreach ($pendingDocuments as $document): ?>
<a class="queue-item" href="document.php?id=<?= (int)$document['id'] ?>">
<div>
<strong><?= h($document['title']) ?></strong>
<span><?= h($document['folder_path']) ?></span>
</div>
<span class="status-pill pending">Menunggu</span>
</a>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-panel">
<i class="bi bi-inbox"></i>
<p class="mb-0">Belum ada dokumen yang menunggu validasi.</p>
</div>
<?php endif; ?>
</div>
</section>
<section class="card border-0 shadow-sm table-card" id="documentTable">
<div class="card-body p-4">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-3">
<div>
<p class="section-kicker mb-2">Daftar arsip</p>
<h2 class="h5 mb-1">Dokumen terbaru</h2>
<p class="text-secondary mb-0 small">Setiap arsip memiliki detail metadata, status validasi, dan jejak audit.</p>
</div>
<div class="search-box">
<i class="bi bi-search"></i>
<input type="search" class="form-control" id="documentSearch" placeholder="Cari judul, folder, atau pembuat">
</div>
</div>
<?php if ($recentDocuments): ?>
<div class="table-responsive">
<table class="table align-middle archive-table mb-0">
<thead>
<tr>
<th>Dokumen</th>
<th>Folder</th>
<th>Upload</th>
<th>Status</th>
<th class="text-end">Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentDocuments as $document): ?>
<tr data-search-row="<?= h(strtolower($document['title'] . ' ' . $document['folder_path'] . ' ' . $document['created_by'])) ?>">
<td>
<strong class="d-block"><?= h($document['title']) ?></strong>
<small class="text-secondary"><?= h($document['category']) ?> · <?= h(format_filesize((int)$document['attachment_size'])) ?></small>
</td>
<td><?= h($document['folder_path']) ?></td>
<td>
<span class="d-block"><?= h($document['created_by']) ?></span>
<small class="text-secondary"><?= h(date('d M Y H:i', strtotime((string)$document['created_at']))) ?></small>
</td>
<td><span class="status-pill <?= $document['status'] === 'validated' ? 'validated' : 'pending' ?>"><?= $document['status'] === 'validated' ? 'Tervalidasi' : 'Menunggu' ?></span></td>
<td class="text-end"><a class="btn btn-sm btn-outline-secondary" href="document.php?id=<?= (int)$document['id'] ?>">Buka detail</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-panel tall">
<i class="bi bi-folder2-open"></i>
<p class="mb-1 fw-semibold">Belum ada arsip.</p>
<p class="mb-0 text-secondary small">Mulai dengan memilih folder di sidebar lalu unggah dokumen pertama Anda.</p>
</div>
<?php endif; ?>
</div>
</section>
</main>
</div>
</div>
<?php endif; ?>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?= urlencode((string)filemtime(__DIR__ . '/assets/js/main.js')) ?>"></script>
</body>
</html>