314 lines
9.2 KiB
Plaintext
314 lines
9.2 KiB
Plaintext
---
|
|
// Password-protected admin page for viewing consent logs
|
|
import { db, ConsentLog, desc } from 'astro:db';
|
|
|
|
// Simple password protection (in production, use proper auth)
|
|
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 {
|
|
logs = await db.select().from(ConsentLog).orderBy(desc(ConsentLog.timestamp)).limit(100);
|
|
} catch (err) {
|
|
error = 'Failed to load consent logs. Make sure database is initialized.';
|
|
console.error(err);
|
|
}
|
|
} else {
|
|
error = 'Invalid password';
|
|
}
|
|
}
|
|
---
|
|
|
|
<html lang="en">
|
|
<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;
|
|
}
|
|
.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-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;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🔐 Consent Logs Admin Dashboard</h1>
|
|
|
|
{!isAuthenticated ? (
|
|
<div class="login-form">
|
|
<h2 class="text-xl font-bold mb-4">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 class="mt-4 text-sm text-gray-600">
|
|
Default password: <code>changeme</code> (change in .env)
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<div class="actions flex gap-4 mb-4">
|
|
<a href="/admin/consent-logs" class="btn btn-primary">Refresh</a>
|
|
<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>Locale</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="9" 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 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 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>
|
|
<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>
|
|
</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');
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|