import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { eq, desc, inArray } from "drizzle-orm"; import { db } from "@/db/client"; import { listings, properties, moderationActions, users } from "@/db/schema"; import { assertSameOrigin, requireRole } from "@/lib/auth/session"; import { requireFreshMfa } from "@/lib/admin/mfa"; import { audit } from "@/lib/auth/audit"; import { sendMail, notifyMail } from "@/lib/auth/email"; import { handle, parseBody, apiError } from "@/lib/api"; export const dynamic = "force-dynamic"; export async function GET(req: NextRequest) { try { const u = await requireRole("MODERATOR"); requireFreshMfa(u); const status = req.nextUrl.searchParams.get("status") ?? "PENDING_REVIEW"; const rows = await db().select({ l: listings, p: properties, ownerEmail: users.email }).from(listings).innerJoin(properties, eq(properties.id, listings.propertyId)).leftJoin(users, eq(users.id, listings.ownerUserId)).where(inArray(listings.status, status === "all" ? ["DRAFT", "PENDING_REVIEW", "PUBLISHED", "PAUSED", "REJECTED", "ARCHIVED", "SOLD", "RENTED"] : [status])).orderBy(desc(listings.updatedAt)).limit(200); return NextResponse.json({ items: rows }); } catch (e) { return handle(e); } } const Body = z.object({ id: z.string().uuid(), action: z.enum(["approve", "reject", "unpublish"]), note: z.string().max(500).optional() }); export async function POST(req: NextRequest) { try { await assertSameOrigin(); const u = await requireRole("MODERATOR"); requireFreshMfa(u); const b = await parseBody(req, Body); const d = db(); const [l] = await d.select().from(listings).where(eq(listings.id, b.id)); if (!l) return apiError(404, "not_found", "Annonce introuvable"); const set = b.action === "approve" ? { status: "PUBLISHED", moderationState: "HUMAN_OK", moderationNote: null, publicationAt: l.publicationAt ?? new Date(), expiresAt: new Date(Date.now() + 90 * 86400000) } : b.action === "reject" ? { status: "REJECTED", moderationState: "REJECTED", moderationNote: b.note ?? "Refusée par la modération" } : { status: "PAUSED", moderationState: "FLAGGED", moderationNote: b.note ?? "Retirée par la modération" }; await d.update(listings).set({ ...set, updatedAt: new Date() }).where(eq(listings.id, b.id)); await d.insert(moderationActions).values({ actorId: u.id, targetType: "LISTING", targetId: b.id, action: b.action, note: b.note ?? null }); await audit(`admin.listing.${b.action}`, { actorId: u.id, objectType: "listing", objectId: b.id, meta: { note: b.note ?? null } }); if (l.ownerUserId) { const [o] = await d.select({ email: users.email }).from(users).where(eq(users.id, l.ownerUserId)); if (o) { const base = (process.env.APP_BASE_URL ?? "https://www.toveli.ca").replace(/\/$/, ""); try { await sendMail(notifyMail(o.email, b.action === "approve" ? "Votre annonce est publiée" : b.action === "reject" ? "Votre annonce a été refusée" : "Votre annonce a été retirée", b.action === "approve" ? `« ${l.title ?? "Votre annonce"} » est maintenant visible sur Toveli.` : `Motif : ${b.note ?? "non conforme aux conditions"}. Vous pouvez corriger l'annonce et la soumettre de nouveau, ou nous écrire pour contester.`, `${base}/membre/annonces/${l.id}`, "Voir mon annonce", `moderation_${b.action}`)); } catch { /* */ } } } return NextResponse.json({ ok: true }); } catch (e) { return handle(e); } }