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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user