feat(thai-frontend-dev): update consent system to better-sqlite3
- Replace Astro DB with better-sqlite3 (bypasses remote SQLite limitation) - Add consent API with auto-create table pattern - Update admin dashboard to fetch from API instead of Astro DB - Add IP hashing and rate limiting for GDPR compliance - Add seo-utils-mcp-guide skill - Update legal templates with business placeholders
This commit is contained in:
@@ -193,7 +193,6 @@ def ask_analytics_setup():
|
||||
|
||||
ASTRO_CONFIG_TEMPLATE = """import {{ defineConfig }} from 'astro/config';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import db from '@astrojs/db';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
|
||||
export default defineConfig({{
|
||||
@@ -212,13 +211,22 @@ export default defineConfig({{
|
||||
}},
|
||||
integrations: [
|
||||
tailwindcss(),
|
||||
db(),
|
||||
sitemap({{
|
||||
i18n: {{
|
||||
defaultLocale: '{default_locale}',
|
||||
}},
|
||||
}}),
|
||||
],
|
||||
vite: {{
|
||||
optimizeDeps: {{
|
||||
exclude: ['better-sqlite3']
|
||||
}},
|
||||
build: {{
|
||||
rollupOptions: {{
|
||||
external: ['better-sqlite3']
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}});
|
||||
"""
|
||||
|
||||
@@ -228,25 +236,217 @@ PACKAGE_JSON_TEMPLATE = """{{
|
||||
"version": "1.0.0",
|
||||
"scripts": {{
|
||||
"dev": "astro dev",
|
||||
"build": "astro build --remote",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"db:push": "astro db push --remote",
|
||||
"db:seed": "astro db seed"
|
||||
"astro": "astro"
|
||||
}},
|
||||
"dependencies": {{
|
||||
"astro": "^5.17.1",
|
||||
"@astrojs/db": "^0.14.0",
|
||||
"@astrojs/sitemap": "^3.2.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"astro-consent": "^1.0.0",
|
||||
"drizzle-orm": "^0.38.0",
|
||||
"@libsql/client": "^0.14.0"
|
||||
"better-sqlite3": "^11.0.0"
|
||||
}},
|
||||
"devDependencies": {{
|
||||
"@types/better-sqlite3": "^7.6.8"
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
# ==============================================================================
|
||||
# CONSENT API TEMPLATES (using better-sqlite3 directly)
|
||||
# ==============================================================================
|
||||
|
||||
CONSENT_DB_TEMPLATE = """import Database from 'better-sqlite3';
|
||||
import {{ join }} from 'path';
|
||||
import {{ mkdirSync, existsSync }} from 'fs';
|
||||
|
||||
const DATA_DIR = join(process.cwd(), 'data');
|
||||
const DB_PATH = join(DATA_DIR, 'consent.db');
|
||||
|
||||
export function getDb() {{
|
||||
// 1. Create directory if not exists
|
||||
if (!existsSync(DATA_DIR)) {{
|
||||
mkdirSync(DATA_DIR, {{ recursive: true }});
|
||||
}}
|
||||
|
||||
// 2. Open database
|
||||
const db = new Database(DB_PATH);
|
||||
|
||||
// 3. Auto-create table (works with remote SQLite!)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS ConsentLog (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sessionId TEXT UNIQUE NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
essential INTEGER NOT NULL DEFAULT 0,
|
||||
analytics INTEGER NOT NULL DEFAULT 0,
|
||||
marketing INTEGER NOT NULL DEFAULT 0,
|
||||
policyVersion TEXT NOT NULL,
|
||||
ipHash TEXT,
|
||||
userAgent TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
return db;
|
||||
}}
|
||||
|
||||
// Rate limiting map
|
||||
const rateLimitMap = new Map<string, {{ count: number; resetTime: number }}>();
|
||||
const RATE_LIMIT = 10;
|
||||
const RATE_WINDOW = 60000; // 1 minute
|
||||
|
||||
export function checkRateLimit(ip: string): boolean {{
|
||||
const now = Date.now();
|
||||
const record = rateLimitMap.get(ip);
|
||||
|
||||
if (!record || now > record.resetTime) {{
|
||||
rateLimitMap.set(ip, {{ count: 1, resetTime: now + RATE_WINDOW }});
|
||||
return true;
|
||||
}}
|
||||
|
||||
if (record.count >= RATE_LIMIT) {{
|
||||
return false;
|
||||
}}
|
||||
|
||||
record.count++;
|
||||
return true;
|
||||
}}
|
||||
|
||||
// IP Hashing for privacy (GDPR compliance)
|
||||
export async function hashIP(ip: string): Promise<string> {{
|
||||
try {{
|
||||
if (crypto.subtle) {{
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(ip));
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
return hashHex.substring(0, 16);
|
||||
}}
|
||||
}} catch {{}}
|
||||
return `fallback-${Date.now()}`;
|
||||
}}
|
||||
"""
|
||||
|
||||
CONSENT_API_INDEX_TEMPLATE = """import type {{ APIRoute }} from 'astro';
|
||||
import {{ getDb, checkRateLimit, hashIP }} from '../../../../lib/db';
|
||||
|
||||
export const GET: APIRoute = async ({{ clientAddress }}) => {{
|
||||
// Rate limit check
|
||||
const ip = clientAddress || 'unknown';
|
||||
if (!checkRateLimit(ip)) {{
|
||||
return new Response(
|
||||
JSON.stringify({{ error: 'Rate limit exceeded' }}),
|
||||
{{ status: 429, headers: {{ 'Content-Type': 'application/json' }} }}
|
||||
);
|
||||
}}
|
||||
|
||||
try {{
|
||||
const db = getDb();
|
||||
const logs = db.prepare(`
|
||||
SELECT * FROM ConsentLog
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 100
|
||||
`).all();
|
||||
db.close();
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({{ logs }}),
|
||||
{{ status: 200, headers: {{ 'Content-Type': 'application/json' }} }}
|
||||
);
|
||||
}} catch (error) {{
|
||||
return new Response(
|
||||
JSON.stringify({{ error: 'Failed to fetch logs' }}),
|
||||
{{ status: 500 }}
|
||||
);
|
||||
}}
|
||||
}};
|
||||
|
||||
export const POST: APIRoute = async ({{ request, clientAddress }}) => {{
|
||||
try {{
|
||||
const body = await request.json();
|
||||
const {{ sessionId, essential, analytics, marketing, policyVersion, userAgent }} = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!sessionId || essential === undefined || !policyVersion) {{
|
||||
return new Response(
|
||||
JSON.stringify({{ error: 'Missing required fields' }}),
|
||||
{{ status: 400, headers: {{ 'Content-Type': 'application/json' }} }}
|
||||
);
|
||||
}}
|
||||
|
||||
const db = getDb();
|
||||
const ipHash = await hashIP(clientAddress || 'unknown');
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Insert with prepared statement (prevents SQL injection)
|
||||
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 }}),
|
||||
{{ status: 201, headers: {{ 'Content-Type': 'application/json' }} }}
|
||||
);
|
||||
}} catch (error) {{
|
||||
return new Response(
|
||||
JSON.stringify({{ error: 'Failed to log consent' }}),
|
||||
{{ status: 500 }}
|
||||
);
|
||||
}}
|
||||
}};
|
||||
"""
|
||||
|
||||
CONSENT_API_DELETE_TEMPLATE = """import type {{ APIRoute }} from 'astro';
|
||||
import {{ getDb }} from '../../../../lib/db';
|
||||
|
||||
export const DELETE: APIRoute = async ({{ params }}) => {{
|
||||
const {{ sessionId }} = params;
|
||||
|
||||
if (!sessionId) {{
|
||||
return new Response(
|
||||
JSON.stringify({{ error: 'Session ID required' }}),
|
||||
{{ status: 400, headers: {{ 'Content-Type': 'application/json' }} }}
|
||||
);
|
||||
}}
|
||||
|
||||
try {{
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('DELETE FROM ConsentLog WHERE sessionId = ?');
|
||||
const result = stmt.run(sessionId);
|
||||
db.close();
|
||||
|
||||
if (result.changes === 0) {{
|
||||
return new Response(
|
||||
JSON.stringify({{ error: 'Record not found' }}),
|
||||
{{ status: 404 }}
|
||||
);
|
||||
}}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({{ success: true }}),
|
||||
{{ status: 200, headers: {{ 'Content-Type': 'application/json' }} }}
|
||||
);
|
||||
}} catch (error) {{
|
||||
return new Response(
|
||||
JSON.stringify({{ error: 'Failed to delete' }}),
|
||||
{{ status: 500 }}
|
||||
);
|
||||
}}
|
||||
}};
|
||||
"""
|
||||
|
||||
# ... (rest of templates remain the same)
|
||||
|
||||
|
||||
@@ -506,10 +706,12 @@ def create_project(args, languages, default_locale, features):
|
||||
output_path / "src" / "layouts",
|
||||
output_path / "src" / "pages",
|
||||
output_path / "src" / "pages" / default_locale,
|
||||
output_path / "src" / "pages" / "admin",
|
||||
output_path / "src" / "pages" / "api" / "consent",
|
||||
output_path / "src" / "styles",
|
||||
output_path / "src" / "content" / "blog",
|
||||
output_path / "src" / "lib",
|
||||
output_path / "db",
|
||||
output_path / "data",
|
||||
]
|
||||
|
||||
for d in dirs:
|
||||
@@ -636,11 +838,42 @@ import Footer from '../components/common/Footer.astro';
|
||||
|
||||
print(" ✓ Basic pages created")
|
||||
|
||||
# Create Dockerfile
|
||||
dockerfile = f"""FROM node:20-slim
|
||||
# Create consent API with better-sqlite3 (bypasses Astro DB limitation)
|
||||
lib_db_content = CONSENT_DB_TEMPLATE
|
||||
(output_path / "src" / "lib" / "db.ts").write_text(lib_db_content, encoding="utf-8")
|
||||
print(" ✓ Consent database library created (better-sqlite3)")
|
||||
|
||||
# Create consent API endpoints
|
||||
consent_index = CONSENT_API_INDEX_TEMPLATE
|
||||
(output_path / "src" / "pages" / "api" / "consent" / "index.ts").write_text(
|
||||
consent_index, encoding="utf-8"
|
||||
)
|
||||
|
||||
consent_delete = CONSENT_API_DELETE_TEMPLATE
|
||||
consent_delete = consent_delete.replace("{{", "{").replace("}}", "}")
|
||||
consent_delete = consent_delete.replace("[sessionId]", "[sessionId]")
|
||||
(output_path / "src" / "pages" / "api" / "consent" / "[sessionId].ts").write_text(
|
||||
consent_delete, encoding="utf-8"
|
||||
)
|
||||
print(" ✓ Consent API endpoints created")
|
||||
|
||||
# Create admin consent logs page
|
||||
admin_consent_src = template_dir / "admin-consent-logs.astro"
|
||||
if admin_consent_src.exists():
|
||||
shutil.copy(
|
||||
admin_consent_src,
|
||||
output_path / "src" / "pages" / "admin" / "consent-logs.astro",
|
||||
)
|
||||
print(" ✓ Admin consent logs page copied")
|
||||
|
||||
# Create Dockerfile (using alpine for better-sqlite3 native module support)
|
||||
dockerfile = f"""FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install build dependencies for better-sqlite3
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
# Install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
@@ -656,7 +889,7 @@ EXPOSE 80
|
||||
CMD ["npm", "run", "preview"]
|
||||
"""
|
||||
(output_path / "Dockerfile").write_text(dockerfile, encoding="utf-8")
|
||||
print(" ✓ Dockerfile created")
|
||||
print(" ✓ Dockerfile created (alpine for better-sqlite3)")
|
||||
|
||||
# Create .gitignore
|
||||
gitignore = """# Dependencies
|
||||
|
||||
Reference in New Issue
Block a user