feat(thai-frontend-dev): update consent system to better-sqlite3

- Replace Astro DB with better-sqlite3 (bypasses remote SQLite limitation)
- Add consent API with auto-create table pattern
- Update admin dashboard to fetch from API instead of Astro DB
- Add IP hashing and rate limiting for GDPR compliance
- Add seo-utils-mcp-guide skill
- Update legal templates with business placeholders
This commit is contained in:
Kunthawat Greethong
2026-04-01 18:36:51 +07:00
parent e4d41e3ae5
commit 5053ccdba2
6 changed files with 539 additions and 499 deletions

View File

@@ -1,8 +1,7 @@
---
// Password-protected admin page for viewing consent logs
import { db, ConsentLog, desc } from 'astro:db';
// Uses API instead of Astro DB (better-sqlite3) to bypass remote SQLite limitation
// Simple password protection (in production, use proper auth)
const ADMIN_PASSWORD = Astro.env.ADMIN_PASSWORD || 'changeme';
let logs = [];
@@ -16,9 +15,11 @@ if (Astro.request.method === 'POST') {
if (password === ADMIN_PASSWORD) {
isAuthenticated = true;
try {
logs = await db.select().from(ConsentLog).orderBy(desc(ConsentLog.timestamp)).limit(100);
const response = await fetch('/api/consent');
const data = await response.json();
logs = data.logs || [];
} catch (err) {
error = 'Failed to load consent logs. Make sure database is initialized.';
error = 'Failed to load consent logs. Make sure the API is running.';
console.error(err);
}
} else {
@@ -27,7 +28,7 @@ if (Astro.request.method === 'POST') {
}
---
<html lang="en">
<html lang="th">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -134,6 +135,8 @@ if (Astro.request.method === 'POST') {
}
.actions {
margin-bottom: 1rem;
display: flex;
gap: 1rem;
}
.btn {
display: inline-block;
@@ -150,6 +153,15 @@ if (Astro.request.method === 'POST') {
.btn-primary:hover {
background: #1d4ed8;
}
.btn-success {
background: #16a34a;
color: white;
border: none;
cursor: pointer;
}
.btn-success:hover {
background: #15803d;
}
.btn-danger {
background: #dc2626;
color: white;
@@ -174,6 +186,24 @@ if (Astro.request.method === 'POST') {
background: #fee2e2;
color: #dc2626;
}
.info-box {
margin-top: 1rem;
padding: 1rem;
background: #fef3c7;
border-radius: 0.375rem;
}
.info-box h3 {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #92400e;
}
.info-box ul {
font-size: 0.75rem;
color: #92400e;
list-style: disc;
padding-left: 1.5rem;
}
</style>
</head>
<body>
@@ -182,7 +212,7 @@ if (Astro.request.method === 'POST') {
{!isAuthenticated ? (
<div class="login-form">
<h2 class="text-xl font-bold mb-4">Admin Login</h2>
<h2 style="font-size: 1.25rem; font-weight: bold; margin-bottom: 1rem;">Admin Login</h2>
{error && <div class="error">{error}</div>}
<form method="POST">
<div class="form-group">
@@ -197,14 +227,15 @@ if (Astro.request.method === 'POST') {
</div>
<button type="submit">Login</button>
</form>
<p class="mt-4 text-sm text-gray-600">
<p style="margin-top: 1rem; font-size: 0.875rem; color: #6b7280;">
Default password: <code>changeme</code> (change in .env)
</p>
</div>
) : (
<div>
<div class="actions flex gap-4 mb-4">
<div class="actions">
<a href="/admin/consent-logs" class="btn btn-primary">Refresh</a>
<button class="btn btn-success" onclick="exportCSV()">Export CSV</button>
<a href="/" class="btn" style="background: #6b7280; color: white;">← Back to Site</a>
</div>
@@ -215,7 +246,6 @@ if (Astro.request.method === 'POST') {
<thead>
<tr>
<th>Date/Time</th>
<th>Locale</th>
<th>Session ID</th>
<th>Essential</th>
<th>Analytics</th>
@@ -228,15 +258,14 @@ if (Astro.request.method === 'POST') {
<tbody>
{logs.length === 0 ? (
<tr>
<td colspan="9" style="text-align: center; padding: 2rem;">
<td colspan="8" style="text-align: center; padding: 2rem;">
No consent logs found. Make sure the website has received consent.
</td>
</tr>
) : (
logs.map((log) => (
<tr>
<td>{new Date(log.timestamp).toLocaleString('en-GB')}</td>
<td>{log.locale.toUpperCase()}</td>
<td>{new Date(log.timestamp).toLocaleString('th-TH')}</td>
<td style="font-family: monospace; font-size: 0.75rem;">{log.sessionId}</td>
<td>
<span class="badge badge-green">{log.essential ? 'Yes' : 'No'}</span>
@@ -273,13 +302,13 @@ if (Astro.request.method === 'POST') {
</table>
</div>
<div style="margin-top: 1rem; padding: 1rem; background: #fef3c7; border-radius: 0.375rem;">
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 0.5rem;">⚠️ Important Notes:</h3>
<ul style="font-size: 0.75rem; color: #92400e; list-style: disc; padding-left: 1.5rem;">
<li>Consent records must be retained for 10 years (PDPA requirement)</li>
<div class="info-box">
<h3>⚠️ Important Notes (PDPA Compliance):</h3>
<ul>
<li>Consent records must be retained for 10 years</li>
<li>Only delete records when user exercises "right to be forgotten"</li>
<li>Document all deletions for compliance audit</li>
<li>IP addresses are hashed for privacy protection</li>
<li>IP addresses are hashed (SHA-256, first 16 chars) for privacy</li>
<li>Rate limiting: 10 requests/minute per IP</li>
</ul>
</div>
</div>
@@ -308,6 +337,46 @@ if (Astro.request.method === 'POST') {
alert('Error deleting consent record');
}
}
async function exportCSV() {
try {
const response = await fetch('/api/consent');
const data = await response.json();
const logs = data.logs || [];
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(','));
}
// UTF-8 BOM for Excel compatibility
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('Failed to export CSV');
}
}
</script>
</body>
</html>