feat: migrate website-creator from Next.js+Payload to Astro+Tina CMS
Major changes: - Replace Payload CMS with Tina CMS (self-hosted) - Add Astro DB for consent logging (PDPA compliant) - Update Tailwind v3 to v4 (@tailwindcss/vite plugin) - Add astro-tina-starter template - Rewrite consent template for Astro (ConsentBanner.astro, Astro DB, Nano Stores) - Add install-tina-backend.sh for self-hosted Tina per customer - Rename convert-astro.sh to migrate-tina.sh - Add AGENTS.md template for generated websites - Delete all Payload/Next.js files Technical updates: - Astro DB using defineDb with eq operators for queries - Tailwind v4 with @theme block - Tina CMS local development mode - Proper Astro API routes for consent Research-verified with official documentation (April 2026)
This commit is contained in:
120
skills/website-creator/templates/consent/api/consent.ts
Normal file
120
skills/website-creator/templates/consent/api/consent.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db, eq } from 'astro:db';
|
||||
import { ConsentLog } from '../db/config';
|
||||
|
||||
export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
const {
|
||||
action = 'accept',
|
||||
purpose = 'all',
|
||||
analytics = false,
|
||||
marketing = false,
|
||||
functional = false,
|
||||
} = body;
|
||||
|
||||
const ip = clientAddress || 'unknown';
|
||||
const userAgent = request.headers.get('user-agent') || 'unknown';
|
||||
|
||||
const doc = await db.insert(ConsentLog).values({
|
||||
action,
|
||||
purpose,
|
||||
analytics,
|
||||
marketing,
|
||||
functional,
|
||||
ip,
|
||||
userAgent,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
doc,
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Consent API error:', error);
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Failed to log consent',
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const sessionId = url.searchParams.get('sessionId');
|
||||
|
||||
let docs;
|
||||
if (sessionId) {
|
||||
docs = await db.select().from(ConsentLog).where(
|
||||
eq(ConsentLog.sessionId, sessionId)
|
||||
);
|
||||
} else {
|
||||
docs = await db.select().from(ConsentLog);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
docs,
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Consent GET error:', error);
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Failed to retrieve consent logs',
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const DELETE: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const sessionId = url.searchParams.get('sessionId');
|
||||
|
||||
if (!sessionId) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'sessionId is required',
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const deleted = await db.delete(ConsentLog).where(
|
||||
eq(ConsentLog.sessionId, sessionId)
|
||||
);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
deleted,
|
||||
message: 'All consent records for this session have been deleted',
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Right to be forgotten error:', error);
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Failed to delete consent records',
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@/payload.config'
|
||||
|
||||
/**
|
||||
* DELETE /api/consent - Right to be forgotten (GDPR/PDPA)
|
||||
*
|
||||
* Deletes all consent records for a given session or user
|
||||
*/
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const payloadConfig = await config
|
||||
const payload = await getPayload({ config: payloadConfig })
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const sessionId = searchParams.get('sessionId')
|
||||
|
||||
if (!sessionId) {
|
||||
return NextResponse.json({ error: 'sessionId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Find and delete all consent logs for this session
|
||||
const result = await payload.delete({
|
||||
collection: 'consent-logs',
|
||||
where: {
|
||||
sessionId: { equals: sessionId },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
deleted: result.deletedDocs?.length || 0,
|
||||
message: 'All consent records for this session have been deleted'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Right to be forgotten error:', error)
|
||||
return NextResponse.json({ error: 'Failed to delete consent records' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user