src/app/api/admin/auth/vitrine/route.ts

route·app·6.9 KB · 223 lignes· Voir l'itinéraire
Annotation non disponible

Lance npm run annotate (nécessite ANTHROPIC_API_KEY dans .env.local) pour générer une annotation française par Claude Haiku 4.5.

2 exports

POSTdynamic

Code source· typescript· tronqué à 200 lignes sur 223

import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { isValidCodeFormat, normalizeCode } from "@/lib/admin/code-acces";
import { createSession } from "@/lib/auth/session";
import { envoyerBienvenueAdmin } from "@/lib/email";
import { SignJWT } from "jose";

export const dynamic = "force-dynamic";

const NEXTAUTH_SECRET = process.env.NEXTAUTH_SECRET;
if (!NEXTAUTH_SECRET) {
  throw new Error("NEXTAUTH_SECRET is required");
}
const secret = new TextEncoder().encode(NEXTAUTH_SECRET);

const SUBDOMAIN_REGEX = /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])?$/;
const COULEUR_REGEX = /^#[0-9a-fA-F]{6}$/;
const MAX_CATEGORIES = 3;

// POST /api/admin/auth/vitrine
// Body: { code, nomVitrine, subdomain, logoUrl?, categoriesIds[], couleurAccent }
// - Update tenant : nom, subdomain (le permanent), logoUrl, couleurAccent, modules
// - Set TenantCategorieWari
// - Transition onboardingStep → DONE + codeAcces = null
// - Crée la session JWT pour login auto
// - Envoie email bienvenue best-effort
export async function POST(req: NextRequest) {
  let body: {
    code?: string;
    nomVitrine?: string;
    subdomain?: string;
    logoUrl?: string | null;
    categoriesIds?: string[];
    couleurAccent?: string;
  };
  try {
    body = await req.json();
  } catch {
    return NextResponse.json({ error: "Body invalide" }, { status: 400 });
  }

  const code = normalizeCode(body.code ?? "");
  const nomVitrine = (body.nomVitrine ?? "").trim();
  const subdomain = (body.subdomain ?? "").trim().toLowerCase();
  const logoUrl = body.logoUrl ?? null;
  const categoriesIds = Array.isArray(body.categoriesIds) ? body.categoriesIds : [];
  const couleurAccent = (body.couleurAccent ?? "#C9A227").trim();

  if (!isValidCodeFormat(code)) {
    return NextResponse.json({ error: "Code d'accès invalide" }, { status: 400 });
  }
  if (nomVitrine.length < 2 || nomVitrine.length > 60) {
    return NextResponse.json({ error: "Le nom de vitrine doit faire 2 à 60 caractères" }, { status: 400 });
  }
  if (!SUBDOMAIN_REGEX.test(subdomain) || subdomain.length < 3 || subdomain.length > 30) {
    return NextResponse.json({ error: "Sous-domaine invalide" }, { status: 400 });
  }
  if (!COULEUR_REGEX.test(couleurAccent)) {
    return NextResponse.json({ error: "Couleur invalide (format #RRGGBB)" }, { status: 400 });
  }
  if (categoriesIds.length > MAX_CATEGORIES) {
    return NextResponse.json(
      { error: `Maximum ${MAX_CATEGORIES} catégories` },
      { status: 400 }
    );
  }

  const tenant = await prisma.tenant.findUnique({
    where: { codeAcces: code },
    select: {
      id: true,
      actif: true,
      onboardingStep: true,
      modulesPrevus: true,
      users: {
        where: { isPrimary: true },
        select: { id: true, email: true, username: true, motDePasseHash: true, role: true },
        take: 1,
      },
    },
  });
  if (!tenant) {
    return NextResponse.json({ error: "Code invalide" }, { status: 404 });
  }
  if (!tenant.actif || tenant.onboardingStep === "DONE") {
    return NextResponse.json({ error: "Setup non disponible" }, { status: 422 });
  }

  const primary = tenant.users[0];
  if (!primary || !primary.motDePasseHash) {
    return NextResponse.json(
      { error: "Termine d'abord les étapes email + username + mot de passe" },
      { status: 422 }
    );
  }

  // Vérifier subdomain encore dispo (race condition)
  const subdomainTaken = await prisma.tenant.findFirst({
    where: { subdomain, NOT: { id: tenant.id } },
    select: { id: true },
  });
  if (subdomainTaken) {
    return NextResponse.json(
      { error: "Sous-domaine déjà pris, choisis-en un autre" },
      { status: 409 }
    );
  }

  // Vérifier catégories existent
  if (categoriesIds.length > 0) {
    const found = await prisma.categorieWari.count({
      where: { id: { in: categoriesIds } },
    });
    if (found !== categoriesIds.length) {
      return NextResponse.json({ error: "Catégorie inconnue" }, { status: 400 });
    }
  }

  // Update transactionnel
  await prisma.$transaction(async (tx) => {
    await tx.tenant.update({
      where: { id: tenant.id },
      data: {
        nom: nomVitrine,
        subdomain,
        logoUrl,
        couleurAccent,
        onboardingStep: "DONE",
        codeAcces: null, // consommé
        vitrineActive: true, // BUG-146 — wizard couvre logo+subdomain+catégories+couleur → publié par défaut
      },
    });

    // Reset puis re-insert TenantCategorieWari
    await tx.tenantCategorieWari.deleteMany({ where: { tenantId: tenant.id } });
    if (categoriesIds.length > 0) {
      await tx.tenantCategorieWari.createMany({
        data: categoriesIds.map((catId) => ({ tenantId: tenant.id, categorieWariId: catId })),
      });
    }
  });

  // Crée session JWT (login auto web)
  await createSession(primary.id, primary.role, tenant.id);

  // BUG-138 — Bearer JWT pour le mobile (parité /api/mobile/vitrine/login)
  const bearerToken = await new SignJWT({
    userId: primary.id,
    role: primary.role,
    tenantId: tenant.id,
  })
    .setProtectedHeader({ alg: "HS256" })
    .setExpirationTime("30d")
    .sign(secret);

  // Fetch tenant enrichi pour le mobile (format aligné /api/mobile/vitrine/login)
  const tenantForMobile = await prisma.tenant.findUnique({
    where: { id: tenant.id },
    select: {
      id: true,
      subdomain: true,
      nom: true,
      description: true,
      logoUrl: true,
      banniere: true,
      whatsapp: true,
      ville: true,
      pays: true,
      themeCouleur: true,
      couleurAccent: true,
      vitrineActive: true,
      modeVitrineSiteWeb: true,
      siteWebUrl: true,
      modules: { where: { actif: true }, select: { nom: true } },
    },
  });

  // Email bienvenue best-effort
  try {
    await envoyerBienvenueAdmin({
      email: primary.email,
      tenantNom: nomVitrine,
      subdomain,
      username: primary.username ?? primary.email,
    });
  } catch (e) {
    console.error("Erreur email bienvenue:", e);
  }

  return NextResponse.json({
    ok: true,
    subdomain,
    dashboardUrl: "/admin",
    bearerToken,
    user: {
      id: primary.id,
      email: primary.email,
      nom: primary.username,
      role: primary.role,
      tenantId: tenant.id,