Major updates: - Added 35+ new skills from awesome-opencode-skills and antigravity repos - Merged SEO skills into seo-master - Merged architecture skills into architecture - Merged security skills into security-auditor and security-coder - Merged testing skills into testing-master and testing-patterns - Merged pentesting skills into pentesting - Renamed website-creator to thai-frontend-dev - Replaced skill-creator with github version - Removed Chutes references (use MiniMax API instead) - Added install-openclaw-skills.sh for cross-platform installation - Updated .env.example with MiniMax API credentials
1285 lines
49 KiB
Python
1285 lines
49 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Website Refactoring Script - Update existing Astro websites to new PDPA-compliant standard
|
|
|
|
This script migrates existing Astro websites to the new standardized structure with:
|
|
- PDPA-compliant legal pages
|
|
- Cookie consent system
|
|
- Consent logging database
|
|
- i18n routing (Thai/English)
|
|
- Umami Analytics integration
|
|
|
|
Usage:
|
|
python3 refactor_existing_website.py \
|
|
--input "./existing-website" \
|
|
--output "./refactored-website" \
|
|
--languages "th,en"
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import shutil
|
|
import argparse
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Refactor existing Astro website to PDPA-compliant standard'
|
|
)
|
|
parser.add_argument('--input', '-i', required=True, help='Input directory (existing website)')
|
|
parser.add_argument('--output', '-o', required=True, help='Output directory (refactored website)')
|
|
parser.add_argument('--languages', default='th,en', help='Languages (comma-separated)')
|
|
parser.add_argument('--umami-id', default='', help='Umami Website ID')
|
|
parser.add_argument('--umami-domain', default='analytics.example.com', help='Umami domain')
|
|
parser.add_argument('--admin-password', default='changeme', help='Admin password')
|
|
parser.add_argument('--skip-backup', action='store_true', help='Skip backup creation')
|
|
|
|
args = parser.parse_args()
|
|
|
|
input_dir = Path(args.input)
|
|
output_dir = Path(args.output)
|
|
languages = [lang.strip() for lang in args.languages.split(',')]
|
|
|
|
if not input_dir.exists():
|
|
print(f"Error: Input directory '{input_dir}' does not exist")
|
|
sys.exit(1)
|
|
|
|
print(f"🔄 Refactoring website from: {input_dir}")
|
|
print(f"📁 Output directory: {output_dir}")
|
|
print(f"🌐 Languages: {languages}")
|
|
|
|
# Create backup
|
|
if not args.skip_backup:
|
|
create_backup(input_dir)
|
|
|
|
# Create new structure
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Migrate content
|
|
migrate_content(input_dir, output_dir, languages)
|
|
|
|
# Add new features
|
|
add_pdpa_features(output_dir, args, languages)
|
|
|
|
# Update configuration
|
|
update_configs(output_dir, args, languages)
|
|
|
|
print(f"\n✅ Refactoring complete!")
|
|
print(f"\n📁 Refactored website at: {output_dir}")
|
|
print("\n📋 Next steps:")
|
|
print(f" 1. cd {output_dir}")
|
|
print(" 2. Review changes")
|
|
print(" 3. npm install")
|
|
print(" 4. Update .env with your credentials")
|
|
print(" 5. npm run dev")
|
|
print("\n⚠️ Important:")
|
|
print(" - Review Privacy Policy content (update company info)")
|
|
print(" - Review Terms & Conditions (update service details)")
|
|
print(" - Change admin password in .env")
|
|
print(" - Test all features before deployment")
|
|
|
|
|
|
def create_backup(input_dir):
|
|
"""Create backup of existing website."""
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
backup_dir = input_dir.parent / f"{input_dir.name}_backup_{timestamp}"
|
|
|
|
print(f"💾 Creating backup: {backup_dir}")
|
|
shutil.copytree(input_dir, backup_dir)
|
|
print(f" ✓ Backup created")
|
|
|
|
|
|
def migrate_content(input_dir, output_dir, languages):
|
|
"""Migrate existing content to new structure."""
|
|
|
|
# Create new folder structure
|
|
create_new_structure(output_dir)
|
|
|
|
# Copy existing content
|
|
src_input = input_dir / 'src'
|
|
|
|
if src_input.exists():
|
|
# Copy components
|
|
components_input = src_input / 'components'
|
|
if components_input.exists():
|
|
print("📦 Migrating components...")
|
|
shutil.copytree(
|
|
components_input,
|
|
output_dir / 'src' / 'components' / 'migrated',
|
|
dirs_exist_ok=True
|
|
)
|
|
print(f" ✓ Components migrated")
|
|
|
|
# Copy layouts
|
|
layouts_input = src_input / 'layouts'
|
|
if layouts_input.exists():
|
|
print("📐 Migrating layouts...")
|
|
shutil.copytree(
|
|
layouts_input,
|
|
output_dir / 'src' / 'layouts' / 'migrated',
|
|
dirs_exist_ok=True
|
|
)
|
|
print(f" ✓ Layouts migrated")
|
|
|
|
# Copy content collections (blog, products)
|
|
content_input = src_input / 'content'
|
|
if content_input.exists():
|
|
print("📝 Migrating content collections...")
|
|
# Copy blog posts
|
|
blog_input = content_input / 'blog'
|
|
if blog_input.exists():
|
|
# Organize by language if possible
|
|
for lang in languages:
|
|
lang_dir = output_dir / 'src' / 'content' / 'blog' / f'({lang})'
|
|
lang_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Copy all posts to default language folder for now
|
|
default_lang = 'en' if 'en' in languages else languages[0]
|
|
default_dir = output_dir / 'src' / 'content' / 'blog' / f'({default_lang})'
|
|
|
|
for md_file in blog_input.glob('*.md'):
|
|
shutil.copy2(md_file, default_dir)
|
|
print(f" ✓ Copied: {md_file.name}")
|
|
|
|
# Copy static assets
|
|
public_input = input_dir / 'public'
|
|
if public_input.exists():
|
|
print("🖼️ Migrating static assets...")
|
|
shutil.copytree(
|
|
public_input,
|
|
output_dir / 'public',
|
|
dirs_exist_ok=True
|
|
)
|
|
print(f" ✓ Assets migrated")
|
|
|
|
# Copy images
|
|
for img_dir in src_input.glob('**/images'):
|
|
rel_path = img_dir.relative_to(src_input)
|
|
dest_dir = output_dir / 'src' / 'components' / rel_path
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
for img_file in img_dir.glob('*'):
|
|
if img_file.suffix.lower() in ['.jpg', '.jpeg', '.png', '.svg', '.webp']:
|
|
shutil.copy2(img_file, dest_dir)
|
|
|
|
print(f" ✓ Content migration complete")
|
|
|
|
|
|
def create_new_structure(output_dir):
|
|
"""Create the new standardized folder structure."""
|
|
|
|
dirs = [
|
|
output_dir / 'public' / 'images',
|
|
output_dir / 'src' / 'components' / 'common',
|
|
output_dir / 'src' / 'components' / 'consent',
|
|
output_dir / 'src' / 'components' / 'ui',
|
|
output_dir / 'src' / 'components' / 'migrated',
|
|
output_dir / 'src' / 'layouts',
|
|
output_dir / 'src' / 'layouts' / 'migrated',
|
|
output_dir / 'src' / 'pages',
|
|
output_dir / 'src' / 'pages' / 'api' / 'consent',
|
|
output_dir / 'src' / 'pages' / 'admin',
|
|
output_dir / 'src' / 'styles',
|
|
output_dir / 'src' / 'content' / 'blog',
|
|
output_dir / 'src' / 'lib',
|
|
output_dir / 'db',
|
|
]
|
|
|
|
for d in dirs:
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
|
|
print("📁 Created new folder structure")
|
|
|
|
|
|
def add_pdpa_features(output_dir, args, languages):
|
|
"""Add PDPA compliance features."""
|
|
|
|
default_locale = 'en' if 'en' in languages else languages[0]
|
|
|
|
# Create Privacy Policy pages
|
|
print("📄 Creating Privacy Policy pages...")
|
|
for lang in languages:
|
|
lang_dir = output_dir / 'src' / 'pages' / lang
|
|
lang_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
privacy_policy = get_privacy_policy_template(lang, args)
|
|
(lang_dir / 'privacy-policy.astro').write_text(privacy_policy)
|
|
|
|
print(" ✓ Privacy Policy created")
|
|
|
|
# Create Terms & Conditions pages
|
|
print("📄 Creating Terms & Conditions pages...")
|
|
for lang in languages:
|
|
lang_dir = output_dir / 'src' / 'pages' / lang
|
|
terms = get_terms_template(lang)
|
|
(lang_dir / 'terms-and-conditions.astro').write_text(terms)
|
|
|
|
print(" ✓ Terms & Conditions created")
|
|
|
|
# Create consent components
|
|
print("🍪 Creating cookie consent components...")
|
|
cookie_banner = get_cookie_banner_template()
|
|
(output_dir / 'src' / 'components' / 'consent' / 'CookieBanner.astro').write_text(cookie_banner)
|
|
print(" ✓ Cookie banner created")
|
|
|
|
# Create admin dashboard
|
|
print("🔐 Creating admin dashboard...")
|
|
admin_page = get_admin_dashboard_template()
|
|
admin_dir = output_dir / 'src' / 'pages' / 'admin'
|
|
admin_dir.mkdir(parents=True, exist_ok=True)
|
|
(admin_dir / 'consent-logs.astro').write_text(admin_page)
|
|
print(" ✓ Admin dashboard created")
|
|
|
|
# Create database schema
|
|
print("💾 Creating database schema...")
|
|
db_config = get_db_config_template()
|
|
(output_dir / 'db' / 'config.ts').write_text(db_config)
|
|
|
|
db_seed = get_db_seed_template()
|
|
(output_dir / 'db' / 'seed.ts').write_text(db_seed)
|
|
print(" ✓ Database schema created")
|
|
|
|
# Create API endpoints
|
|
print("🔌 Creating API endpoints...")
|
|
create_api_endpoints(output_dir)
|
|
print(" ✓ API endpoints created")
|
|
|
|
# Create i18n lib
|
|
print("🌐 Creating i18n utilities...")
|
|
i18n_lib = get_i18n_lib_template(languages)
|
|
(output_dir / 'src' / 'lib' / 'i18n.ts').write_text(i18n_lib)
|
|
print(" ✓ i18n utilities created")
|
|
|
|
|
|
def update_configs(output_dir, args, languages):
|
|
"""Update configuration files."""
|
|
|
|
default_locale = 'en' if 'en' in languages else languages[0]
|
|
locales_str = ', '.join([f"'{lang}'" for lang in languages])
|
|
|
|
# astro.config.mjs
|
|
print("⚙️ Updating astro.config.mjs...")
|
|
astro_config = f"""import {{ defineConfig }} from 'astro/config';
|
|
import tailwindcss from '@tailwindcss/vite';
|
|
import db from '@astrojs/db';
|
|
import sitemap from '@astrojs/sitemap';
|
|
|
|
export default defineConfig({{
|
|
site: 'https://example.com',
|
|
output: 'hybrid',
|
|
i18n: {{
|
|
locales: [{locales_str}],
|
|
defaultLocale: '{default_locale}',
|
|
routing: {{
|
|
prefixDefaultLocale: false,
|
|
fallbackType: 'rewrite',
|
|
}},
|
|
fallback: {{
|
|
th: 'en',
|
|
}},
|
|
}},
|
|
integrations: [
|
|
tailwindcss(),
|
|
db(),
|
|
sitemap({{
|
|
i18n: {{
|
|
defaultLocale: '{default_locale}',
|
|
}},
|
|
}}),
|
|
],
|
|
}});
|
|
"""
|
|
(output_dir / 'astro.config.mjs').write_text(astro_config)
|
|
print(" ✓ astro.config.mjs updated")
|
|
|
|
# package.json (add dependencies)
|
|
print("📦 Updating package.json...")
|
|
package_json_path = output_dir / 'package.json'
|
|
if package_json_path.exists():
|
|
import json
|
|
with open(package_json_path, 'r') as f:
|
|
package_json = json.load(f)
|
|
|
|
# Add new dependencies
|
|
new_deps = {
|
|
"@astrojs/db": "^0.14.0",
|
|
"@astrojs/sitemap": "^3.2.0",
|
|
"drizzle-orm": "^0.38.0",
|
|
"@libsql/client": "^0.14.0",
|
|
}
|
|
|
|
if 'dependencies' not in package_json:
|
|
package_json['dependencies'] = {}
|
|
|
|
package_json['dependencies'].update(new_deps)
|
|
|
|
# Add new scripts
|
|
package_json['scripts']['db:push'] = 'astro db push --remote'
|
|
package_json['scripts']['db:seed'] = 'astro db seed'
|
|
|
|
with open(package_json_path, 'w') as f:
|
|
json.dump(package_json, f, indent=2)
|
|
|
|
print(" ✓ package.json updated")
|
|
|
|
# Create .env.example
|
|
print("🔐 Creating .env.example...")
|
|
env_example = f"""# Umami Analytics
|
|
UMAMI_WEBSITE_ID={args.umami_id or 'your-website-id-here'}
|
|
UMAMI_DOMAIN={args.umami_domain}
|
|
|
|
# Admin
|
|
ADMIN_PASSWORD={args.admin_password}
|
|
|
|
# Database (optional)
|
|
# ASTRO_DB_REMOTE_URL=libsql://your-db.turso.io
|
|
# ASTRO_DB_APP_TOKEN=your-turso-token
|
|
|
|
# Site Configuration
|
|
SITE_URL=https://example.com
|
|
SITE_NAME="Website Name"
|
|
"""
|
|
(output_dir / '.env.example').write_text(env_example)
|
|
|
|
# Update .gitignore
|
|
gitignore = """node_modules
|
|
dist
|
|
.env
|
|
.astro
|
|
*.db
|
|
*.log
|
|
.DS_Store
|
|
"""
|
|
(output_dir / '.gitignore').write_text(gitignore)
|
|
print(" ✓ .env.example created")
|
|
|
|
# Create/update Dockerfile
|
|
print("🐳 Creating Dockerfile...")
|
|
dockerfile = """FROM node:20-alpine AS builder
|
|
WORKDIR /app
|
|
COPY package*.json ./
|
|
RUN npm install
|
|
COPY . .
|
|
RUN npm run build
|
|
|
|
FROM node:20-alpine
|
|
WORKDIR /app
|
|
COPY package*.json ./
|
|
RUN npm install --production
|
|
COPY --from=builder /app/dist ./dist
|
|
COPY --from=builder /app/db ./db
|
|
|
|
RUN apk add --no-cache sqlite-libs
|
|
|
|
EXPOSE 80
|
|
|
|
ENV NODE_ENV=production
|
|
ENV ASTRO_DB_REMOTE_URL=file:/app/data/consent.db
|
|
|
|
CMD ["sh", "-c", "mkdir -p /app/data && npx astro preview --host 0.0.0.0 --port 80"]
|
|
"""
|
|
(output_dir / 'Dockerfile').write_text(dockerfile)
|
|
print(" ✓ Dockerfile created")
|
|
|
|
# Create MIGRATION.md
|
|
print("📝 Creating migration guide...")
|
|
migration_guide = f"""# Migration Guide
|
|
|
|
## What Changed
|
|
|
|
This website has been refactored to include:
|
|
- ✅ PDPA-compliant Privacy Policy
|
|
- ✅ PDPA-compliant Terms & Conditions
|
|
- ✅ Cookie consent system
|
|
- ✅ Consent logging database
|
|
- ✅ i18n routing ({', '.join(languages)})
|
|
- ✅ Umami Analytics integration
|
|
- ✅ Admin dashboard
|
|
|
|
## New Features
|
|
|
|
### 1. Privacy Policy
|
|
Location: `/privacy-policy`, `/th/privacy-policy`
|
|
- All 14 PDPA Section 36 disclosures
|
|
- Version tracking
|
|
- Last updated date
|
|
|
|
### 2. Cookie Consent
|
|
- Appears on first visit
|
|
- Opt-in model (required by PDPA)
|
|
- Granular choices (essential/analytics/marketing)
|
|
- Consent logged to database
|
|
|
|
### 3. Admin Dashboard
|
|
URL: `/admin/consent-logs`
|
|
Password: {args.admin_password} (**CHANGE THIS!**)
|
|
|
|
Features:
|
|
- View all consent records
|
|
- Filter by date/locale
|
|
- Delete records (right to be forgotten)
|
|
|
|
### 4. Database
|
|
Schema: `db/config.ts`
|
|
- Stores consent logs
|
|
- SQLite file (development)
|
|
- Turso ready (production)
|
|
|
|
### 5. i18n Routing
|
|
- Default locale: {default_locale}
|
|
- URL structure: `/about` ({default_locale}), `/th/about` (Thai)
|
|
- Fallback: Thai → English
|
|
|
|
## Migration Steps
|
|
|
|
### 1. Review Changes
|
|
```bash
|
|
# Check new files
|
|
ls -la src/pages/
|
|
ls -la src/components/consent/
|
|
ls -la db/
|
|
```
|
|
|
|
### 2. Update Content
|
|
- [ ] Edit Privacy Policy (add your company info)
|
|
- [ ] Edit Terms & Conditions (add service details)
|
|
- [ ] Update .env with your credentials
|
|
- [ ] Change admin password
|
|
|
|
### 3. Test Features
|
|
```bash
|
|
npm install
|
|
npm run dev
|
|
# Open http://localhost:4321
|
|
```
|
|
|
|
Checklist:
|
|
- [ ] Language switcher works
|
|
- [ ] Cookie consent appears
|
|
- [ ] Admin dashboard accessible
|
|
- [ ] Consent logging works
|
|
|
|
### 4. Deploy
|
|
```bash
|
|
npm run build
|
|
docker build -t website:latest .
|
|
# Deploy to Easypanel
|
|
```
|
|
|
|
## Content Migration
|
|
|
|
### Blog Posts
|
|
Your existing blog posts have been copied to:
|
|
`src/content/blog/(en)/` (or your default language)
|
|
|
|
To add bilingual content:
|
|
1. Translate posts
|
|
2. Copy to `src/content/blog/(th)/`
|
|
3. Update frontmatter with `locale: 'th'`
|
|
|
|
### Components
|
|
Migrated components are in:
|
|
`src/components/migrated/`
|
|
|
|
You can continue using them or migrate to new structure.
|
|
|
|
## Rollback
|
|
|
|
If you need to rollback:
|
|
1. Use the backup created at: `{args.input}_backup_TIMESTAMP`
|
|
2. Restore from backup
|
|
3. Revert deployment
|
|
|
|
## Support
|
|
|
|
See documentation:
|
|
- `SKILL.md` - Complete skill workflow
|
|
- `SPECIFICATION.md` - Technical details
|
|
- `IMPLEMENTATION_SUMMARY.md` - Feature summary
|
|
"""
|
|
(output_dir / 'MIGRATION.md').write_text(migration_guide)
|
|
print(" ✓ Migration guide created")
|
|
|
|
|
|
def get_privacy_policy_template(lang, args):
|
|
"""Generate Privacy Policy template."""
|
|
|
|
if lang == 'th':
|
|
return f"""---
|
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
|
|
const lastUpdated = new Date().toLocaleDateString('th-TH', {{
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
}});
|
|
const version = '1.0.0';
|
|
---
|
|
|
|
<BaseLayout title="นโยบายความเป็นส่วนตัว | {args.umami_domain.split('.')[0] if args.umami_domain else 'Website'}">
|
|
<section class="py-20 bg-white">
|
|
<div class="container mx-auto px-4">
|
|
<h1 class="text-4xl md:text-5xl font-bold text-center mb-4 text-gray-900">
|
|
นโยบายความเป็นส่วนตัว
|
|
</h1>
|
|
<p class="text-center text-gray-600 mb-12">
|
|
ฉบับที่ {{version}} | อัปเดตล่าสุด: {{lastUpdated}}
|
|
</p>
|
|
|
|
<div class="max-w-4xl mx-auto prose prose-lg">
|
|
<h2 class="text-2xl font-bold mb-4">1. ผู้ควบคุมข้อมูล</h2>
|
|
<p class="mb-6">
|
|
<strong>{args.umami_domain.split('.')[0] if args.umami_domain else 'Website'}</strong> เป็นผู้ควบคุมข้อมูลส่วนบุคคลของคุณ
|
|
<br />
|
|
ติดต่อ: [ข้อมูลการติดต่อของคุณ]
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">2. ข้อมูลที่เก็บรวบรวม</h2>
|
|
<p class="mb-6">เราเก็บรวบรวมข้อมูลส่วนบุคคลประเภทต่อไปนี้:</p>
|
|
<ul class="mb-6">
|
|
<li>ข้อมูลการติดต่อ (ชื่อ, อีเมล, เบอร์โทรศัพท์) เมื่อคุณติดต่อเรา</li>
|
|
<li>ข้อมูลการใช้งาน (จำนวนการดูหน้า, ระยะเวลาเซสชัน) ผ่านการวิเคราะห์</li>
|
|
<li>การตั้งค่าคุกกี้และบันทึกการยินยอม</li>
|
|
<li>ข้อมูลทางเทคนิค (ที่อยู่ IP, ประเภทเบราว์เซอร์, ข้อมูลอุปกรณ์)</li>
|
|
</ul>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">3. วัตถุประสงค์ในการประมวลผล</h2>
|
|
<p class="mb-6">เราประมวลผลข้อมูลของคุณเพื่อวัตถุประสงค์ดังนี้:</p>
|
|
<ul class="mb-6">
|
|
<li>เพื่อให้บริการและบำรุงรักษาบริการของเรา</li>
|
|
<li>เพื่อสื่อสารกับคุณเกี่ยวกับบริการของเรา</li>
|
|
<li>เพื่อปรับปรุงเว็บไซต์ผ่านการวิเคราะห์ (ด้วยความยินยอมของคุณ)</li>
|
|
<li>เพื่อปฏิบัติตามข้อผูกพันทางกฎหมาย</li>
|
|
</ul>
|
|
<p class="mb-6">
|
|
<strong>ฐานทางกฎหมาย:</strong> ความยินยอม ผลประโยชน์โดยชอบด้วยกฎหมาย ความจำเป็นตามสัญญา
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">4. ระยะเวลาเก็บรักษาข้อมูล</h2>
|
|
<p class="mb-6">
|
|
เราเก็บรักษาข้อมูลส่วนบุคคลตราบเท่าที่จำเป็นเพื่อวัตถุประสงค์ที่ระบุในนโยบายนี้
|
|
หรือตามที่กฎหมายกำหนด บันทึกการยินยอมถูกเก็บรักษาไว้ 10 ปีเพื่อปฏิบัติตามข้อกำหนด PDPA
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">5. การเปิดเผยข้อมูล</h2>
|
|
<p class="mb-6">เราไม่ขายหรือให้เช่าข้อมูลส่วนบุคคลของคุณ เราอาจแบ่งปันข้อมูลกับ:</p>
|
|
<ul class="mb-6">
|
|
<li>ผู้ให้บริการที่ประมวลผลข้อมูลในนามของเรา</li>
|
|
<li>หน่วยงานทางกฎหมายเมื่อ-required by law</li>
|
|
<li>ผู้ให้บริการการวิเคราะห์ (เฉพาะเมื่อคุณยินยอม)</li>
|
|
</ul>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">6. คุกกี้และการติดตาม</h2>
|
|
<p class="mb-6">
|
|
เราใช้คุกกี้และเทคโนโลยีที่คล้ายคลึงกัน คุกกี้ที่จำเป็นจะทำงานเสมอ
|
|
คุกกี้การวิเคราะห์และการตลาดต้องการความยินยอมที่ชัดเจนจากคุณ
|
|
คุณสามารถจัดการการตั้งค่าได้ผ่านแบนเนอร์การยินยอมคุกกี้ของเรา
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">7. สิทธิ์ของคุณ (PDPA)</h2>
|
|
<p class="mb-6">คุณมีสิทธิ์ดังต่อไปนี้ภายใต้ PDPA:</p>
|
|
<ul class="mb-6">
|
|
<li>สิทธิ์ในการเข้าถึงข้อมูลส่วนบุคคลของคุณ</li>
|
|
<li>สิทธิ์ในการแก้ไขข้อมูลที่ไม่ถูกต้อง</li>
|
|
<li>สิทธิ์ในการลบข้อมูล (สิทธิ์ที่จะถูกลืม)</li>
|
|
<li>สิทธิ์ในการระงับการประมวลผล</li>
|
|
<li>สิทธิ์ในการพกพาข้อมูล</li>
|
|
<li>สิทธิ์ในการคัดค้านการประมวลผล</li>
|
|
<li>สิทธิ์ในการถอนความยินยอมเมื่อใดก็ได้</li>
|
|
</ul>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">8. ความปลอดภัยของข้อมูล</h2>
|
|
<p class="mb-6">
|
|
เราใช้มาตรการรักษาความปลอดภัยที่เหมาะสมทั้งทางเทคนิคและองค์กร
|
|
เพื่อปกป้องข้อมูลส่วนบุคคลของคุณจากการเข้าถึง การแก้ไข
|
|
การเปิดเผย หรือการทำลายโดยไม่ได้รับอนุญาต
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">9. การโอนข้อมูลข้ามประเทศ</h2>
|
|
<p class="mb-6">
|
|
[ถ้ามี] ข้อมูลของคุณอาจถูกโอนไปยังและประมวลผลในประเทศอื่นๆ นอกเหนือจากประเทศไทย
|
|
เรารับรองว่ามีมาตรการคุ้มครองที่เหมาะสมสำหรับการโอนดังกล่าว
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">10. การติดต่อและข้อร้องเรียน</h2>
|
|
<p class="mb-6">
|
|
สำหรับคำถามใดๆ หรือเพื่อใช้สิทธิ์ของคุณ ติดต่อเราได้ที่: [อีเมลติดต่อของคุณ]
|
|
<br /><br />
|
|
คุณยังมีสิทธิ์ในการยื่นข้อร้องเรียนต่อคณะกรรมการคุ้มครองข้อมูลส่วนบุคคล (PDPC)
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">11. การอัปเดตนโยบาย</h2>
|
|
<p class="mb-6">
|
|
เราอาจอัปเดตนโยบายนี้เป็นครั้งคราว เราจะแจ้งให้คุณทราบถึงการเปลี่ยนแปลงใดๆ
|
|
โดยการลงประกาศนโยบายใหม่บนหน้านี้และอัปเดตเลขฉบับที่
|
|
</p>
|
|
|
|
<div class="mt-12 pt-8 border-t">
|
|
<p class="text-gray-500">
|
|
For the English version of this policy, please see:
|
|
<a href="/privacy-policy" class="text-primary hover:underline">Privacy Policy</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</BaseLayout>
|
|
"""
|
|
else:
|
|
return f"""---
|
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
|
|
const lastUpdated = new Date().toLocaleDateString('en-US', {{
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
}});
|
|
const version = '1.0.0';
|
|
---
|
|
|
|
<BaseLayout title="Privacy Policy | {args.umami_domain.split('.')[0] if args.umami_domain else 'Website'}">
|
|
<section class="py-20 bg-white">
|
|
<div class="container mx-auto px-4">
|
|
<h1 class="text-4xl md:text-5xl font-bold text-center mb-4 text-gray-900">
|
|
Privacy Policy
|
|
</h1>
|
|
<p class="text-center text-gray-600 mb-12">
|
|
Version {{version}} | Last updated: {{lastUpdated}}
|
|
</p>
|
|
|
|
<div class="max-w-4xl mx-auto prose prose-lg">
|
|
<h2 class="text-2xl font-bold mb-4">1. Data Controller</h2>
|
|
<p class="mb-6">
|
|
<strong>{args.umami_domain.split('.')[0] if args.umami_domain else 'Website'}</strong> is the data controller responsible for your personal data.
|
|
<br />
|
|
Contact: [Your contact information]
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">2. Data We Collect</h2>
|
|
<p class="mb-6">We collect the following types of personal data:</p>
|
|
<ul class="mb-6">
|
|
<li>Contact information (name, email, phone number) when you contact us</li>
|
|
<li>Usage data (page views, session duration) via analytics</li>
|
|
<li>Cookie preferences and consent records</li>
|
|
<li>Technical data (IP address, browser type, device information)</li>
|
|
</ul>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">3. Purpose of Processing</h2>
|
|
<p class="mb-6">We process your data for the following purposes:</p>
|
|
<ul class="mb-6">
|
|
<li>To provide and maintain our services</li>
|
|
<li>To communicate with you about our services</li>
|
|
<li>To improve our website through analytics (with your consent)</li>
|
|
<li>To comply with legal obligations</li>
|
|
</ul>
|
|
<p class="mb-6">
|
|
<strong>Legal Basis:</strong> Consent, legitimate interest, contractual necessity
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">4. Data Retention</h2>
|
|
<p class="mb-6">
|
|
We retain personal data for as long as necessary to fulfill the purposes outlined in this policy,
|
|
or as required by law. Consent records are retained for 10 years to comply with PDPA requirements.
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">5. Data Sharing & Disclosure</h2>
|
|
<p class="mb-6">We do not sell or rent your personal data. We may share data with:</p>
|
|
<ul class="mb-6">
|
|
<li>Service providers who process data on our behalf</li>
|
|
<li>Legal authorities when required by law</li>
|
|
<li>Analytics providers (only with your consent)</li>
|
|
</ul>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">6. Cookies & Tracking</h2>
|
|
<p class="mb-6">
|
|
We use cookies and similar technologies. Essential cookies are always active.
|
|
Analytics and marketing cookies require your explicit consent. You can manage
|
|
your preferences through our cookie consent banner.
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">7. Your Rights (PDPA)</h2>
|
|
<p class="mb-6">You have the following rights under PDPA:</p>
|
|
<ul class="mb-6">
|
|
<li>Right to access your personal data</li>
|
|
<li>Right to rectification (correction) of inaccurate data</li>
|
|
<li>Right to erasure (right to be forgotten)</li>
|
|
<li>Right to restrict processing</li>
|
|
<li>Right to data portability</li>
|
|
<li>Right to object to processing</li>
|
|
<li>Right to withdraw consent at any time</li>
|
|
</ul>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">8. Data Security</h2>
|
|
<p class="mb-6">
|
|
We implement appropriate technical and organizational measures to protect your personal data
|
|
against unauthorized access, alteration, disclosure, or destruction.
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">9. Cross-Border Transfers</h2>
|
|
<p class="mb-6">
|
|
[If applicable] Your data may be transferred to and processed in countries other than Thailand.
|
|
We ensure appropriate safeguards are in place for such transfers.
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">10. Contact & Complaints</h2>
|
|
<p class="mb-6">
|
|
For any questions or to exercise your rights, contact us at: [Your contact email]
|
|
<br /><br />
|
|
You also have the right to lodge a complaint with the Personal Data Protection Committee (PDPC).
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">11. Policy Updates</h2>
|
|
<p class="mb-6">
|
|
We may update this policy from time to time. We will notify you of any changes by posting
|
|
the new policy on this page and updating the version number.
|
|
</p>
|
|
|
|
<div class="mt-12 pt-8 border-t">
|
|
<p class="text-gray-500">
|
|
สำหรับเวอร์ชันภาษาไทย โปรดดูที่:
|
|
<a href="/th/privacy-policy" class="text-primary hover:underline">นโยบายความเป็นส่วนตัว</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</BaseLayout>
|
|
"""
|
|
|
|
|
|
def get_terms_template(lang):
|
|
"""Generate Terms & Conditions template."""
|
|
|
|
if lang == 'th':
|
|
return """---
|
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
|
|
const lastUpdated = new Date().toLocaleDateString('th-TH', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
---
|
|
|
|
<BaseLayout title="ข้อกำหนดและเงื่อนไข | Website">
|
|
<section class="py-20 bg-white">
|
|
<div class="container mx-auto px-4">
|
|
<h1 class="text-4xl md:text-5xl font-bold text-center mb-4 text-gray-900">
|
|
ข้อกำหนดและเงื่อนไข
|
|
</h1>
|
|
<p class="text-center text-gray-600 mb-12">
|
|
อัปเดตล่าสุด: {lastUpdated}
|
|
</p>
|
|
|
|
<div class="max-w-4xl mx-auto prose prose-lg">
|
|
<h2 class="text-2xl font-bold mb-4">1. การยอมรับเงื่อนไข</h2>
|
|
<p class="mb-6">
|
|
ด้วยการเข้าถึงและใช้เว็บไซต์นี้ คุณยอมรับและตกลงที่จะถูกผูกพันด้วยข้อกำหนด
|
|
และบทบัญญัติของข้อตกลงนี้
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">2. บริการ</h2>
|
|
<p class="mb-6">
|
|
เว็บไซต์ให้บริการ [ระบุบริการของคุณ] รายละเอียดบริการที่สมบูรณ์จะแจ้งให้ทราบแยกต่างหากเมื่อมีการใช้บริการ
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">3. ทรัพย์สินทางปัญญา</h2>
|
|
<p class="mb-6">
|
|
เนื้อหาทั้งหมดบนเว็บไซต์นี้ รวมถึงข้อความ กราฟิก โลโก้ และซอฟต์แวร์
|
|
เป็นทรัพย์สินของเว็บไซต์และอยู่ภายใต้การคุ้มครองกฎหมายลิขสิทธิ์ของไทยและสากล
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">4. หน้าที่ของผู้ใช้</h2>
|
|
<p class="mb-6">
|
|
คุณตกลงที่จะใช้เว็บไซต์นี้เพื่อวัตถุประสงค์ที่ถูกต้องตามกฎหมายเท่านั้น
|
|
และในวิธีที่ไม่ละเมิดสิทธิ์ จำกัด หรือยับยั้งการใช้งานของผู้อื่น
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">5. การจำกัดความรับผิด</h2>
|
|
<p class="mb-6">
|
|
เว็บไซต์จะไม่รับผิดต่อความเสียหายทางอ้อม โดยบังเอิญ เฉพาะเรื่อง หรือเชิงลงโทษ
|
|
อันเกิดจากการใช้หรือไม่สามารถใช้เว็บไซต์นี้
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">6. กฎหมายที่ใช้บังคับ</h2>
|
|
<p class="mb-6">
|
|
ข้อกำหนดเหล่านี้จะอยู่ภายใต้การตีความและบังคับตามกฎหมายของประเทศไทย
|
|
โดยไม่คำนึงถึงหลักการขัดกันแห่งกฎหมาย
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">7. การระงับข้อพิพาท</h2>
|
|
<p class="mb-6">
|
|
ข้อพิพาทใดๆ ที่เกิดจากข้อกำหนดนี้จะได้รับการแก้ไขผ่านการเจรจาต่อรองด้วยดี
|
|
หากไม่สำเร็จ ข้อพิพาทจะถูกยื่นต่อศาลที่มีเขตอำนาจในประเทศไทย
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">8. การแก้ไข</h2>
|
|
<p class="mb-6">
|
|
เราขอสงวนสิทธิ์ในการแก้ไขข้อกำหนดเหล่านี้ได้ตลอดเวลา
|
|
การใช้เว็บไซต์ต่อไปหลังจากมีการเปลี่ยนแปลงถือเป็นการยอมรับการเปลี่ยนแปลง tersebut
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">9. ข้อมูลการติดต่อ</h2>
|
|
<p class="mb-6">
|
|
สำหรับคำถามเกี่ยวกับข้อกำหนดเหล่านี้ โปรดติดต่อเราที่: [อีเมลติดต่อของคุณ]
|
|
</p>
|
|
|
|
<div class="mt-12 pt-8 border-t">
|
|
<p class="text-gray-500">
|
|
For the English version of these terms, please see:
|
|
<a href="/terms-and-conditions" class="text-primary hover:underline">Terms and Conditions</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</BaseLayout>
|
|
"""
|
|
else:
|
|
return """---
|
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
|
|
const lastUpdated = new Date().toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
---
|
|
|
|
<BaseLayout title="Terms & Conditions | Website">
|
|
<section class="py-20 bg-white">
|
|
<div class="container mx-auto px-4">
|
|
<h1 class="text-4xl md:text-5xl font-bold text-center mb-4 text-gray-900">
|
|
Terms and Conditions
|
|
</h1>
|
|
<p class="text-center text-gray-600 mb-12">
|
|
Last updated: {lastUpdated}
|
|
</p>
|
|
|
|
<div class="max-w-4xl mx-auto prose prose-lg">
|
|
<h2 class="text-2xl font-bold mb-4">1. Acceptance of Terms</h2>
|
|
<p class="mb-6">
|
|
By accessing and using this website, you accept and agree to be bound by the terms
|
|
and provision of this agreement.
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">2. Services</h2>
|
|
<p class="mb-6">
|
|
Website provides [describe your services]. Detailed service terms are provided
|
|
separately upon engagement.
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">3. Intellectual Property</h2>
|
|
<p class="mb-6">
|
|
All content on this website, including text, graphics, logos, and software, is the
|
|
property of Website and is protected by Thai and international copyright laws.
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">4. User Obligations</h2>
|
|
<p class="mb-6">
|
|
You agree to use this website only for lawful purposes and in a way that does not
|
|
infringe the rights of, restrict or inhibit anyone else's use of the website.
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">5. Limitation of Liability</h2>
|
|
<p class="mb-6">
|
|
Website shall not be liable for any indirect, incidental, special, consequential
|
|
or punitive damages resulting from your use of or inability to use this website.
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">6. Governing Law</h2>
|
|
<p class="mb-6">
|
|
These terms shall be governed by and construed in accordance with the laws of
|
|
Thailand, without regard to its conflict of law provisions.
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">7. Dispute Resolution</h2>
|
|
<p class="mb-6">
|
|
Any disputes arising from these terms shall be resolved through good faith negotiations.
|
|
If unsuccessful, disputes shall be submitted to the competent courts of Thailand.
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">8. Modifications</h2>
|
|
<p class="mb-6">
|
|
We reserve the right to modify these terms at any time. Continued use of the website
|
|
following any changes constitutes acceptance of those changes.
|
|
</p>
|
|
|
|
<h2 class="text-2xl font-bold mb-4">9. Contact Information</h2>
|
|
<p class="mb-6">
|
|
For questions about these terms, please contact us at: [Your contact email]
|
|
</p>
|
|
|
|
<div class="mt-12 pt-8 border-t">
|
|
<p class="text-gray-500">
|
|
สำหรับเวอร์ชันภาษาไทย โปรดดูที่:
|
|
<a href="/th/terms-and-conditions" class="text-primary hover:underline">ข้อกำหนดและเงื่อนไข</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</BaseLayout>
|
|
"""
|
|
|
|
|
|
def get_cookie_banner_template():
|
|
"""Get cookie banner component template."""
|
|
return """---
|
|
const siteName = "Website";
|
|
---
|
|
|
|
<div
|
|
id="cookie-consent-banner"
|
|
class="fixed bottom-0 left-0 right-0 bg-white shadow-lg p-6 z-50 hidden"
|
|
data-component="cookie-banner"
|
|
>
|
|
<div class="container mx-auto max-w-4xl">
|
|
<h2 class="text-xl font-bold mb-4">🍪 Cookie Consent</h2>
|
|
<p class="mb-6 text-base">
|
|
We use cookies to improve your experience. By clicking "Accept All",
|
|
you consent to our use of cookies.
|
|
<a href="/privacy-policy" class="text-blue-600 underline">Learn more</a>
|
|
</p>
|
|
<div class="flex gap-4 flex-wrap">
|
|
<button
|
|
id="consent-reject"
|
|
class="px-6 py-3 bg-gray-200 hover:bg-gray-300 rounded text-base"
|
|
>
|
|
Reject Non-Essential
|
|
</button>
|
|
<button
|
|
id="consent-accept"
|
|
class="px-6 py-3 bg-blue-600 text-white hover:bg-blue-700 rounded text-base"
|
|
>
|
|
Accept All
|
|
</button>
|
|
<button
|
|
id="consent-customize"
|
|
class="px-6 py-3 border border-blue-600 text-blue-600 hover:bg-blue-50 rounded text-base"
|
|
>
|
|
Customize
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function initCookieBanner() {
|
|
const banner = document.getElementById('cookie-consent-banner');
|
|
|
|
window.addEventListener('show-consent-banner', () => {
|
|
const existingConsent = localStorage.getItem('consent-preferences');
|
|
if (!existingConsent) {
|
|
banner?.classList.remove('hidden');
|
|
}
|
|
});
|
|
|
|
document.getElementById('consent-accept')?.addEventListener('click', () => {
|
|
handleConsent({ essential: true, analytics: true, marketing: true });
|
|
banner?.classList.add('hidden');
|
|
});
|
|
|
|
document.getElementById('consent-reject')?.addEventListener('click', () => {
|
|
handleConsent({ essential: true, analytics: false, marketing: false });
|
|
banner?.classList.add('hidden');
|
|
});
|
|
|
|
document.getElementById('consent-customize')?.addEventListener('click', () => {
|
|
const event = new CustomEvent('open-consent-preferences');
|
|
window.dispatchEvent(event);
|
|
});
|
|
|
|
async function handleConsent(consent) {
|
|
localStorage.setItem('consent-preferences', JSON.stringify({
|
|
timestamp: new Date().toISOString(),
|
|
...consent
|
|
}));
|
|
|
|
const sessionId = crypto.randomUUID();
|
|
await fetch('/api/consent', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
sessionId,
|
|
locale: document.documentElement.lang,
|
|
...consent,
|
|
policyVersion: '1.0.0',
|
|
}),
|
|
});
|
|
|
|
if (consent.analytics) {
|
|
location.reload();
|
|
}
|
|
}
|
|
}
|
|
|
|
initCookieBanner();
|
|
</script>
|
|
"""
|
|
|
|
|
|
def get_admin_dashboard_template():
|
|
"""Get admin dashboard template."""
|
|
return """---
|
|
import { db, ConsentLog, desc } from 'astro:db';
|
|
|
|
const ADMIN_PASSWORD = Astro.env.ADMIN_PASSWORD || 'changeme';
|
|
let logs = [];
|
|
let isAuthenticated = false;
|
|
|
|
if (Astro.request.method === 'POST') {
|
|
const formData = await Astro.request.formData();
|
|
const password = formData.get('password');
|
|
|
|
if (password === ADMIN_PASSWORD) {
|
|
isAuthenticated = true;
|
|
logs = await db.select().from(ConsentLog).orderBy(desc(ConsentLog.timestamp)).limit(100);
|
|
}
|
|
}
|
|
---
|
|
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>Consent Logs Admin</title>
|
|
<style>
|
|
body { font-family: system-ui; background: #f3f4f6; padding: 2rem; }
|
|
.container { max-width: 1400px; margin: 0 auto; }
|
|
h1 { font-size: 2rem; margin-bottom: 1.5rem; }
|
|
.login-form { max-width: 400px; background: white; padding: 2rem; border-radius: 0.5rem; }
|
|
input[type="password"] { width: 100%; padding: 0.75rem; margin-bottom: 1rem; border: 1px solid #d1d5db; border-radius: 0.375rem; }
|
|
button { width: 100%; padding: 0.75rem; background: #2563eb; color: white; border: none; border-radius: 0.375rem; cursor: pointer; }
|
|
table { width: 100%; background: white; border-radius: 0.5rem; overflow: hidden; }
|
|
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; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🔐 Consent Logs Admin</h1>
|
|
{!isAuthenticated ? (
|
|
<div class="login-form">
|
|
<h2>Admin Login</h2>
|
|
<form method="POST">
|
|
<input type="password" name="password" placeholder="Password" required />
|
|
<button type="submit">Login</button>
|
|
</form>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<a href="/admin/consent-logs" style="display: inline-block; margin-bottom: 1rem; padding: 0.5rem 1rem; background: #2563eb; color: white; text-decoration: none; border-radius: 0.375rem;">Refresh</a>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th><th>Locale</th><th>Session ID</th><th>Essential</th><th>Analytics</th><th>Marketing</th><th>Policy Ver</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{logs.map(log => (
|
|
<tr>
|
|
<td>{new Date(log.timestamp).toLocaleString()}</td>
|
|
<td>{log.locale}</td>
|
|
<td style="font-family: monospace;">{log.sessionId}</td>
|
|
<td>{log.essential ? '✓' : '✗'}</td>
|
|
<td>{log.analytics ? '✓' : '✗'}</td>
|
|
<td>{log.marketing ? '✓' : '✗'}</td>
|
|
<td>{log.policyVersion}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def get_db_config_template():
|
|
"""Get database configuration template."""
|
|
return """import { defineDb, defineTable, column } from 'astro:db';
|
|
|
|
const ConsentLog = defineTable({
|
|
columns: {
|
|
id: column.number({ primaryKey: true }),
|
|
sessionId: column.text({ unique: true }),
|
|
timestamp: column.date(),
|
|
locale: column.text(),
|
|
essential: column.boolean(),
|
|
analytics: column.boolean(),
|
|
marketing: column.boolean(),
|
|
policyVersion: column.text(),
|
|
ipHash: column.text(),
|
|
userAgent: column.text(),
|
|
},
|
|
});
|
|
|
|
export default defineDb({
|
|
tables: { ConsentLog },
|
|
});
|
|
"""
|
|
|
|
|
|
def get_db_seed_template():
|
|
"""Get database seed template."""
|
|
return """import { db, ConsentLog } from 'astro:db';
|
|
|
|
export default async function seed() {
|
|
await db.insert(ConsentLog).values([
|
|
{
|
|
sessionId: 'dev-session-001',
|
|
timestamp: new Date(),
|
|
locale: 'en',
|
|
essential: true,
|
|
analytics: true,
|
|
marketing: false,
|
|
policyVersion: '1.0.0',
|
|
ipHash: 'dev1234567890abcd',
|
|
userAgent: 'Mozilla/5.0 (development)',
|
|
},
|
|
]);
|
|
}
|
|
"""
|
|
|
|
|
|
def get_i18n_lib_template(languages):
|
|
"""Get i18n utilities template."""
|
|
lang_entries = []
|
|
for lang in languages:
|
|
if lang == 'th':
|
|
lang_entries.append(" th: { name: 'ไทย', locale: 'th' },")
|
|
else:
|
|
lang_entries.append(" en: { name: 'English', locale: 'en' },")
|
|
|
|
return f"""export const languages = {{
|
|
{chr(10).join(lang_entries)}
|
|
}};
|
|
|
|
export const defaultLocale = 'en';
|
|
|
|
export function getLanguageFromLocale(locale: string) {{
|
|
return languages[locale as keyof typeof languages] || languages.en;
|
|
}}
|
|
"""
|
|
|
|
|
|
def create_api_endpoints(output_dir):
|
|
"""Create API endpoint files."""
|
|
|
|
api_dir = output_dir / 'src' / 'pages' / 'api' / 'consent'
|
|
api_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# POST endpoint
|
|
post_endpoint = """import type { APIRoute } from 'astro';
|
|
import { db, ConsentLog } from 'astro:db';
|
|
import { createHash } from 'crypto';
|
|
|
|
export const POST: APIRoute = async ({ request }) => {
|
|
try {
|
|
const data = await request.json();
|
|
const { sessionId, locale, essential, analytics, marketing, policyVersion } = data;
|
|
|
|
if (!sessionId || !locale) {
|
|
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 = createHash('sha256').update(ip).digest('hex').substring(0, 16);
|
|
|
|
await db.insert(ConsentLog).values({
|
|
sessionId,
|
|
timestamp: new Date(),
|
|
locale,
|
|
essential: essential || false,
|
|
analytics: analytics || false,
|
|
marketing: marketing || false,
|
|
policyVersion,
|
|
ipHash,
|
|
userAgent: request.headers.get('user-agent') || '',
|
|
});
|
|
|
|
return new Response(
|
|
JSON.stringify({ success: true, sessionId }),
|
|
{ status: 201, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
} catch (error) {
|
|
console.error('Consent logging error:', error);
|
|
return new Response(
|
|
JSON.stringify({ error: 'Failed to log consent' }),
|
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
};
|
|
"""
|
|
(api_dir / 'POST.ts').write_text(post_endpoint)
|
|
|
|
# GET endpoint
|
|
get_endpoint = """import type { APIRoute } from 'astro';
|
|
import { db, ConsentLog, desc } from 'astro:db';
|
|
|
|
export const GET: APIRoute = async () => {
|
|
try {
|
|
const logs = await db.select().from(ConsentLog).orderBy(desc(ConsentLog.timestamp)).limit(100);
|
|
|
|
return new Response(
|
|
JSON.stringify({ success: true, logs }),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
} catch (error) {
|
|
console.error('Consent fetch error:', error);
|
|
return new Response(
|
|
JSON.stringify({ error: 'Failed to fetch consent logs' }),
|
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
};
|
|
"""
|
|
(api_dir / 'GET.ts').write_text(get_endpoint)
|
|
|
|
# DELETE endpoint
|
|
delete_dir = api_dir / '[sessionId]'
|
|
delete_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
delete_endpoint = """import type { APIRoute } from 'astro';
|
|
import { db, ConsentLog, eq } from 'astro:db';
|
|
|
|
export const DELETE: APIRoute = async ({ params }) => {
|
|
try {
|
|
const { sessionId } = params;
|
|
|
|
if (!sessionId) {
|
|
return new Response(
|
|
JSON.stringify({ error: 'Session ID required' }),
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
|
|
const result = await db.delete(ConsentLog).where(
|
|
eq(ConsentLog.sessionId, sessionId)
|
|
);
|
|
|
|
return new Response(
|
|
JSON.stringify({ success: true, deleted: result.changes > 0 }),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
} catch (error) {
|
|
console.error('Consent deletion error:', error);
|
|
return new Response(
|
|
JSON.stringify({ error: 'Failed to delete consent' }),
|
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
};
|
|
"""
|
|
(delete_dir / 'DELETE.ts').write_text(delete_endpoint)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|