chahinebrini 93eb3aceec feat(vip): VIP-Slot-Replace Backend — Swap mit 24h-Cooldown
Wenn die VIP-Liste (Layer 2) voll ist (>30 eigene Web-Domains) und der
User eine neue Custom-Domain hinzufügt, ersetzt er bewusst eine
bestehende — der Tausch greift in der VIP erst nach 24h Cooldown.

- Schema: UserCustomDomain.vipDeferUntil + vipEvictAt
  (Migration 20260522_add_vip_swap_fields, additiv + nullable)
- getWebCustomDomains: filtert deferred (noch nicht in VIP) + evicted
  (Cooldown durch → raus) — lazy ausgewertet, kein Cron
- POST /api/custom-domains: neue Web-Domain über dem 30er-Cap → wird
  zurückgestellt (vipDeferUntil gesetzt), Response-Flag vipFull
- POST /api/custom-domains/vip-swap: setzt effectiveAt = jetzt+24h auf
  neue + ersetzte Domain
- Layer 1 bleibt unberührt — die neue Domain ist dort sofort aktiv

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:05:44 +02:00

64 lines
2.2 KiB
TypeScript

import { usePrisma } from "../../utils/prisma";
// 24h-Cooldown — identisch zu SWAP_COOLDOWN_MS in index.post.ts.
const SWAP_COOLDOWN_MS = 24 * 60 * 60 * 1000;
/**
* POST /api/custom-domains/vip-swap
*
* VIP-Slot-Replace: die VIP-Liste (Layer 2) ist voll. Der User hat gerade eine
* neue Custom-Domain hinzugefügt (`newDomainId` — steht via `vipDeferUntil`
* bereits zurückgestellt) und wählt jetzt eine seiner EIGENEN Domains
* (`evictedDomainId`), die sie ersetzt.
*
* Beide bekommen denselben `effectiveAt` = jetzt + 24h:
* - die ersetzte Domain fällt dann aus der VIP-Liste (`vipEvictAt`),
* - die neue Domain kommt dann rein (`vipDeferUntil`).
* Layer 1 bleibt für beide unberührt — die neue Domain ist dort sofort aktiv.
*/
export default defineEventHandler(async (event) => {
const user = await requireUser(event);
const body = await readBody(event);
const newDomainId =
typeof body?.newDomainId === "string" ? body.newDomainId : "";
const evictedDomainId =
typeof body?.evictedDomainId === "string" ? body.evictedDomainId : "";
if (!newDomainId || !evictedDomainId) {
throw createError({ statusCode: 400, data: { error: "MISSING_IDS" } });
}
if (newDomainId === evictedDomainId) {
throw createError({ statusCode: 400, data: { error: "SAME_DOMAIN" } });
}
const db = usePrisma();
// Beide Domains müssen dem User gehören und web-Typ sein.
const [newDomain, evicted] = await Promise.all([
db.userCustomDomain.findFirst({
where: { id: newDomainId, userId: user.id, type: "web" },
select: { id: true },
}),
db.userCustomDomain.findFirst({
where: { id: evictedDomainId, userId: user.id, type: "web" },
select: { id: true },
}),
]);
if (!newDomain || !evicted) {
throw createError({ statusCode: 404, data: { error: "DOMAIN_NOT_FOUND" } });
}
const effectiveAt = new Date(Date.now() + SWAP_COOLDOWN_MS);
await db.$transaction([
db.userCustomDomain.update({
where: { id: newDomainId },
data: { vipDeferUntil: effectiveAt },
}),
db.userCustomDomain.update({
where: { id: evictedDomainId },
data: { vipEvictAt: effectiveAt },
}),
]);
return { ok: true, effectiveAt: effectiveAt.toISOString() };
});