src/app/api/auth/client/verify/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
7 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
GET
Code source· typescript
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { createHash } from "crypto";
import { createSession } from "@/lib/auth/session";
import { fusionnerPanierEtWishlist } from "@/lib/panier/fusion";
export async function GET(req: NextRequest) {
try {
const rawToken = req.nextUrl.searchParams.get("token");
const source = req.nextUrl.searchParams.get("source");
if (!rawToken) {
return NextResponse.json({ error: "Token manquant" }, { status: 400 });
}
const hashedToken = createHash("sha256").update(rawToken).digest("hex");
const magicToken = await prisma.magicToken.findUnique({
where: { token: hashedToken },
include: { client: true },
});
if (!magicToken) {
return NextResponse.json({ error: "Token invalide" }, { status: 401 });
}
if (magicToken.usedAt) {
return NextResponse.json({ error: "Token deja utilise" }, { status: 401 });
}
if (new Date() > magicToken.expiresAt) {
return NextResponse.json({ error: "Token expire" }, { status: 401 });
}
// Source mobile — ne PAS consommer le token ici.
// Le token sera consommé par POST /api/mobile/auth côté app native.
// Redirige vers le deep link wari:// avec token + identifier.
if (source === "mobile") {
const identifier = encodeURIComponent(magicToken.identifier);
return NextResponse.redirect(
`wari://auth/verify?token=${rawToken}&identifier=${identifier}`
);
}
const isNewClient = !magicToken.client;
let client = magicToken.client;
if (!client) {
client = await prisma.clientAccount.create({
data: {
phone: magicToken.identifierType === "phone" ? magicToken.identifier : null,
email: magicToken.identifierType === "email" ? magicToken.identifier : null,
originTenantId: magicToken.tenantId === "wari" ? null : magicToken.tenantId,
},
});
}
await prisma.magicToken.update({
where: { id: magicToken.id },
data: { usedAt: new Date(), clientId: client.id },
});
await prisma.clientAccount.update({
where: { id: client.id },
data: { lastSeenAt: new Date() },
});
// Fusion panier/wishlist anonyme → compte client
const sessionId = req.cookies.get("panier_session")?.value;
if (sessionId && magicToken.tenantId !== "wari") {
await fusionnerPanierEtWishlist(sessionId, client.id, magicToken.tenantId);
}
await createSession(client.id, "CLIENT", magicToken.tenantId);
const baseUrl = process.env.MAGIC_LINK_BASE_URL || "http://localhost:3000";
const tenantId = magicToken.tenantId;
const newEmail = req.nextUrl.searchParams.get("newEmail");
const invitationToken = req.nextUrl.searchParams.get("invitationToken");
// Vérification email
if (source === "verify-email") {
await prisma.clientAccount.update({
where: { id: client.id },
data: { emailVerifiedAt: new Date() },
});
}
// Changement email
if (source === "change-email" && newEmail) {
await prisma.clientAccount.update({
where: { id: client.id },
data: { email: newEmail, emailVerifiedAt: new Date() },
});
}
const tenantBaseUrl2 = tenantId === "wari" ? baseUrl : baseUrl.replace("https://", "https://" + tenantId + ".");
if (isNewClient) return NextResponse.redirect(tenantBaseUrl2 + "/auth/client/profil" + (tenantId === "wari" ? "?tenant=wari" : "?tenant=" + tenantId));
if (source === "reset") return NextResponse.redirect(tenantBaseUrl2 + "/auth/client/nouveau-mdp" + (tenantId === "wari" ? "?tenant=wari" : ""));
if (source === "verify-email" || source === "change-email") return NextResponse.redirect(tenantBaseUrl2 + "/compte?verified=1");
if (source === "invitation" && invitationToken) {
return NextResponse.redirect(tenantBaseUrl2 + "/invitation/" + tenantId + "?token=" + invitationToken);
}
return NextResponse.redirect(tenantBaseUrl2);
} catch (error) {
console.error("verify error:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { createHash } from "crypto";
import { createSession } from "@/lib/auth/session";
import { fusionnerPanierEtWishlist } from "@/lib/panier/fusion";
export async function GET(req: NextRequest) {
try {
const rawToken = req.nextUrl.searchParams.get("token");
const source = req.nextUrl.searchParams.get("source");
if (!rawToken) {
return NextResponse.json({ error: "Token manquant" }, { status: 400 });
}
const hashedToken = createHash("sha256").update(rawToken).digest("hex");
const magicToken = await prisma.magicToken.findUnique({
where: { token: hashedToken },
include: { client: true },
});
if (!magicToken) {
return NextResponse.json({ error: "Token invalide" }, { status: 401 });
}
if (magicToken.usedAt) {
return NextResponse.json({ error: "Token deja utilise" }, { status: 401 });
}
if (new Date() > magicToken.expiresAt) {
return NextResponse.json({ error: "Token expire" }, { status: 401 });
}
// Source mobile — ne PAS consommer le token ici.
// Le token sera consommé par POST /api/mobile/auth côté app native.
// Redirige vers le deep link wari:// avec token + identifier.
if (source === "mobile") {
const identifier = encodeURIComponent(magicToken.identifier);
return NextResponse.redirect(
`wari://auth/verify?token=${rawToken}&identifier=${identifier}`
);
}
const isNewClient = !magicToken.client;
let client = magicToken.client;
if (!client) {
client = await prisma.clientAccount.create({
data: {
phone: magicToken.identifierType === "phone" ? magicToken.identifier : null,
email: magicToken.identifierType === "email" ? magicToken.identifier : null,
originTenantId: magicToken.tenantId === "wari" ? null : magicToken.tenantId,
},
});
}
await prisma.magicToken.update({
where: { id: magicToken.id },
data: { usedAt: new Date(), clientId: client.id },
});
await prisma.clientAccount.update({
where: { id: client.id },
data: { lastSeenAt: new Date() },
});
// Fusion panier/wishlist anonyme → compte client
const sessionId = req.cookies.get("panier_session")?.value;
if (sessionId && magicToken.tenantId !== "wari") {
await fusionnerPanierEtWishlist(sessionId, client.id, magicToken.tenantId);
}
await createSession(client.id, "CLIENT", magicToken.tenantId);
const baseUrl = process.env.MAGIC_LINK_BASE_URL || "http://localhost:3000";
const tenantId = magicToken.tenantId;
const newEmail = req.nextUrl.searchParams.get("newEmail");
const invitationToken = req.nextUrl.searchParams.get("invitationToken");
// Vérification email
if (source === "verify-email") {
await prisma.clientAccount.update({
where: { id: client.id },
data: { emailVerifiedAt: new Date() },
});
}
// Changement email
if (source === "change-email" && newEmail) {
await prisma.clientAccount.update({
where: { id: client.id },
data: { email: newEmail, emailVerifiedAt: new Date() },
});
}
const tenantBaseUrl2 = tenantId === "wari" ? baseUrl : baseUrl.replace("https://", "https://" + tenantId + ".");
if (isNewClient) return NextResponse.redirect(tenantBaseUrl2 + "/auth/client/profil" + (tenantId === "wari" ? "?tenant=wari" : "?tenant=" + tenantId));
if (source === "reset") return NextResponse.redirect(tenantBaseUrl2 + "/auth/client/nouveau-mdp" + (tenantId === "wari" ? "?tenant=wari" : ""));
if (source === "verify-email" || source === "change-email") return NextResponse.redirect(tenantBaseUrl2 + "/compte?verified=1");
if (source === "invitation" && invitationToken) {
return NextResponse.redirect(tenantBaseUrl2 + "/invitation/" + tenantId + "?token=" + invitationToken);
}
return NextResponse.redirect(tenantBaseUrl2);
} catch (error) {
console.error("verify error:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}