import { awardPoints } from "../../utils/scoring"; import { addUserCustomDomain, countActiveCustomDomains, CUSTOM_DOMAIN_TYPES, type CustomDomainType, } from "../../db/domains"; import { getProfile } from "../../db/profile"; import { getPlanLimits } from "../../utils/plan-features"; // Regex: Domain muss mindestens eine TLD haben (z.B. "casino.de", "x.co.uk") const DOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/; // Display-Name-Pattern: Text ohne Punkte/Slashes (keine Domain-Syntax) // Erlaubt: Buchstaben, Ziffern, Leerzeichen, Bindestrich, Unterstrich const DISPLAY_NAME_PATTERN_RE = /^[a-zA-Z0-9\s\-_]+$/; export default defineEventHandler(async (event) => { const user = await requireUser(event); const body = await readBody(event); // type aus Body lesen, Default 'web' const rawType = (body?.type as string)?.trim() ?? "web"; if (!CUSTOM_DOMAIN_TYPES.includes(rawType as CustomDomainType)) { throw createError({ statusCode: 400, data: { error: "INVALID_TYPE", validTypes: CUSTOM_DOMAIN_TYPES }, }); } const type = rawType as CustomDomainType; // domain/pattern normalisieren let value = (body?.domain as string)?.trim() ?? ""; if (type === "mail_display_name") { // Display-Name-Pattern: Case-sensitive gespeichert wie eingegeben, // Matching erfolgt case-insensitiv. Keine Domain-Normalisierung. if (!value || value.length < 2 || !DISPLAY_NAME_PATTERN_RE.test(value)) { throw createError({ statusCode: 400, data: { error: "INVALID_DISPLAY_NAME_PATTERN" }, }); } // Sanity: kein Punkt/Slash → kein Domain-Format if (value.includes(".") || value.includes("/")) { throw createError({ statusCode: 400, data: { error: "DISPLAY_NAME_LOOKS_LIKE_DOMAIN" }, }); } } else { // web und mail_domain: Domain-Validierung value = value.toLowerCase().replace(/^https?:\/\//, ""); if (!value || !DOMAIN_RE.test(value)) { throw createError({ statusCode: 400, data: { error: "INVALID_DOMAIN" } }); } if (type === "mail_domain" && !value.includes(".")) { throw createError({ statusCode: 400, data: { error: "MAIL_DOMAIN_MISSING_TLD" }, }); } } // Shared Slot-Pool prüfen (alle Types zusammen) const profile = await getProfile(user.id); const limits = getPlanLimits(profile?.plan ?? "free"); if (limits.customDomains !== Infinity) { const activeCount = await countActiveCustomDomains(user.id); if (activeCount >= limits.customDomains) { throw createError({ statusCode: 403, data: { error: "PLAN_LIMIT", resource: "custom_domains", current: activeCount, limit: limits.customDomains, }, }); } } try { const data = await addUserCustomDomain(user.id, value, "manual", type); await awardPoints(user.id, "custom_domain_submitted", { domain: value }).catch( () => {}, ); return data; } catch (err: any) { const msg = err.message?.includes("duplicate") || err.code === "P2002" ? "Eintrag bereits vorhanden" : err.message ?? "Fehler"; throw createError({ statusCode: 400, message: msg }); } });