User asked for the admin review tooling — and the lyra-bot community post / notification text that goes out with each submission — to know whether a submission is a website-domain or a mail-sender-domain. Until now the type lived only on user_custom_domains and the submission inherited it implicitly via the foreign key. Reading it back for the admin list or the lyra prompt meant joining the source row every time. - migration 20260516_domain_submission_type adds a type column to rebreak.domain_submissions with a default of 'web' and backfills every existing row from its linked user_custom_domains.type. The backfill is idempotent (UPDATE … FROM with the type comparison). - Composite index (type, status) so the admin pending-list can scope by category without scanning the whole table. - submitDomainForReview now copies the source row's type into the new submission. The submit endpoint picks it up to vary the auto-generated community-vote post copy: a website framing for type='web' and an "Mail-Absender"-framing for type='mail_domain'. The user's nickname is the only PII referenced. - adminApproveSubmission returns the type alongside the domain so the approve endpoint's Lyra-bot Groq prompt can swap its subject/action labels per category. Reject path unchanged — the notification just carries the bare domain string, no type framing needed. - BlocklistDomain stays type-agnostic on purpose. The mail-daemon's getBlocklistedDomainsSet is a flat string-set match against sender domain or URL host, and works for both categories without splitting. Adding a type there would be redundant work in v1.0 — revisit only if we ever need a UI to surface what category each global entry came from. 38/38 backend tests pass (8 admin/domains, 30 plan-limits including 5 new for the type-copy semantics and community-post text variants).
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
import { submitDomainForReview } from "../../../db/domains";
|
|
import { getProfile } from "../../../db/profile";
|
|
import { getPlanLimits } from "../../../utils/plan-features";
|
|
import { usePrisma } from "../../../utils/prisma";
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const user = await requireUser(event);
|
|
const id = getRouterParam(event, "id");
|
|
if (!id) throw createError({ statusCode: 400, message: "ID fehlt" });
|
|
|
|
// Only Pro/Legend can submit
|
|
const profile = await getProfile(user.id);
|
|
const plan = profile?.plan ?? "free";
|
|
const limits = getPlanLimits(plan);
|
|
if (!limits.domainRefill) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
message: "Nur Pro-User können Domains einreichen",
|
|
});
|
|
}
|
|
|
|
const db = usePrisma();
|
|
// Verify ownership + status
|
|
const existing = await db.userCustomDomain.findFirst({
|
|
where: { id, userId: user.id },
|
|
select: { id: true, domain: true, status: true, type: true },
|
|
});
|
|
if (!existing)
|
|
throw createError({ statusCode: 404, message: "Domain nicht gefunden" });
|
|
if (existing.status !== "active" && existing.status !== "rejected") {
|
|
throw createError({
|
|
statusCode: 409,
|
|
message: "Domain wurde bereits eingereicht oder genehmigt",
|
|
});
|
|
}
|
|
|
|
// v1.0: Display-Name-Patterns sind nicht submittable (keine BlocklistDomain-Erweiterung vor TestFlight)
|
|
if (existing.type === "mail_display_name") {
|
|
throw createError({
|
|
statusCode: 400,
|
|
data: {
|
|
error: "DISPLAY_NAME_NOT_SUBMITTABLE",
|
|
message:
|
|
"Display-name patterns cannot be submitted to the global blocklist in v1.0. Use them as user-private filters only.",
|
|
},
|
|
});
|
|
}
|
|
|
|
// Tier-Routing:
|
|
// - Pro: Community-Post mit Voting-Flow erstellen
|
|
// - Legend: KEIN Post — Domain/Pattern landet direkt in der Admin-Queue
|
|
//
|
|
// Für mail_display_name: domain-Feld enthält das Pattern-String (kein PII).
|
|
// Admin-Review erkennt type via customDomain.type-Feld.
|
|
let postId: string | null = null;
|
|
if (plan === "pro") {
|
|
// Community-Vote-Post-Text variiert je nach Type
|
|
let postContent: string;
|
|
if (existing.type === "mail_domain") {
|
|
postContent = `Domain-Vorschlag (Mail-Absender): **${existing.domain}**\n\nIch schlage vor, diese Absender-Domain zur globalen ReBreak-Sperrliste hinzuzufügen. Casino-Affiliates nutzen oft Mailing-Listen mit harmlosen Namen. Stimme ab: Sollte **${existing.domain}** global gesperrt werden?`;
|
|
} else {
|
|
// type === "web" (mail_display_name ist durch Guard oben schon blockiert)
|
|
postContent = `Domain-Vorschlag: **${existing.domain}**\n\nIch schlage vor, diese Domain zur globalen ReBreak-Sperrliste hinzuzufügen. Stimme ab: Sollte **${existing.domain}** global gesperrt werden?`;
|
|
}
|
|
const post = await db.communityPost.create({
|
|
data: {
|
|
userId: user.id,
|
|
category: "domain_vote",
|
|
content: postContent,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
postId = post.id;
|
|
}
|
|
|
|
const { submission } = await submitDomainForReview(
|
|
user.id,
|
|
id,
|
|
plan as "free" | "pro" | "legend",
|
|
postId ?? undefined,
|
|
);
|
|
|
|
return {
|
|
ok: true,
|
|
postId,
|
|
submissionId: submission.id,
|
|
domain: existing.domain,
|
|
type: existing.type,
|
|
route: plan === "legend" ? "admin_direct" : "community_vote",
|
|
};
|
|
});
|