fix: Admin consent logs - implement Export CSV, fix delete, switch to better-sqlite3

- Replace Astro DB with better-sqlite3 for reliable SQLite access
- Implement Export CSV feature in admin panel
- Fix delete consent function (make it global)
- Add better-sqlite3 dependency
This commit is contained in:
Kunthawat Greethong
2026-03-31 11:00:20 +07:00
parent ae4a897d11
commit cfd8bd196a
8 changed files with 501 additions and 45 deletions

View File

@@ -184,11 +184,68 @@ const ADMIN_PASSWORD = import.meta.env.ADMIN_PASSWORD || 'changeme';
});
document.getElementById('refresh-btn').addEventListener('click', loadConsentLogs);
document.getElementById('export-btn').addEventListener('click', () => alert('ฟีเจอร์ Export CSV กำลังจะพัฒนาเพิ่มเติม'));
document.getElementById('export-btn').addEventListener('click', exportToCSV);
document.getElementById('logout-btn').addEventListener('click', () => {
sessionStorage.removeItem('admin-logged-in');
showLogin();
});
});
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');
}
}
window.deleteConsent = async function(sessionId) {
if (!confirm('คุณแน่ใจหรือไม่ที่จะลบบันทึกนี้?')) return;
try {
const response = await fetch(`/api/consent/${sessionId}`, { method: 'DELETE' });
if (response.ok) {
alert('ลบบันทึกเรียบร้อยแล้ว');
loadConsentLogs();
} else {
alert('เกิดข้อผิดพลาดในการลบ');
}
} catch (error) {
alert('เกิดข้อผิดพลาดในการลบ');
}
};
</script>
</Layout>

View File

@@ -1,19 +1,30 @@
import type { APIRoute } from 'astro';
import Database from 'better-sqlite3';
import { join } from 'path';
export const prerender = false;
// DELETE /api/consent/:sessionId - Right to be forgotten
const DB_PATH = join(process.cwd(), 'data', 'consent.db');
function getDb() {
return new Database(DB_PATH);
}
export const DELETE: APIRoute = async ({ params }) => {
try {
const { sessionId } = params;
const sessionId = params.sessionId;
if (!sessionId) {
return new Response(
JSON.stringify({ error: 'Session ID required' }),
JSON.stringify({ error: 'Missing sessionId' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const db = getDb();
const stmt = db.prepare('DELETE FROM ConsentLog WHERE sessionId = ?');
stmt.run(sessionId);
db.close();
return new Response(
JSON.stringify({ success: true, message: 'Consent deleted' }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }

View File

@@ -1,23 +1,15 @@
import type { APIRoute } from 'astro';
import { db } from 'astro:db';
import { defineTable, column } from 'astro:db';
const ConsentLog = defineTable({
columns: {
id: column.number({ primaryKey: true }),
sessionId: column.text({ unique: true }),
timestamp: column.date(),
essential: column.boolean(),
analytics: column.boolean(),
marketing: column.boolean(),
policyVersion: column.text(),
ipHash: column.text(),
userAgent: column.text()
}
});
import Database from 'better-sqlite3';
import { join } from 'path';
export const prerender = false;
const DB_PATH = join(process.cwd(), 'data', 'consent.db');
function getDb() {
return new Database(DB_PATH);
}
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT = 10;
const RATE_WINDOW = 60000;
@@ -39,6 +31,16 @@ function checkRateLimit(ip: string): boolean {
return true;
}
async function hashIP(ip: string): Promise<string> {
try {
if (crypto.subtle) {
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(ip));
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('').substring(0, 16);
}
} catch {}
return `fallback-${Date.now()}`;
}
export const POST: APIRoute = async ({ request, clientAddress }) => {
const ip = clientAddress || 'unknown';
if (!checkRateLimit(ip)) {
@@ -59,26 +61,17 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
);
}
let ipHash = 'unknown';
try {
if (crypto.subtle) {
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(ip));
ipHash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('').substring(0, 16);
}
} catch {
ipHash = `fallback-${Date.now()}`;
}
const db = getDb();
const ipHash = await hashIP(ip);
const timestamp = new Date().toISOString();
await db.insert(ConsentLog as any).values({
sessionId,
timestamp: new Date(),
essential,
analytics: analytics ?? false,
marketing: marketing ?? false,
policyVersion,
ipHash,
userAgent: userAgent || 'unknown'
});
const stmt = db.prepare(`
INSERT INTO ConsentLog (sessionId, timestamp, essential, analytics, marketing, policyVersion, ipHash, userAgent)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(sessionId, timestamp, essential ? 1 : 0, analytics ? 1 : 0, marketing ? 1 : 0, policyVersion, ipHash, userAgent || 'unknown');
db.close();
return new Response(
JSON.stringify({ success: true, sessionId, message: 'Consent logged' }),
@@ -94,8 +87,28 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
};
export const GET: APIRoute = async () => {
return new Response(
JSON.stringify({ logs: [], message: 'DB integration in progress' }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
try {
const db = getDb();
const stmt = db.prepare('SELECT * FROM ConsentLog ORDER BY timestamp DESC LIMIT 100');
const logs = stmt.all();
db.close();
const formattedLogs = logs.map((log: any) => ({
...log,
essential: log.essential === 1,
analytics: log.analytics === 1,
marketing: log.marketing === 1
}));
return new Response(
JSON.stringify({ logs: formattedLogs, message: 'Success' }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('Error fetching logs:', error);
return new Response(
JSON.stringify({ logs: [], error: 'Failed to fetch logs' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
};