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>
64 lines
2.2 KiB
TypeScript
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() };
|
|
});
|