src/app/api/auth/client/verifier-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
5 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
1 export
POST
Code source· typescript
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { getSession } from "@/lib/auth/session";
import { createHash, randomUUID } from "crypto";
import { envoyerMagicLink } from "@/lib/email";
const TTL = parseInt(process.env.MAGIC_LINK_TTL_SECONDS || "900");
const BASE_URL = process.env.MAGIC_LINK_BASE_URL || "http://localhost:3000";
export async function POST(req: NextRequest) {
try {
const session = await getSession();
if (!session || session.role !== "CLIENT") {
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
}
const { newEmail, tenantId } = await req.json();
const client = await prisma.clientAccount.findUnique({
where: { id: session.userId },
select: { email: true, name: true, prenom: true },
});
if (!client) return NextResponse.json({ error: "Client introuvable" }, { status: 404 });
const emailCible = newEmail ?? client.email;
if (!emailCible) return NextResponse.json({ error: "Aucun email à vérifier" }, { status: 400 });
// Vérifier que le nouvel email n'est pas déjà pris
if (newEmail && newEmail !== client.email) {
const existant = await prisma.clientAccount.findFirst({ where: { email: newEmail } });
if (existant) return NextResponse.json({ error: "Cet email est déjà utilisé" }, { status: 409 });
}
const rawToken = randomUUID();
const hashedToken = createHash("sha256").update(rawToken).digest("hex");
const expiresAt = new Date(Date.now() + TTL * 1000);
await prisma.magicToken.create({
data: {
token: hashedToken,
clientId: session.userId,
tenantId: tenantId ?? "wari",
identifier: emailCible,
identifierType: "email",
expiresAt,
},
});
const source = newEmail ? "change-email" : "verify-email";
const newEmailParam = newEmail ? "&newEmail=" + encodeURIComponent(newEmail) : "";
const tenantBaseUrl = !tenantId || tenantId === "wari" ? BASE_URL : BASE_URL.replace("https://", "https://" + tenantId + ".");
const magicLink = tenantBaseUrl + "/auth/client/verify?token=" + rawToken + "&source=" + source + newEmailParam;
const tenant = await prisma.tenant.findUnique({ where: { subdomain: tenantId ?? "" }, select: { nom: true } });
const tenantNom = tenant?.nom ?? "wari.pro";
await envoyerMagicLink({ identifier: emailCible, magicLink, tenantNom, expiresInMinutes: Math.round(TTL / 60) });
return NextResponse.json({ success: true });
} catch (error) {
console.error("verifier-email error:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { getSession } from "@/lib/auth/session";
import { createHash, randomUUID } from "crypto";
import { envoyerMagicLink } from "@/lib/email";
const TTL = parseInt(process.env.MAGIC_LINK_TTL_SECONDS || "900");
const BASE_URL = process.env.MAGIC_LINK_BASE_URL || "http://localhost:3000";
export async function POST(req: NextRequest) {
try {
const session = await getSession();
if (!session || session.role !== "CLIENT") {
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
}
const { newEmail, tenantId } = await req.json();
const client = await prisma.clientAccount.findUnique({
where: { id: session.userId },
select: { email: true, name: true, prenom: true },
});
if (!client) return NextResponse.json({ error: "Client introuvable" }, { status: 404 });
const emailCible = newEmail ?? client.email;
if (!emailCible) return NextResponse.json({ error: "Aucun email à vérifier" }, { status: 400 });
// Vérifier que le nouvel email n'est pas déjà pris
if (newEmail && newEmail !== client.email) {
const existant = await prisma.clientAccount.findFirst({ where: { email: newEmail } });
if (existant) return NextResponse.json({ error: "Cet email est déjà utilisé" }, { status: 409 });
}
const rawToken = randomUUID();
const hashedToken = createHash("sha256").update(rawToken).digest("hex");
const expiresAt = new Date(Date.now() + TTL * 1000);
await prisma.magicToken.create({
data: {
token: hashedToken,
clientId: session.userId,
tenantId: tenantId ?? "wari",
identifier: emailCible,
identifierType: "email",
expiresAt,
},
});
const source = newEmail ? "change-email" : "verify-email";
const newEmailParam = newEmail ? "&newEmail=" + encodeURIComponent(newEmail) : "";
const tenantBaseUrl = !tenantId || tenantId === "wari" ? BASE_URL : BASE_URL.replace("https://", "https://" + tenantId + ".");
const magicLink = tenantBaseUrl + "/auth/client/verify?token=" + rawToken + "&source=" + source + newEmailParam;
const tenant = await prisma.tenant.findUnique({ where: { subdomain: tenantId ?? "" }, select: { nom: true } });
const tenantNom = tenant?.nom ?? "wari.pro";
await envoyerMagicLink({ identifier: emailCible, magicLink, tenantNom, expiresInMinutes: Math.round(TTL / 60) });
return NextResponse.json({ success: true });
} catch (error) {
console.error("verifier-email error:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}