Files
D1041231009-WebGIS-PovertyMap/resources/views/admin/users.blade.php
T
GuavaPopper c1c1fae93c fix: escape user-controlled fields in admin/users innerHTML to prevent stored XSS
u.name and u.email are stored user input and were interpolated raw into
tbody.innerHTML. Added esc() helper and applied to name, email, and role
display string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 13:53:01 +07:00

448 lines
23 KiB
PHP

<!DOCTYPE html>
<html lang="id" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Manajemen Pengguna - WebGIS</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<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=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
<style>
* { font-family: 'Plus Jakarta Sans', sans-serif; }
body { font-weight: 500; background-color: #f8fafc; }
h1, h2, h3, h4, h5, h6, .btn, .card-title, .stat-value { font-weight: 700 !important; letter-spacing: -0.01em; }
</style>
</head>
<body class="min-h-screen">
<!-- ═══ NAVBAR ═══════════════════════════════════════════════════════════ -->
<div class="navbar bg-base-100 border-b border-base-200 shadow-sm sticky top-0 z-[1000]">
<div class="flex-1">
<a href="{{ url('/') }}" class="btn btn-ghost text-lg font-bold gap-2 text-primary">
<i class="fa-solid fa-house-chimney-crack"></i>
WebGIS Pemetaan Kemiskinan
</a>
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1 gap-1">
<li>
<a href="{{ url('/map') }}" class="font-medium">
<i class="fa-solid fa-map"></i> Peta Interaktif
</a>
</li>
<li>
<a href="{{ url('/data') }}" class="font-medium">
<i class="fa-solid fa-table-list"></i> Data Tabular
</a>
</li>
<li>
<a href="{{ url('/admin/users') }}" class="font-semibold bg-primary/10 text-primary rounded-lg">
<i class="fa-solid fa-users-gear"></i> Admin
</a>
</li>
</ul>
<!-- User dropdown -->
<div class="dropdown dropdown-end ml-2">
<div tabindex="0" role="button" class="btn btn-ghost gap-2">
<div class="avatar placeholder">
<div class="bg-primary text-primary-content rounded-full w-8">
<span class="text-xs font-bold">{{ substr(auth()->user()->name, 0, 1) }}</span>
</div>
</div>
<span class="hidden sm:inline text-sm font-semibold">{{ auth()->user()->name }}</span>
<i class="fa-solid fa-chevron-down text-xs opacity-50"></i>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-lg border border-base-200 mt-2">
<li class="menu-title">
<span class="text-xs text-base-content/50 capitalize">{{ str_replace('_', ' ', auth()->user()->getRoleNames()->first() ?? 'user') }}</span>
</li>
<li>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="text-error w-full text-left flex items-center gap-2">
<i class="fa-solid fa-right-from-bracket"></i>
Keluar
</button>
</form>
</li>
</ul>
</div>
</div>
</div>
<!-- ═══ MAIN CONTENT ════════════════════════════════════════════════════ -->
<main class="max-w-screen-xl mx-auto px-4 py-8">
<!-- Header -->
<div class="flex flex-wrap items-center justify-between gap-4 mb-8">
<div>
<h1 class="text-3xl font-bold flex items-center gap-3">
<i class="fa-solid fa-users-gear text-primary"></i>
Manajemen Pengguna
</h1>
<p class="text-base-content/50 mt-1">Kelola akun pengguna dan hak akses sistem.</p>
</div>
<button onclick="openCreateModal()" class="btn btn-primary shadow-lg gap-2">
<i class="fa-solid fa-user-plus"></i> Tambah Pengguna
</button>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8" id="statsGrid">
<!-- Diisi oleh JS -->
</div>
<!-- Table -->
<div class="bg-white rounded-2xl shadow-sm border border-base-200 overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr class="text-xs font-bold uppercase opacity-50">
<th>#</th>
<th>Nama</th>
<th>Email</th>
<th>Role</th>
<th class="text-center">Aksi</th>
</tr>
</thead>
<tbody id="bodyUsers"></tbody>
</table>
</div>
</main>
<!-- ═══ MODALS ═══════════════════════════════════════════════════════════ -->
<!-- Modal: Tambah Pengguna -->
<dialog id="modal_create_user" class="modal">
<div class="modal-box w-11/12 max-w-md">
<h3 class="font-bold text-lg mb-4">
<i class="fa-solid fa-user-plus mr-2 text-primary"></i>Tambah Pengguna Baru
</h3>
<div class="flex flex-col gap-3">
<div class="form-control">
<label class="label"><span class="label-text font-bold">Nama Lengkap *</span></label>
<input id="create_name" type="text" class="input input-bordered w-full" placeholder="Nama pengguna">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Email *</span></label>
<input id="create_email" type="email" class="input input-bordered w-full" placeholder="email@domain.com">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Password *</span></label>
<input id="create_password" type="password" class="input input-bordered w-full" placeholder="Min. 8 karakter">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Role *</span></label>
<select id="create_role" class="select select-bordered w-full">
<option value="administrator">Administrator</option>
<option value="pengurus_ibadah">Pengurus Ibadah</option>
<option value="petugas_pendataan" selected>Petugas Pendataan</option>
<option value="pemangku_kebijakan">Pemangku Kebijakan</option>
</select>
</div>
</div>
<div class="modal-action">
<button onclick="document.getElementById('modal_create_user').close()" class="btn btn-ghost">Batal</button>
<button onclick="saveCreateUser()" class="btn btn-primary">
<i class="fa-solid fa-floppy-disk mr-1"></i>Simpan
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<!-- Modal: Edit Pengguna -->
<dialog id="modal_edit_user" class="modal">
<div class="modal-box w-11/12 max-w-md">
<h3 class="font-bold text-lg mb-4">
<i class="fa-solid fa-user-pen mr-2 text-warning"></i>Edit Pengguna
</h3>
<input type="hidden" id="edit_user_id">
<div class="flex flex-col gap-3">
<div class="form-control">
<label class="label"><span class="label-text font-bold">Nama Lengkap *</span></label>
<input id="edit_name" type="text" class="input input-bordered w-full" placeholder="Nama pengguna">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Email *</span></label>
<input id="edit_email" type="email" class="input input-bordered w-full" placeholder="email@domain.com">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Role *</span></label>
<select id="edit_role" class="select select-bordered w-full">
<option value="administrator">Administrator</option>
<option value="pengurus_ibadah">Pengurus Ibadah</option>
<option value="petugas_pendataan">Petugas Pendataan</option>
<option value="pemangku_kebijakan">Pemangku Kebijakan</option>
</select>
</div>
</div>
<div class="modal-action">
<button onclick="document.getElementById('modal_edit_user').close()" class="btn btn-ghost">Batal</button>
<button onclick="saveEditUser()" class="btn btn-warning">
<i class="fa-solid fa-floppy-disk mr-1"></i>Simpan Perubahan
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<!-- Modal: Reset Password -->
<dialog id="modal_reset_password" class="modal">
<div class="modal-box w-11/12 max-w-md">
<h3 class="font-bold text-lg mb-4">
<i class="fa-solid fa-key mr-2 text-info"></i>Reset Password
</h3>
<input type="hidden" id="reset_user_id">
<p class="text-sm text-base-content/60 mb-4" id="reset_user_label"></p>
<div class="flex flex-col gap-3">
<div class="form-control">
<label class="label"><span class="label-text font-bold">Password Baru *</span></label>
<input id="reset_password" type="password" class="input input-bordered w-full" placeholder="Min. 8 karakter">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Konfirmasi Password *</span></label>
<input id="reset_password_confirmation" type="password" class="input input-bordered w-full" placeholder="Ulangi password baru">
</div>
</div>
<div class="modal-action">
<button onclick="document.getElementById('modal_reset_password').close()" class="btn btn-ghost">Batal</button>
<button onclick="saveResetPassword()" class="btn btn-info text-white">
<i class="fa-solid fa-key mr-1"></i>Reset Password
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<!-- Modal: Confirm Delete -->
<dialog id="modal_delete_user" class="modal">
<div class="modal-box max-w-sm text-center">
<div class="text-5xl mb-4 text-error"><i class="fa-solid fa-trash-can"></i></div>
<h3 class="font-bold text-xl mb-2">Hapus Pengguna?</h3>
<p class="text-sm text-base-content/60 mb-6" id="delete_user_label"></p>
<input type="hidden" id="delete_user_id">
<div class="flex gap-2 justify-center">
<button class="btn btn-ghost px-8" onclick="document.getElementById('modal_delete_user').close()">Batal</button>
<button class="btn btn-error px-8 text-white" onclick="confirmDeleteUser()">Ya, Hapus</button>
</div>
</div>
</dialog>
<!-- Toast Container -->
<div class="toast toast-end toast-bottom z-[9999]" id="toastBox"></div>
<!-- ═══ JAVASCRIPT ════════════════════════════════════════════════════════ -->
<script>
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
let usersData = [];
function esc(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
document.addEventListener('DOMContentLoaded', loadUsers);
async function loadUsers() {
try {
const res = await fetch('{{ url('/api/admin/users') }}', {
headers: { 'X-CSRF-TOKEN': csrfToken }
});
usersData = await res.json();
renderUsers();
} catch (e) {
showToast('Gagal memuat data pengguna', 'error');
}
}
const roleLabels = {
administrator: 'Administrator',
pengurus_ibadah: 'Pengurus Ibadah',
petugas_pendataan: 'Petugas Pendataan',
pemangku_kebijakan:'Pemangku Kebijakan',
};
const roleBadge = {
administrator: 'badge-error',
pengurus_ibadah: 'badge-success',
petugas_pendataan: 'badge-warning',
pemangku_kebijakan:'badge-info',
};
function renderUsers() {
// Stats
const counts = { administrator: 0, pengurus_ibadah: 0, petugas_pendataan: 0, pemangku_kebijakan: 0 };
usersData.forEach(u => { if (counts[u.role] !== undefined) counts[u.role]++; });
document.getElementById('statsGrid').innerHTML = Object.entries(roleLabels).map(([key, label]) => `
<div class="stat bg-white shadow rounded-2xl border border-base-200">
<div class="stat-figure text-3xl opacity-20"><i class="fa-solid fa-user"></i></div>
<div class="stat-title text-xs uppercase font-bold opacity-50">${label}</div>
<div class="stat-value text-2xl">${counts[key]}</div>
</div>
`).join('');
// Table
const tbody = document.getElementById('bodyUsers');
tbody.innerHTML = usersData.length ? usersData.map((u, i) => `
<tr>
<td class="text-xs font-bold opacity-30">${i + 1}</td>
<td><div class="font-bold">${esc(u.name)}</div></td>
<td class="text-sm opacity-70">${esc(u.email)}</td>
<td>
<span class="badge badge-sm ${roleBadge[u.role] || 'badge-neutral'} badge-outline capitalize">
${esc(roleLabels[u.role] || u.role)}
</span>
</td>
<td class="text-center">
<div class="flex gap-1 justify-center">
<button onclick="openEditModal(${u.id})" class="btn btn-xs btn-warning btn-outline" title="Edit">
<i class="fa-solid fa-pen"></i>
</button>
<button onclick="openResetModal(${u.id})" class="btn btn-xs btn-info btn-outline" title="Reset Password">
<i class="fa-solid fa-key"></i>
</button>
<button onclick="openDeleteModal(${u.id})" class="btn btn-xs btn-error btn-outline" title="Hapus">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</td>
</tr>
`).join('') : '<tr><td colspan="5" class="text-center py-10 opacity-30 italic">Belum ada pengguna</td></tr>';
}
// ── CREATE ──────────────────────────────────────────────────
function openCreateModal() {
document.getElementById('create_name').value = '';
document.getElementById('create_email').value = '';
document.getElementById('create_password').value = '';
document.getElementById('create_role').value = 'petugas_pendataan';
document.getElementById('modal_create_user').showModal();
}
async function saveCreateUser() {
const body = {
name: document.getElementById('create_name').value,
email: document.getElementById('create_email').value,
password: document.getElementById('create_password').value,
role: document.getElementById('create_role').value,
};
const res = await fetch('{{ url('/api/admin/users') }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.success) {
document.getElementById('modal_create_user').close();
showToast('Pengguna berhasil ditambahkan', 'success');
await loadUsers();
} else {
showToast(json.message || 'Gagal menambahkan pengguna', 'error');
}
}
// ── EDIT ────────────────────────────────────────────────────
function openEditModal(id) {
const u = usersData.find(u => u.id === id);
if (!u) return;
document.getElementById('edit_user_id').value = u.id;
document.getElementById('edit_name').value = u.name;
document.getElementById('edit_email').value = u.email;
document.getElementById('edit_role').value = u.role || 'petugas_pendataan';
document.getElementById('modal_edit_user').showModal();
}
async function saveEditUser() {
const id = document.getElementById('edit_user_id').value;
const body = {
name: document.getElementById('edit_name').value,
email: document.getElementById('edit_email').value,
role: document.getElementById('edit_role').value,
};
const res = await fetch(`{{ url('/api/admin/users') }}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.success) {
document.getElementById('modal_edit_user').close();
showToast('Data pengguna diperbarui', 'success');
await loadUsers();
} else {
showToast(json.message || 'Gagal memperbarui data', 'error');
}
}
// ── RESET PASSWORD ──────────────────────────────────────────
function openResetModal(id) {
const u = usersData.find(u => u.id === id);
if (!u) return;
document.getElementById('reset_user_id').value = u.id;
document.getElementById('reset_user_label').textContent = `Reset password untuk: ${u.name} (${u.email})`;
document.getElementById('reset_password').value = '';
document.getElementById('reset_password_confirmation').value = '';
document.getElementById('modal_reset_password').showModal();
}
async function saveResetPassword() {
const id = document.getElementById('reset_user_id').value;
const body = {
password: document.getElementById('reset_password').value,
password_confirmation: document.getElementById('reset_password_confirmation').value,
};
const res = await fetch(`{{ url('/api/admin/users') }}/${id}/password`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.success) {
document.getElementById('modal_reset_password').close();
showToast('Password berhasil direset', 'success');
} else {
showToast(json.message || 'Gagal reset password', 'error');
}
}
// ── DELETE ──────────────────────────────────────────────────
function openDeleteModal(id) {
const u = usersData.find(u => u.id === id);
if (!u) return;
document.getElementById('delete_user_id').value = u.id;
document.getElementById('delete_user_label').textContent = `Pengguna "${u.name}" (${u.email}) akan dihapus secara permanen.`;
document.getElementById('modal_delete_user').showModal();
}
async function confirmDeleteUser() {
const id = document.getElementById('delete_user_id').value;
const res = await fetch(`{{ url('/api/admin/users') }}/${id}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
});
const json = await res.json();
if (json.success) {
document.getElementById('modal_delete_user').close();
showToast('Pengguna dihapus', 'success');
await loadUsers();
} else {
showToast(json.message || 'Gagal menghapus pengguna', 'error');
}
}
// ── TOAST ──────────────────────────────────────────────────
function showToast(msg, type = 'info') {
const t = document.createElement('div');
const cls = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-error' : 'alert-info';
t.className = `alert ${cls} shadow-lg py-3 text-xs font-bold`;
t.innerHTML = `<span><i class="fa-solid fa-circle-info mr-2"></i>${msg}</span>`;
document.getElementById('toastBox').appendChild(t);
setTimeout(() => t.remove(), 3000);
}
</script>
</body>
</html>