Implement full consent logging system with SQLite database

- Install better-sqlite3 and @astrojs/node adapter
- Update consent API to use SQLite database
- Add DELETE endpoint for consent logs
- Update admin consent-logs page with full UI (stats, table, export, delete)
- Add sessionId to consent tracking
- Admin password: Coolm@n1234mo

Note: Database stored at data/consent.db (gitignored)
This commit is contained in:
Kunthawat
2026-04-01 15:09:16 +07:00
parent 8cce63bba3
commit 41bf954d80
7 changed files with 996 additions and 71 deletions

View File

@@ -3,8 +3,9 @@ import BaseLayout from '@/layouts/BaseLayout.astro';
import Header from '@/components/common/Header.astro';
import Footer from '@/components/common/Footer.astro';
export const prerender = false;
const adminPassword = import.meta.env.ADMIN_PASSWORD || 'Coolm@n1234mo';
const submitted = Astro.request.method === 'POST';
const password = Astro.url.searchParams.get('password') || '';
const isAuthorized = password === adminPassword;
---
@@ -17,48 +18,64 @@ const isAuthorized = password === adminPassword;
<div class="container-custom">
{isAuthorized ? (
<div>
<h1 class="text-3xl font-bold text-secondary-900 mb-8">Consent Logs</h1>
<div class="card bg-white p-6 mb-8">
<h2 class="text-xl font-semibold mb-4">วิธีใช้งาน</h2>
<ul class="list-disc pl-6 text-secondary-700 space-y-2">
<li>หน้านี้แสดง log การยอมรับ cookie consent ที่ส่งมาจากผู้เยี่ยมชมเว็บไซต์</li>
<li>ข้อมูลถูกบันทึกใน log file ของ server</li>
<li>สำหรับดู log จริง ให้ SSH ไปที่ server แล้วดูที่ <code class="bg-gray-100 px-2 py-1 rounded">pm2 logs</code> หรือ <code class="bg-gray-100 px-2 py-1 rounded">docker logs</code></li>
</ul>
<div class="flex flex-wrap gap-4 mb-8">
<button id="refresh-btn" class="bg-primary text-white px-6 py-2 rounded-lg font-bold hover:bg-primary-600 transition">🔄 รีเฟรช</button>
<button id="export-btn" class="bg-green-500 text-white px-6 py-2 rounded-lg font-bold hover:bg-green-600 transition">📥 Export CSV</button>
<button id="logout-btn" class="bg-gray-500 text-white px-6 py-2 rounded-lg font-bold hover:bg-gray-600 transition">🚪 ออกจากระบบ</button>
</div>
<div class="card bg-white p-6">
<h2 class="text-xl font-semibold mb-4">ตัวอย่างข้อมูลที่บันทึก</h2>
<pre class="bg-gray-900 text-green-400 p-4 rounded-lg overflow-x-auto text-sm">
{`[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"essential": true,
"analytics": true,
"marketing": false,
"timestamp": 1709300000000,
"policyVersion": "1.0",
"ip": "127.0.0.1",
"userAgent": "Mozilla/5.0...",
"createdAt": "2024-03-01T12:00:00.000Z"
}
]`}</pre>
<div class="grid md:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-sm font-medium text-gray-600 mb-2">Total Consents</h3>
<p id="stat-total" class="text-3xl font-bold text-secondary-900">0</p>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-sm font-medium text-gray-600 mb-2">Accepted Analytics</h3>
<p id="stat-analytics" class="text-3xl font-bold text-green-600">0</p>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-sm font-medium text-gray-600 mb-2">Rejected Analytics</h3>
<p id="stat-rejected" class="text-3xl font-bold text-red-600">0</p>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-sm font-medium text-gray-600 mb-2">Acceptance Rate</h3>
<p id="stat-rate" class="text-3xl font-bold text-accent-500">0%</p>
</div>
</div>
<div class="mt-8 text-center">
<a href="/admin/consent-logs" class="btn-secondary">ออกจากระบบ</a>
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-bold text-secondary-900">บันทึกความยินยอม (100 ล่าสุด)</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">วันที่/เวลา</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Session ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Essential</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Analytics</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Marketing</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Policy Version</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody id="logs-table-body" class="bg-white divide-y divide-gray-200">
<tr><td colspan="7" class="px-6 py-4 text-center text-gray-500">กำลังโหลด...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
) : (
<div class="max-w-md mx-auto">
<div class="card bg-white p-8">
<h1 class="text-2xl font-bold text-secondary-900 mb-6 text-center">Admin Login</h1>
<div class="bg-white rounded-lg shadow-md p-8">
<h1 class="text-2xl font-bold text-secondary-900 mb-6 text-center">เข้าสู่ระบบ Admin</h1>
<form method="GET" action="/admin/consent-logs" class="space-y-4">
<div>
<label for="password" class="block text-sm font-medium text-secondary-700 mb-2">
Password
รหัสผ่าน
</label>
<input
type="password"
@@ -66,7 +83,7 @@ const isAuthorized = password === adminPassword;
name="password"
required
class="w-full px-4 py-3 border border-secondary-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Enter admin password"
placeholder="กรอกรหัสผ่าน"
/>
</div>
@@ -77,10 +94,6 @@ const isAuthorized = password === adminPassword;
เข้าสู่ระบบ
</button>
</form>
{submitted && (
<p class="mt-4 text-center text-red-600">รหัสผ่านไม่ถูกต้อง</p>
)}
</div>
</div>
)}
@@ -89,4 +102,122 @@ const isAuthorized = password === adminPassword;
</main>
<Footer slot="footer" />
<script>
async function loadConsentLogs() {
try {
const response = await fetch('/api/consent');
const data = await response.json();
const logs = data.logs || [];
const tbody = document.getElementById('logs-table-body');
if (logs.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="px-6 py-4 text-center text-gray-500">ยังไม่มีการบันทึกความยินยอม</td></tr>';
updateStats([]);
return;
}
tbody.innerHTML = logs.map(log => `
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm">${new Date(log.timestamp).toLocaleString('th-TH')}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono">${(log.sessionId || '').substring(0, 8)}...</td>
<td class="px-6 py-4 whitespace-nowrap text-sm"><span class="px-2 py-1 text-xs font-semibold rounded-full ${log.essential ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}">${log.essential ? '✓' : '✗'}</span></td>
<td class="px-6 py-4 whitespace-nowrap text-sm"><span class="px-2 py-1 text-xs font-semibold rounded-full ${log.analytics ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}">${log.analytics ? '✓' : '✗'}</span></td>
<td class="px-6 py-4 whitespace-nowrap text-sm"><span class="px-2 py-1 text-xs font-semibold rounded-full ${log.marketing ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}">${log.marketing ? '✓' : '✗'}</span></td>
<td class="px-6 py-4 whitespace-nowrap text-sm">${log.policyVersion || '-'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm"><button onclick="deleteConsent('${log.sessionId}')" class="text-red-600 hover:text-red-900 font-medium">ลบ</button></td>
</tr>
`).join('');
updateStats(logs);
} catch (error) {
console.error('Error loading logs:', error);
const tbody = document.getElementById('logs-table-body');
tbody.innerHTML = '<tr><td colspan="7" class="px-6 py-4 text-center text-red-500">เกิดข้อผิดพลาดในการโหลดข้อมูล</td></tr>';
}
}
function updateStats(logs) {
const total = logs.length;
const analytics = logs.filter(l => l.analytics).length;
const rejected = total - analytics;
const rate = total > 0 ? ((analytics / total) * 100).toFixed(1) : '0';
document.getElementById('stat-total').textContent = total.toString();
document.getElementById('stat-analytics').textContent = analytics.toString();
document.getElementById('stat-rejected').textContent = rejected.toString();
document.getElementById('stat-rate').textContent = rate + '%';
}
async function deleteConsent(sessionId) {
if (!confirm('คุณแน่ใจหรือไม่ที่จะลบบันทึกนี้?')) return;
try {
const response = await fetch('/api/consent/' + sessionId, { method: 'DELETE' });
if (response.ok) {
alert('ลบบันทึกเรียบร้อยแล้ว');
loadConsentLogs();
} else {
alert('เกิดข้อผิดพลาดในการลบ');
}
} catch (error) {
alert('เกิดข้อผิดพลาดในการลบ');
}
}
async function exportToCSV() {
try {
const response = await fetch('/api/consent');
const data = await response.json();
const logs = data.logs || [];
if (logs.length === 0) {
alert('ไม่มีข้อมูลให้ export');
return;
}
const headers = ['วันที่/เวลา', 'Session ID', 'Essential', 'Analytics', 'Marketing', 'Policy Version', 'IP Hash', 'User Agent'];
const csvRows = [headers.join(',')];
for (const log of logs) {
const row = [
new Date(log.timestamp).toLocaleString('th-TH'),
log.sessionId,
log.essential ? 'Yes' : 'No',
log.analytics ? 'Yes' : 'No',
log.marketing ? 'Yes' : 'No',
log.policyVersion || '',
log.ipHash || '',
(log.userAgent || '').replace(/,/g, ';')
];
csvRows.push(row.join(','));
}
const csvContent = '\ufeff' + csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'consent-logs-' + new Date().toISOString().split('T')[0] + '.csv';
link.click();
URL.revokeObjectURL(url);
} catch (error) {
console.error('Export error:', error);
alert('เกิดข้อผิดพลาดในการ export');
}
}
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('stat-total')) {
loadConsentLogs();
document.getElementById('refresh-btn').addEventListener('click', loadConsentLogs);
document.getElementById('export-btn').addEventListener('click', exportToCSV);
document.getElementById('logout-btn').addEventListener('click', () => {
window.location.href = '/admin/consent-logs';
});
}
});
(window as any).deleteConsent = deleteConsent;
</script>
</BaseLayout>