TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1import { NextRequest, NextResponse } from "next/server";2import { z } from "zod";3import { eq, desc, inArray } from "drizzle-orm";4import { db } from "@/db/client";5import { listings, properties, moderationActions, users } from "@/db/schema";6import { assertSameOrigin, requireRole } from "@/lib/auth/session";7import { requireFreshMfa } from "@/lib/admin/mfa";8import { audit } from "@/lib/auth/audit";9import { sendMail, notifyMail } from "@/lib/auth/email";10import { handle, parseBody, apiError } from "@/lib/api";11export const dynamic = "force-dynamic";12export 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); } }13const Body = z.object({ id: z.string().uuid(), action: z.enum(["approve", "reject", "unpublish"]), note: z.string().max(500).optional() });14export async function POST(req: NextRequest) {15 try { await assertSameOrigin(); const u = await requireRole("MODERATOR"); requireFreshMfa(u); const b = await parseBody(req, Body); const d = db();16 const [l] = await d.select().from(listings).where(eq(listings.id, b.id)); if (!l) return apiError(404, "not_found", "Annonce introuvable");17 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" };18 await d.update(listings).set({ ...set, updatedAt: new Date() }).where(eq(listings.id, b.id));19 await d.insert(moderationActions).values({ actorId: u.id, targetType: "LISTING", targetId: b.id, action: b.action, note: b.note ?? null });20 await audit(`admin.listing.${b.action}`, { actorId: u.id, objectType: "listing", objectId: b.id, meta: { note: b.note ?? null } });21 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 { /* */ } } }22 return NextResponse.json({ ok: true }); } catch (e) { return handle(e); }23}24