Copy overhaul + qr designs

This commit is contained in:
2026-07-27 17:54:59 +02:00
parent 033bc7e29d
commit 70d97aa970
144 changed files with 23107 additions and 1699 deletions

View File

@@ -0,0 +1,129 @@
import { NextRequest, NextResponse } from 'next/server';
import { getSessionUserId } from '@/lib/session';
import { db } from '@/lib/db';
import { csrfProtection } from '@/lib/csrf';
import { z } from 'zod';
/**
* Saved QR design presets.
*
* Business-only. The value here is repeatability, not novelty: an agency running
* several clients needs client A to look identical across every code, including
* the 500 that came out of one spreadsheet upload.
*/
const MAX_PRESETS = 50;
const presetSchema = z.object({
name: z.string().min(1, 'Name is required').max(60),
style: z.record(z.any()),
});
function isAllowed(plan: string | undefined): boolean {
return plan === 'BUSINESS' || plan === 'ENTERPRISE';
}
export async function GET() {
const userId = getSessionUserId();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const presets = await db.qRDesignPreset.findMany({
where: { userId },
orderBy: { updatedAt: 'desc' },
});
return NextResponse.json(presets);
}
export async function POST(request: NextRequest) {
const csrfCheck = csrfProtection(request);
if (!csrfCheck.valid) {
return NextResponse.json({ error: csrfCheck.error }, { status: 403 });
}
const userId = getSessionUserId();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const user = await db.user.findUnique({
where: { id: userId },
select: { plan: true },
});
if (!isAllowed(user?.plan)) {
return NextResponse.json(
{
error: 'Upgrade required',
message: 'Saved design presets are part of the Business plan.',
plan: user?.plan ?? 'FREE',
},
{ status: 403 }
);
}
let data;
try {
data = presetSchema.parse(await request.json());
} catch (err) {
if (err instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid input', details: err.errors },
{ status: 400 }
);
}
return NextResponse.json({ error: 'Invalid input' }, { status: 400 });
}
const count = await db.qRDesignPreset.count({ where: { userId } });
const existing = await db.qRDesignPreset.findFirst({
where: { userId, name: data.name },
select: { id: true },
});
if (!existing && count >= MAX_PRESETS) {
return NextResponse.json(
{
error: 'Preset limit reached',
message: `You can keep up to ${MAX_PRESETS} presets. Delete one to save another.`,
},
{ status: 403 }
);
}
// Same name overwrites rather than creating a near-duplicate. Someone saving
// "Client A" twice means "update it", not "keep both".
const preset = await db.qRDesignPreset.upsert({
where: { userId_name: { userId, name: data.name } },
create: { userId, name: data.name, style: data.style },
update: { style: data.style },
});
return NextResponse.json(preset, { status: existing ? 200 : 201 });
}
export async function DELETE(request: NextRequest) {
const csrfCheck = csrfProtection(request);
if (!csrfCheck.valid) {
return NextResponse.json({ error: csrfCheck.error }, { status: 403 });
}
const userId = getSessionUserId();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const id = new URL(request.url).searchParams.get('id');
if (!id) {
return NextResponse.json({ error: 'Missing id' }, { status: 400 });
}
const deleted = await db.qRDesignPreset.deleteMany({ where: { id, userId } });
if (deleted.count === 0) {
return NextResponse.json({ error: 'Preset not found' }, { status: 404 });
}
return NextResponse.json({ ok: true });
}