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