src/app/api/admin/auth/email/route.ts
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.
Concepts détectés — comprends la théorie
ORM Prisma
6 occurrencesCe fichier accède à la base de données via Prisma. Prisma est l'ORM utilisé côté backend pour les requêtes typées sur PostgreSQL.
Voir l'article général
Route API Next.js
3 occurrencesCe fichier est une route API Next.js (App Router). Voir le contrat API complet pour les conventions de réponse et d'auth.
Voir l'article général
2 exports
POSTdynamic
Code source· typescript
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { isValidCodeFormat, normalizeCode } from "@/lib/admin/code-acces";
import { envoyerCodeVerifAdmin } from "@/lib/email";
export const dynamic = "force-dynamic";
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const CODE_TTL_MIN = 15;
function genCode6(): string {
return String(Math.floor(100000 + Math.random() * 900000));
}
// POST /api/admin/auth/email
// Body: { code: "WARI-XXXX-XXXX", email: "..." }
// Crée le User primaire si nécessaire (verified=false, motDePasseHash=null),
// génère un code 6 chiffres, envoie l'email Resend, passe à onboardingStep=VERIF.
export async function POST(req: NextRequest) {
let body: { code?: string; email?: string };
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Body invalide" }, { status: 400 });
}
const code = normalizeCode(body.code ?? "");
const email = (body.email ?? "").trim().toLowerCase();
if (!isValidCodeFormat(code)) {
return NextResponse.json({ error: "Code d'accès invalide" }, { status: 400 });
}
if (!EMAIL_REGEX.test(email)) {
return NextResponse.json({ error: "Adresse email invalide" }, { status: 400 });
}
const tenant = await prisma.tenant.findUnique({
where: { codeAcces: code },
select: {
id: true,
nom: true,
onboardingStep: true,
actif: true,
users: { where: { isPrimary: true }, select: { id: 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 });
}
// Vérifier que cet email n'est pas déjà utilisé par un autre tenant (unique globally)
const emailExistant = await prisma.user.findFirst({
where: { email, NOT: { tenantId: tenant.id } },
select: { id: true },
});
if (emailExistant) {
return NextResponse.json(
{ error: "Cet email est déjà utilisé par un autre compte wari.pro." },
{ status: 409 }
);
}
const code6 = genCode6();
const expiry = new Date(Date.now() + CODE_TTL_MIN * 60 * 1000);
// Crée ou met à jour le User primaire
const primary = tenant.users[0];
if (primary) {
await prisma.user.update({
where: { id: primary.id },
data: {
email,
emailVerifiedAt: null, // réinitialise si changement d'email
resetToken: code6,
resetTokenExpiry: expiry,
},
});
} else {
await prisma.user.create({
data: {
tenantId: tenant.id,
email,
role: "TENANT_ADMIN",
isPrimary: true,
resetToken: code6,
resetTokenExpiry: expiry,
},
});
}
await prisma.tenant.update({
where: { id: tenant.id },
data: { onboardingStep: "VERIF" },
});
// Envoi email best-effort (ne pas bloquer si Resend down)
try {
await envoyerCodeVerifAdmin({
email,
code: code6,
tenantNom: tenant.nom,
expiresInMinutes: CODE_TTL_MIN,
});
} catch (e) {
console.error("Erreur envoi code email verif:", e);
}
return NextResponse.json({ ok: true, expiresInMinutes: CODE_TTL_MIN });
}
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { isValidCodeFormat, normalizeCode } from "@/lib/admin/code-acces";
import { envoyerCodeVerifAdmin } from "@/lib/email";
export const dynamic = "force-dynamic";
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const CODE_TTL_MIN = 15;
function genCode6(): string {
return String(Math.floor(100000 + Math.random() * 900000));
}
// POST /api/admin/auth/email
// Body: { code: "WARI-XXXX-XXXX", email: "..." }
// Crée le User primaire si nécessaire (verified=false, motDePasseHash=null),
// génère un code 6 chiffres, envoie l'email Resend, passe à onboardingStep=VERIF.
export async function POST(req: NextRequest) {
let body: { code?: string; email?: string };
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Body invalide" }, { status: 400 });
}
const code = normalizeCode(body.code ?? "");
const email = (body.email ?? "").trim().toLowerCase();
if (!isValidCodeFormat(code)) {
return NextResponse.json({ error: "Code d'accès invalide" }, { status: 400 });
}
if (!EMAIL_REGEX.test(email)) {
return NextResponse.json({ error: "Adresse email invalide" }, { status: 400 });
}
const tenant = await prisma.tenant.findUnique({
where: { codeAcces: code },
select: {
id: true,
nom: true,
onboardingStep: true,
actif: true,
users: { where: { isPrimary: true }, select: { id: 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 });
}
// Vérifier que cet email n'est pas déjà utilisé par un autre tenant (unique globally)
const emailExistant = await prisma.user.findFirst({
where: { email, NOT: { tenantId: tenant.id } },
select: { id: true },
});
if (emailExistant) {
return NextResponse.json(
{ error: "Cet email est déjà utilisé par un autre compte wari.pro." },
{ status: 409 }
);
}
const code6 = genCode6();
const expiry = new Date(Date.now() + CODE_TTL_MIN * 60 * 1000);
// Crée ou met à jour le User primaire
const primary = tenant.users[0];
if (primary) {
await prisma.user.update({
where: { id: primary.id },
data: {
email,
emailVerifiedAt: null, // réinitialise si changement d'email
resetToken: code6,
resetTokenExpiry: expiry,
},
});
} else {
await prisma.user.create({
data: {
tenantId: tenant.id,
email,
role: "TENANT_ADMIN",
isPrimary: true,
resetToken: code6,
resetTokenExpiry: expiry,
},
});
}
await prisma.tenant.update({
where: { id: tenant.id },
data: { onboardingStep: "VERIF" },
});
// Envoi email best-effort (ne pas bloquer si Resend down)
try {
await envoyerCodeVerifAdmin({
email,
code: code6,
tenantNom: tenant.nom,
expiresInMinutes: CODE_TTL_MIN,
});
} catch (e) {
console.error("Erreur envoi code email verif:", e);
}
return NextResponse.json({ ok: true, expiresInMinutes: CODE_TTL_MIN });
}