feat: Add PDPA compliance features to Astro project
- Cookie consent banner with Thai language - Consent logging API with SQLite database - Admin dashboard for viewing consent logs - PDPA-compliant privacy policy - Environment configuration template PDPA compliance as per website-creator skill specifications. Build: npm install, Astro project ready for Docker deployment.
This commit is contained in:
4
dealplustech-astro/.env.example
Normal file
4
dealplustech-astro/.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
PUBLIC_UMAMI_WEBSITE_ID=your-website-id-here
|
||||||
|
PUBLIC_UMAMI_DOMAIN=https://analytics.moreminimore.com
|
||||||
|
ADMIN_PASSWORD=changeme
|
||||||
|
ASTRO_DB_REMOTE_URL=file:./data/consent.db
|
||||||
10
dealplustech-astro/.gitignore
vendored
10
dealplustech-astro/.gitignore
vendored
@@ -1 +1,9 @@
|
|||||||
.node-version
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.astro/
|
||||||
|
data/*.db
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|||||||
227
dealplustech-astro/src/components/CookieConsentBanner.astro
Normal file
227
dealplustech-astro/src/components/CookieConsentBanner.astro
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
---
|
||||||
|
// Cookie Consent Banner Component
|
||||||
|
// Displays on first visit, allows users to accept/reject cookie categories
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="cookie-consent-banner"
|
||||||
|
class="fixed bottom-0 left-0 right-0 z-50 bg-white border-t-2 border-primary-600 shadow-2xl p-6 md:p-8 transform translate-y-full transition-transform duration-300 ease-in-out"
|
||||||
|
role="region"
|
||||||
|
aria-labelledby="consent-title"
|
||||||
|
aria-describedby="consent-description"
|
||||||
|
>
|
||||||
|
<div class="container mx-auto max-w-7xl">
|
||||||
|
<div class="flex flex-col lg:flex-row gap-6 items-start lg:items-center justify-between">
|
||||||
|
<!-- Consent Content -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<h2 id="consent-title" class="text-xl md:text-2xl font-bold text-secondary-900 mb-3">
|
||||||
|
เรายึดถือความเป็นส่วนตัวของคุณ
|
||||||
|
</h2>
|
||||||
|
<p id="consent-description" class="text-base md:text-lg text-secondary-700 leading-relaxed mb-4">
|
||||||
|
เราใช้คุกกี้เพื่อปรับปรุงประสบการณ์การใช้งาน วิเคราะห์การเข้าใช้งาน และแสดงเนื้อหาที่ตรงใจคุณ
|
||||||
|
คุณสามารถเลือกยอมรับหรือปฏิเสธคุกกี้ที่ไม่จำเป็นได้
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Cookie Categories -->
|
||||||
|
<div class="space-y-3 mt-4">
|
||||||
|
<!-- Essential Cookies (Always On) -->
|
||||||
|
<div class="flex items-center gap-3 bg-secondary-50 p-3 rounded-lg">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="consent-essential"
|
||||||
|
checked
|
||||||
|
disabled
|
||||||
|
class="w-5 h-5 accent-primary-600 rounded"
|
||||||
|
/>
|
||||||
|
<label for="consent-essential" class="flex-1 cursor-pointer">
|
||||||
|
<span class="font-semibold text-secondary-900">คุกกี้จำเป็น</span>
|
||||||
|
<span class="text-sm text-secondary-600 block">ใช้สำหรับการทำงานของเว็บไซต์ ไม่สามารถปิดได้</span>
|
||||||
|
</label>
|
||||||
|
<span class="text-xs px-2 py-1 bg-primary-600 text-white rounded">จำเป็น</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Analytics Cookies -->
|
||||||
|
<div class="flex items-center gap-3 bg-secondary-50 p-3 rounded-lg">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="consent-analytics"
|
||||||
|
class="w-5 h-5 accent-primary-600 rounded consent-checkbox"
|
||||||
|
/>
|
||||||
|
<label for="consent-analytics" class="flex-1 cursor-pointer">
|
||||||
|
<span class="font-semibold text-secondary-900">คุกกี้วิเคราะห์ข้อมูล</span>
|
||||||
|
<span class="text-sm text-secondary-600 block">ช่วยให้เราเข้าใจพฤติกรรมการใช้งาน</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Marketing Cookies -->
|
||||||
|
<div class="flex items-center gap-3 bg-secondary-50 p-3 rounded-lg">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="consent-marketing"
|
||||||
|
class="w-5 h-5 accent-primary-600 rounded consent-checkbox"
|
||||||
|
/>
|
||||||
|
<label for="consent-marketing" class="flex-1 cursor-pointer">
|
||||||
|
<span class="font-semibold text-secondary-900">คุกกี้การตลาด</span>
|
||||||
|
<span class="text-sm text-secondary-600 block">ใช้สำหรับแสดงโฆษณาที่เกี่ยวข้อง</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex flex-col sm:flex-row gap-3 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
id="consent-reject"
|
||||||
|
class="bg-secondary-800 hover:bg-secondary-900 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
|
||||||
|
>
|
||||||
|
ปฏิเสธทั้งหมด
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="consent-accept"
|
||||||
|
class="bg-primary-600 hover:bg-primary-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
|
||||||
|
>
|
||||||
|
ยอมรับทั้งหมด
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Privacy Policy Link -->
|
||||||
|
<div class="mt-6 pt-6 border-t border-secondary-200 text-center">
|
||||||
|
<p class="text-sm text-secondary-600">
|
||||||
|
การใช้งานคุกกี้ของเราเป็นไปตาม
|
||||||
|
<a href="/privacy-policy" class="text-primary-600 hover:underline font-medium">นโยบายความเป็นส่วนตัว</a>
|
||||||
|
และ
|
||||||
|
<a href="/terms-and-conditions" class="text-primary-600 hover:underline font-medium">ข้อกำหนดการใช้งาน</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
interface ConsentPreferences {
|
||||||
|
essential: boolean;
|
||||||
|
analytics: boolean;
|
||||||
|
marketing: boolean;
|
||||||
|
timestamp: string;
|
||||||
|
policyVersion: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const POLICY_VERSION = '1.0.0';
|
||||||
|
const CONSENT_STORAGE_KEY = 'consent-preferences';
|
||||||
|
const CONSENT_LOG_API = '/api/consent';
|
||||||
|
|
||||||
|
function generateSessionId(): string {
|
||||||
|
return 'ses_' + Math.random().toString(36).substring(2, 15) +
|
||||||
|
Math.random().toString(36).substring(2, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoredConsent(): ConsentPreferences | null {
|
||||||
|
const stored = localStorage.getItem(CONSENT_STORAGE_KEY);
|
||||||
|
return stored ? JSON.parse(stored) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConsent(consent: ConsentPreferences) {
|
||||||
|
localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(consent));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionId = sessionStorage.getItem('consent_session_id') || generateSessionId();
|
||||||
|
sessionStorage.setItem('consent_session_id', sessionId);
|
||||||
|
|
||||||
|
await fetch(CONSENT_LOG_API, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionId,
|
||||||
|
consent,
|
||||||
|
policyVersion: POLICY_VERSION,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to log consent:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBanner() {
|
||||||
|
const banner = document.getElementById('cookie-consent-banner')!;
|
||||||
|
banner.classList.remove('translate-y-full');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideBanner() {
|
||||||
|
const banner = document.getElementById('cookie-consent-banner')!;
|
||||||
|
banner.classList.add('translate-y-full');
|
||||||
|
}
|
||||||
|
|
||||||
|
function initConsent() {
|
||||||
|
const stored = getStoredConsent();
|
||||||
|
|
||||||
|
if (stored) {
|
||||||
|
const analyticsCheckbox = document.getElementById('consent-analytics') as HTMLInputElement;
|
||||||
|
const marketingCheckbox = document.getElementById('consent-marketing') as HTMLInputElement;
|
||||||
|
|
||||||
|
if (analyticsCheckbox) analyticsCheckbox.checked = stored.analytics;
|
||||||
|
if (marketingCheckbox) marketingCheckbox.checked = stored.marketing;
|
||||||
|
|
||||||
|
loadConsentedScripts(stored);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(showBanner, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConsentedScripts(consent: ConsentPreferences) {
|
||||||
|
if (consent.analytics && import.meta.env.PUBLIC_UMAMI_WEBSITE_ID) {
|
||||||
|
const umamiScript = document.createElement('script');
|
||||||
|
umamiScript.defer = true;
|
||||||
|
umamiScript.src = 'https://analytics.moreminimore.com/script.js';
|
||||||
|
umamiScript.setAttribute('data-website-id', import.meta.env.PUBLIC_UMAMI_WEBSITE_ID);
|
||||||
|
document.head.appendChild(umamiScript);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAccept() {
|
||||||
|
const analytics = (document.getElementById('consent-analytics') as HTMLInputElement)?.checked ?? false;
|
||||||
|
const marketing = (document.getElementById('consent-marketing') as HTMLInputElement)?.checked ?? false;
|
||||||
|
|
||||||
|
const consent: ConsentPreferences = {
|
||||||
|
essential: true,
|
||||||
|
analytics: analytics || true,
|
||||||
|
marketing: marketing || true,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
policyVersion: POLICY_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
saveConsent(consent);
|
||||||
|
loadConsentedScripts(consent);
|
||||||
|
hideBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReject() {
|
||||||
|
const consent: ConsentPreferences = {
|
||||||
|
essential: true,
|
||||||
|
analytics: false,
|
||||||
|
marketing: false,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
policyVersion: POLICY_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
saveConsent(consent);
|
||||||
|
hideBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initConsent();
|
||||||
|
|
||||||
|
document.getElementById('consent-accept')?.addEventListener('click', handleAccept);
|
||||||
|
document.getElementById('consent-reject')?.addEventListener('click', handleReject);
|
||||||
|
});
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
openConsentPreferences: () => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.openConsentPreferences = () => {
|
||||||
|
showBanner();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
309
dealplustech-astro/src/pages/admin/consent-logs.astro
Normal file
309
dealplustech-astro/src/pages/admin/consent-logs.astro
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
---
|
||||||
|
import { createClient } from '@libsql/client';
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
url: import.meta.env.ASTRO_DB_REMOTE_URL || 'file:./data/consent.db',
|
||||||
|
authToken: import.meta.env.ASTRO_DB_APP_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ADMIN_PASSWORD = import.meta.env.ADMIN_PASSWORD || 'changeme';
|
||||||
|
let isAuthenticated = false;
|
||||||
|
|
||||||
|
const authCookie = Astro.cookies.get('admin_auth')?.value;
|
||||||
|
if (authCookie === 'true') {
|
||||||
|
isAuthenticated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Astro.request.method === 'POST') {
|
||||||
|
const formData = await Astro.request.formData();
|
||||||
|
const action = formData.get('action');
|
||||||
|
|
||||||
|
if (action === 'login') {
|
||||||
|
const password = formData.get('password');
|
||||||
|
if (password === ADMIN_PASSWORD) {
|
||||||
|
Astro.cookies.set('admin_auth', 'true', {
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
secure: import.meta.env.PROD,
|
||||||
|
maxAge: 60 * 60 * 2
|
||||||
|
});
|
||||||
|
isAuthenticated = true;
|
||||||
|
}
|
||||||
|
} else if (action === 'logout') {
|
||||||
|
Astro.cookies.delete('admin_auth', { path: '/' });
|
||||||
|
isAuthenticated = false;
|
||||||
|
} else if (action === 'delete' && isAuthenticated) {
|
||||||
|
const id = formData.get('id');
|
||||||
|
if (id) {
|
||||||
|
await client.execute({
|
||||||
|
sql: 'DELETE FROM consent_logs WHERE id = ?',
|
||||||
|
args: [Number(id)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (action === 'delete-all' && isAuthenticated) {
|
||||||
|
await client.execute({
|
||||||
|
sql: 'DELETE FROM consent_logs',
|
||||||
|
args: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let consentLogs: any[] = [];
|
||||||
|
if (isAuthenticated) {
|
||||||
|
const result = await client.execute({
|
||||||
|
sql: 'SELECT * FROM consent_logs ORDER BY created_at DESC LIMIT 100',
|
||||||
|
args: [],
|
||||||
|
});
|
||||||
|
consentLogs = result.rows || [];
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="Consent Logs Admin" description="Admin dashboard for viewing consent logs">
|
||||||
|
<main class="min-h-screen bg-secondary-50 py-12">
|
||||||
|
<div class="container mx-auto px-4 max-w-7xl">
|
||||||
|
<div class="bg-white rounded-2xl shadow-lg p-6 md:p-8">
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl md:text-4xl font-bold text-secondary-900 mb-2">
|
||||||
|
Consent Logs Admin
|
||||||
|
</h1>
|
||||||
|
<p class="text-secondary-600">
|
||||||
|
View and manage user consent records (PDPA compliance)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isAuthenticated ? (
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="action" value="logout" />
|
||||||
|
<button type="submit" class="bg-secondary-800 hover:bg-secondary-900 text-white px-4 py-2 rounded-lg">
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isAuthenticated ? (
|
||||||
|
<div class="max-w-md mx-auto">
|
||||||
|
<div class="bg-secondary-50 rounded-xl p-6 border-2 border-secondary-200">
|
||||||
|
<h2 class="text-xl font-bold text-secondary-900 mb-4">Admin Login</h2>
|
||||||
|
<form method="POST" class="space-y-4">
|
||||||
|
<input type="hidden" name="action" value="login" />
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-semibold text-secondary-700 mb-2">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
class="w-full px-4 py-2 border-2 border-secondary-300 rounded-lg focus:outline-none focus:border-primary-600"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg w-full">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||||
|
<div class="bg-primary-50 rounded-xl p-6 border-2 border-primary-200">
|
||||||
|
<div class="text-sm font-semibold text-primary-700 mb-1">Total Consents</div>
|
||||||
|
<div class="text-3xl font-bold text-primary-900">{consentLogs.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-secondary-50 rounded-xl p-6 border-2 border-secondary-200">
|
||||||
|
<div class="text-sm font-semibold text-secondary-700 mb-1">Analytics Accepted</div>
|
||||||
|
<div class="text-3xl font-bold text-secondary-900">
|
||||||
|
{consentLogs.filter(l => l.analytics === 1).length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-accent-50 rounded-xl p-6 border-2 border-accent-200">
|
||||||
|
<div class="text-sm font-semibold text-accent-700 mb-1">Marketing Accepted</div>
|
||||||
|
<div class="text-3xl font-bold text-accent-900">
|
||||||
|
{consentLogs.filter(l => l.marketing === 1).length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-secondary-50 rounded-xl p-6 border-2 border-secondary-200">
|
||||||
|
<div class="text-sm font-semibold text-secondary-700 mb-1">Acceptance Rate</div>
|
||||||
|
<div class="text-3xl font-bold text-secondary-900">
|
||||||
|
{consentLogs.length > 0
|
||||||
|
? Math.round((consentLogs.filter(l => l.analytics === 1).length / consentLogs.length) * 100)
|
||||||
|
: 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 mb-6">
|
||||||
|
<form method="POST" id="delete-all-form" class="inline">
|
||||||
|
<input type="hidden" name="action" value="delete-all" />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
id="delete-all-btn"
|
||||||
|
class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-semibold"
|
||||||
|
>
|
||||||
|
Delete All Logs
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="export-btn"
|
||||||
|
class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg font-semibold"
|
||||||
|
>
|
||||||
|
Export to CSV
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-secondary-800 text-white">
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-semibold">ID</th>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-semibold">Session ID</th>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-semibold">Timestamp</th>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-semibold">Locale</th>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-semibold">Essential</th>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-semibold">Analytics</th>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-semibold">Marketing</th>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-semibold">Policy Version</th>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-semibold">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{consentLogs.map((log, index) => (
|
||||||
|
<tr class={index % 2 === 0 ? 'bg-white' : 'bg-secondary-50'}>
|
||||||
|
<td class="px-4 py-3 text-sm text-secondary-700">{log.id}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-secondary-700 font-mono">{log.session_id}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-secondary-700">{new Date(log.timestamp).toLocaleString('th-TH')}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-secondary-700">{log.locale}</td>
|
||||||
|
<td class="px-4 py-3 text-sm">
|
||||||
|
<span class="px-2 py-1 bg-primary-100 text-primary-800 rounded text-xs font-semibold">
|
||||||
|
{log.essential === 1 ? 'Yes' : 'No'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm">
|
||||||
|
{log.analytics === 1 ? (
|
||||||
|
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs font-semibold">Accepted</span>
|
||||||
|
) : (
|
||||||
|
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs font-semibold">Rejected</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm">
|
||||||
|
{log.marketing === 1 ? (
|
||||||
|
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs font-semibold">Accepted</span>
|
||||||
|
) : (
|
||||||
|
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs font-semibold">Rejected</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-secondary-700 font-mono">{log.policy_version}</td>
|
||||||
|
<td class="px-4 py-3 text-sm">
|
||||||
|
<form method="POST" class="inline delete-form">
|
||||||
|
<input type="hidden" name="action" value="delete" />
|
||||||
|
<input type="hidden" name="id" value={log.id} />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="delete-btn text-red-600 hover:text-red-800 hover:underline"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{consentLogs.length === 0 && (
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<p class="text-secondary-600 text-lg">No consent logs found</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<script id="logs-data" type="application/json">
|
||||||
|
{JSON.stringify(consentLogs)}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
interface ConsentLog {
|
||||||
|
id: number;
|
||||||
|
session_id: string;
|
||||||
|
timestamp: string;
|
||||||
|
locale: string;
|
||||||
|
essential: number;
|
||||||
|
analytics: number;
|
||||||
|
marketing: number;
|
||||||
|
policy_version: string;
|
||||||
|
ip_hash: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToCSV(logs: ConsentLog[]): string {
|
||||||
|
const headers = ['ID', 'Session ID', 'Timestamp', 'Locale', 'Essential', 'Analytics', 'Marketing', 'Policy Version', 'IP Hash', 'Created At'];
|
||||||
|
const rows = logs.map((log: ConsentLog) => [
|
||||||
|
log.id,
|
||||||
|
log.session_id,
|
||||||
|
log.timestamp,
|
||||||
|
log.locale,
|
||||||
|
log.essential === 1 ? 'Yes' : 'No',
|
||||||
|
log.analytics === 1 ? 'Yes' : 'No',
|
||||||
|
log.marketing === 1 ? 'Yes' : 'No',
|
||||||
|
log.policy_version,
|
||||||
|
log.ip_hash,
|
||||||
|
log.created_at,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [headers, ...rows].map(row => row.join(',')).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const deleteAllBtn = document.getElementById('delete-all-btn');
|
||||||
|
const deleteAllForm = document.getElementById('delete-all-form') as HTMLFormElement;
|
||||||
|
|
||||||
|
if (deleteAllBtn && deleteAllForm) {
|
||||||
|
deleteAllBtn.addEventListener('click', (e) => {
|
||||||
|
const confirmDelete = confirm('Delete all consent logs? This cannot be undone.');
|
||||||
|
if (!confirmDelete) {
|
||||||
|
e.preventDefault();
|
||||||
|
deleteAllForm.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportBtn = document.getElementById('export-btn');
|
||||||
|
const logsData = document.getElementById('logs-data');
|
||||||
|
const consentLogs: ConsentLog[] = logsData ? JSON.parse(logsData.textContent || '[]') : [];
|
||||||
|
|
||||||
|
if (exportBtn) {
|
||||||
|
exportBtn.addEventListener('click', () => {
|
||||||
|
const csv = convertToCSV(consentLogs);
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `consent-logs-${new Date().toISOString().split('T')[0]}.csv`;
|
||||||
|
a.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.delete-form').forEach((form) => {
|
||||||
|
const button = form.querySelector('.delete-btn');
|
||||||
|
if (button) {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
const confirmDelete = confirm('Delete this consent log?');
|
||||||
|
if (!confirmDelete) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
92
dealplustech-astro/src/pages/api/consent/index.ts
Normal file
92
dealplustech-astro/src/pages/api/consent/index.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { createClient } from '@libsql/client';
|
||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
url: import.meta.env.ASTRO_DB_REMOTE_URL || 'file:./data/consent.db',
|
||||||
|
authToken: import.meta.env.ASTRO_DB_APP_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.execute({
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE IF NOT EXISTS consent_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
session_id TEXT UNIQUE NOT NULL,
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
locale TEXT DEFAULT 'th',
|
||||||
|
essential INTEGER NOT NULL DEFAULT 1,
|
||||||
|
analytics INTEGER NOT NULL DEFAULT 0,
|
||||||
|
marketing INTEGER NOT NULL DEFAULT 0,
|
||||||
|
policy_version TEXT NOT NULL,
|
||||||
|
ip_hash TEXT,
|
||||||
|
user_agent TEXT,
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { sessionId, consent, policyVersion } = body;
|
||||||
|
|
||||||
|
if (!sessionId || !consent) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Missing required fields' }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ip = request.headers.get('x-forwarded-for') || 'unknown';
|
||||||
|
const ipHash = await hashIP(ip);
|
||||||
|
const userAgent = request.headers.get('user-agent') || 'unknown';
|
||||||
|
const acceptLanguage = request.headers.get('accept-language') || 'th';
|
||||||
|
const locale = acceptLanguage.startsWith('th') ? 'th' : 'en';
|
||||||
|
|
||||||
|
await client.execute({
|
||||||
|
sql: `
|
||||||
|
INSERT OR REPLACE INTO consent_logs (
|
||||||
|
session_id,
|
||||||
|
timestamp,
|
||||||
|
locale,
|
||||||
|
essential,
|
||||||
|
analytics,
|
||||||
|
marketing,
|
||||||
|
policy_version,
|
||||||
|
ip_hash,
|
||||||
|
user_agent
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`,
|
||||||
|
args: [
|
||||||
|
sessionId,
|
||||||
|
consent.timestamp,
|
||||||
|
locale,
|
||||||
|
consent.essential ? 1 : 0,
|
||||||
|
consent.analytics ? 1 : 0,
|
||||||
|
consent.marketing ? 1 : 0,
|
||||||
|
policyVersion,
|
||||||
|
ipHash,
|
||||||
|
userAgent,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Consent API error:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function hashIP(ip: string): Promise<string> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(ip);
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
return hashHex.substring(0, 16);
|
||||||
|
}
|
||||||
203
dealplustech-astro/src/pages/privacy-policy.astro
Normal file
203
dealplustech-astro/src/pages/privacy-policy.astro
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
|
||||||
|
const POLICY_VERSION = '1.0.0';
|
||||||
|
const LAST_UPDATED = '2026-03-10';
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="นโยบายความเป็นส่วนตัว"
|
||||||
|
description="นโยบายความเป็นส่วนตัวของดีล พลัส เทค ตาม พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล พ.ศ. 2562 (PDPA)"
|
||||||
|
>
|
||||||
|
<main class="min-h-screen py-12 bg-secondary-50">
|
||||||
|
<article class="container mx-auto px-4 max-w-4xl">
|
||||||
|
<div class="bg-white rounded-2xl shadow-lg p-6 md:p-12">
|
||||||
|
<header class="mb-12 pb-8 border-b-2 border-secondary-200">
|
||||||
|
<h1 class="text-4xl md:text-5xl font-bold text-secondary-900 mb-4">
|
||||||
|
นโยบายความเป็นส่วนตัว
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-secondary-600">
|
||||||
|
Privacy Policy (Personal Data Protection Policy)
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 text-sm text-secondary-500">
|
||||||
|
<p>Version: {POLICY_VERSION}</p>
|
||||||
|
<p>Last Updated: {new Date(LAST_UPDATED).toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
|
||||||
|
1. ข้อมูลของผู้ควบคุมข้อมูลส่วนบุคคล
|
||||||
|
</h2>
|
||||||
|
<div class="bg-secondary-50 rounded-xl p-6 border-l-4 border-primary-600">
|
||||||
|
<p class="text-secondary-700 mb-4">
|
||||||
|
<strong class="text-secondary-900">บริษัท ดีล พลัส เทค จำกัด</strong> เป็นผู้ควบคุมข้อมูลส่วนบุคคล ตาม พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล พ.ศ. 2562 (PDPA)
|
||||||
|
</p>
|
||||||
|
<ul class="space-y-2 text-secondary-700">
|
||||||
|
<li><strong>ที่อยู่:</strong> 9/70 ซอยนครลุง 17 แขวงบางไผ่ เขตบางแค กทม. 10160</li>
|
||||||
|
<li><strong>โทรศัพท์:</strong> 090-555-1415</li>
|
||||||
|
<li><strong>อีเมล:</strong> info@dealplustech.co.th</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
|
||||||
|
2. ประเภทของข้อมูลที่เก็บรวบรวม
|
||||||
|
</h2>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li class="flex items-start gap-3">
|
||||||
|
<span class="text-primary-600 mt-1">✓</span>
|
||||||
|
<div>
|
||||||
|
<strong class="text-secondary-900">ข้อมูลประจำตัว:</strong>
|
||||||
|
<span class="text-secondary-700"> ชื่อ, นามสกุล, ที่อยู่อีเมล, เบอร์โทรศัพท์</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-3">
|
||||||
|
<span class="text-primary-600 mt-1">✓</span>
|
||||||
|
<div>
|
||||||
|
<strong class="text-secondary-900">ข้อมูลการใช้งาน:</strong>
|
||||||
|
<span class="text-secondary-700"> IP Address, ข้อมูลเบราว์เซอร์, อุปกรณ์ที่ใช้</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-3">
|
||||||
|
<span class="text-primary-600 mt-1">✓</span>
|
||||||
|
<div>
|
||||||
|
<strong class="text-secondary-900">ข้อมูลคุกกี้:</strong>
|
||||||
|
<span class="text-secondary-700"> การตั้งค่าคุกกี้, Session ID</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
|
||||||
|
3. วัตถุประสงค์ในการประมวลผลข้อมูล
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="bg-secondary-50 p-4 rounded-lg">
|
||||||
|
<strong class="text-secondary-900 block mb-1">การให้บริการ</strong>
|
||||||
|
<span class="text-secondary-600 text-sm">ตอบสนองคำขอ, ให้บริการลูกค้า</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-secondary-50 p-4 rounded-lg">
|
||||||
|
<strong class="text-secondary-900 block mb-1">การติดต่อกลับ</strong>
|
||||||
|
<span class="text-secondary-600 text-sm">ตอบคำถาม, ให้ข้อมูลผลิตภัณฑ์</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-secondary-50 p-4 rounded-lg">
|
||||||
|
<strong class="text-secondary-900 block mb-1">การวิเคราะห์</strong>
|
||||||
|
<span class="text-secondary-600 text-sm">ปรับปรุงเว็บไซต์, ประสบการณ์ผู้ใช้</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-secondary-50 p-4 rounded-lg">
|
||||||
|
<strong class="text-secondary-900 block mb-1">ตามกฎหมาย</strong>
|
||||||
|
<span class="text-secondary-600 text-sm">ปฏิบัติตามข้อบังคับทางกฎหมาย</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
|
||||||
|
4. ฐานกฎหมายในการประมวลผลข้อมูล
|
||||||
|
</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="border-l-4 border-primary-600 pl-6">
|
||||||
|
<h3 class="font-bold text-secondary-900 mb-2">4.1 การยินยอม (Consent)</h3>
|
||||||
|
<p class="text-secondary-700">สำหรับการใช้คุกกี้ที่ไม่จำเป็น, การตลาด</p>
|
||||||
|
</div>
|
||||||
|
<div class="border-l-4 border-primary-600 pl-6">
|
||||||
|
<h3 class="font-bold text-secondary-900 mb-2">4.2 การ履行合同 (Contract)</h3>
|
||||||
|
<p class="text-secondary-700">เพื่อการให้บริการและดำเนินการตามคำขอของคุณ</p>
|
||||||
|
</div>
|
||||||
|
<div class="border-l-4 border-primary-600 pl-6">
|
||||||
|
<h3 class="font-bold text-secondary-900 mb-2">4.3 ข้อบังคับทางกฎหมาย (Legal Obligation)</h3>
|
||||||
|
<p class="text-secondary-700">เพื่อปฏิบัติตามกฎหมายและระเบียบที่เกี่ยวข้อง</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
|
||||||
|
5. ระยะเวลาการเก็บรักษาข้อมูล
|
||||||
|
</h2>
|
||||||
|
<div class="bg-primary-50 rounded-xl p-6 border-2 border-primary-200">
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li class="flex justify-between items-center">
|
||||||
|
<span class="text-secondary-700">ข้อมูลการใช้งานเว็บไซต์</span>
|
||||||
|
<span class="font-semibold text-primary-900">10 ปี</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex justify-between items-center">
|
||||||
|
<span class="text-secondary-700">บันทึกการยินยอมคุกกี้</span>
|
||||||
|
<span class="font-semibold text-primary-900">10 ปี</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex justify-between items-center">
|
||||||
|
<span class="text-secondary-700">ข้อมูลติดต่อลูกค้า</span>
|
||||||
|
<span class="font-semibold text-primary-900">5 ปี</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
|
||||||
|
6. สิทธิของเจ้าของข้อมูลส่วนบุคคล
|
||||||
|
</h2>
|
||||||
|
<p class="text-secondary-700 mb-4">
|
||||||
|
ภายใต้ PDPA คุณมีสิทธิดังนี้:
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="bg-secondary-50 p-4 rounded-lg">
|
||||||
|
<h3 class="font-bold text-secondary-900 mb-2">สิทธิขอเข้าถึง</h3>
|
||||||
|
<p class="text-sm text-secondary-600">ขอรับสำเนาข้อมูลส่วนบุคคล</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-secondary-50 p-4 rounded-lg">
|
||||||
|
<h3 class="font-bold text-secondary-900 mb-2">สิทธิขอแก้ไข</h3>
|
||||||
|
<p class="text-sm text-secondary-600">ขอให้แก้ไขข้อมูลที่ไม่ถูกต้อง</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-secondary-50 p-4 rounded-lg">
|
||||||
|
<h3 class="font-bold text-secondary-900 mb-2">สิทธิขอลบ</h3>
|
||||||
|
<p class="text-sm text-secondary-600">ขอให้ลบข้อมูลส่วนบุคคล</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-secondary-50 p-4 rounded-lg">
|
||||||
|
<h3 class="font-bold text-secondary-900 mb-2">สิทธิเพิกถอน</h3>
|
||||||
|
<p class="text-sm text-secondary-600">เพิกถอนความยินยอมเมื่อใดก็ได้</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
|
||||||
|
7. คุกกี้และเทคโนโลยีการติดตาม
|
||||||
|
</h2>
|
||||||
|
<ul class="space-y-4">
|
||||||
|
<li class="bg-secondary-50 p-4 rounded-lg border-l-4 border-primary-600">
|
||||||
|
<h3 class="font-bold text-secondary-900 mb-2">คุกกี้จำเป็น (Essential Cookies)</h3>
|
||||||
|
<p class="text-sm text-secondary-600">จำเป็นสำหรับการทำงานของเว็บไซต์ ไม่สามารถปิดได้</p>
|
||||||
|
</li>
|
||||||
|
<li class="bg-secondary-50 p-4 rounded-lg border-l-4 border-accent-500">
|
||||||
|
<h3 class="font-bold text-secondary-900 mb-2">คุกกี้วิเคราะห์ (Analytics Cookies)</h3>
|
||||||
|
<p class="text-sm text-secondary-600">帮助我们了解网站使用情况 (Umami Analytics)</p>
|
||||||
|
</li>
|
||||||
|
<li class="bg-secondary-50 p-4 rounded-lg border-l-4 border-secondary-400">
|
||||||
|
<h3 class="font-bold text-secondary-900 mb-2">คุกกี้การตลาด (Marketing Cookies)</h3>
|
||||||
|
<p class="text-sm text-secondary-600">ใช้สำหรับแสดงโฆษณาที่เกี่ยวข้อง</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl md:text-3xl font-bold text-secondary-900 mb-6">
|
||||||
|
8. การติดต่อ
|
||||||
|
</h2>
|
||||||
|
<div class="bg-primary-50 rounded-xl p-6 border-2 border-primary-200">
|
||||||
|
<p class="text-secondary-700 mb-4">
|
||||||
|
หากคุณมีคำถามหรือต้องการใช้สิทธิของคุณ กรุณาติดต่อ:
|
||||||
|
</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-secondary-900"><strong>อีเมล:</strong> info@dealplustech.co.th</p>
|
||||||
|
<p class="text-secondary-900"><strong>โทรศัพท์:</strong> 090-555-1415</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
Reference in New Issue
Block a user