Files
opencode-skill/skills/thai-frontend-dev/scripts/templates/admin-consent-logs.astro
Kunthawat Greethong 5053ccdba2 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
2026-04-01 18:36:51 +07:00

383 lines
11 KiB
Plaintext

---
// Password-protected admin page for viewing consent logs
// Uses API instead of Astro DB (better-sqlite3) to bypass remote SQLite limitation
const ADMIN_PASSWORD = Astro.env.ADMIN_PASSWORD || 'changeme';
let logs = [];
let isAuthenticated = false;
let error = '';
if (Astro.request.method === 'POST') {
const formData = await Astro.request.formData();
const password = formData.get('password');
if (password === ADMIN_PASSWORD) {
isAuthenticated = true;
try {
const response = await fetch('/api/consent');
const data = await response.json();
logs = data.logs || [];
} catch (err) {
error = 'Failed to load consent logs. Make sure the API is running.';
console.error(err);
}
} else {
error = 'Invalid password';
}
}
---
<html lang="th">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Consent Logs Admin | PDPA Compliance</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f3f4f6;
padding: 2rem;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
font-size: 2rem;
font-weight: bold;
margin-bottom: 1.5rem;
color: #111827;
}
.login-form {
max-width: 400px;
background: white;
padding: 2rem;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
color: #374151;
}
input[type="password"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 1rem;
}
input[type="password"]:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37,99,235,0.1);
}
button {
width: 100%;
padding: 0.75rem 1.5rem;
background: #2563eb;
color: white;
border: none;
border-radius: 0.375rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #1d4ed8;
}
.error {
background: #fee2e2;
color: #dc2626;
padding: 0.75rem;
border-radius: 0.375rem;
margin-bottom: 1rem;
font-size: 0.875rem;
}
.success {
background: #dcfce7;
color: #16a34a;
padding: 0.75rem;
border-radius: 0.375rem;
margin-bottom: 1rem;
font-size: 0.875rem;
}
table {
width: 100%;
background: white;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
th {
background: #f9fafb;
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6b7280;
}
tr:hover {
background: #f9fafb;
}
.actions {
margin-bottom: 1rem;
display: flex;
gap: 1rem;
}
.btn {
display: inline-block;
padding: 0.5rem 1rem;
font-size: 0.875rem;
border-radius: 0.375rem;
text-decoration: none;
transition: background 0.2s;
}
.btn-primary {
background: #2563eb;
color: white;
}
.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;
border: none;
cursor: pointer;
}
.btn-danger:hover {
background: #b91c1c;
}
.badge {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
border-radius: 9999px;
font-weight: 500;
}
.badge-green {
background: #dcfce7;
color: #16a34a;
}
.badge-red {
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>
<div class="container">
<h1>🔐 Consent Logs Admin Dashboard</h1>
{!isAuthenticated ? (
<div class="login-form">
<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">
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
required
placeholder="Enter admin password"
/>
</div>
<button type="submit">Login</button>
</form>
<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">
<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>
{error && <div class="error">{error}</div>}
<div style="overflow-x: auto;">
<table>
<thead>
<tr>
<th>Date/Time</th>
<th>Session ID</th>
<th>Essential</th>
<th>Analytics</th>
<th>Marketing</th>
<th>Policy Ver</th>
<th>IP Hash</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{logs.length === 0 ? (
<tr>
<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('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>
</td>
<td>
{log.analytics ? (
<span class="badge badge-green">✓</span>
) : (
<span class="badge badge-red">✗</span>
)}
</td>
<td>
{log.marketing ? (
<span class="badge badge-green">✓</span>
) : (
<span class="badge badge-red">✗</span>
)}
</td>
<td>{log.policyVersion}</td>
<td style="font-family: monospace; font-size: 0.75rem;">{log.ipHash}</td>
<td>
<button
class="btn btn-danger"
onclick="deleteConsent('{log.sessionId}')"
style="padding: 0.25rem 0.5rem; font-size: 0.75rem;"
>
Delete
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
<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>IP addresses are hashed (SHA-256, first 16 chars) for privacy</li>
<li>Rate limiting: 10 requests/minute per IP</li>
</ul>
</div>
</div>
)}
</div>
<script>
async function deleteConsent(sessionId) {
if (!confirm('Delete this consent record? This action cannot be undone.')) {
return;
}
try {
const response = await fetch(`/api/consent/${sessionId}`, {
method: 'DELETE',
});
if (response.ok) {
alert('Consent record deleted successfully');
location.reload();
} else {
alert('Failed to delete consent record');
}
} catch (error) {
console.error('Delete error:', error);
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>