src/proxy.ts
Annotation
Ce fichier `proxy.ts` implémente un middleware Next.js qui gère le routage et l'authentification basés sur les sous-domaines et les chemins d'URL. Il extrait le sous-domaine pour identifier le tenant, puis applique des règles de redirection et de vérification de session pour les routes `/admin` et `/superadmin`. La fonction `proxy` est le point d'entrée principal, et `config` définit les chemins sur lesquels ce middleware doit s'exécuter. Ce middleware est branché dans le fichier `middleware.ts` à la racine du projet Next.js.
Concepts détectés — comprends la théorie
JWT / Auth backend
2 occurrencesCe fichier touche au système d'authentification (JWT, session, getSessionFromRequest). Voir le contrat API pour la logique complète.
Voir l'article général
Route API Next.js
1 occurrenceCe 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
Code source· typescript
import { NextRequest, NextResponse } from "next/server";
import { jwtVerify } from "jose";
const NEXTAUTH_SECRET = process.env.NEXTAUTH_SECRET;
if (!NEXTAUTH_SECRET) {
throw new Error("NEXTAUTH_SECRET is required");
}
const secret = new TextEncoder().encode(NEXTAUTH_SECRET);
export async function proxy(request: NextRequest) {
const hostname = request.headers.get("host") || "";
const url = request.nextUrl.clone();
const baseDomain = process.env.DOMAIN || "localhost";
// Extraire le sous-domaine
let subdomain: string | null = null;
if (hostname.endsWith("." + baseDomain)) {
const sub = hostname.split(".")[0];
if (sub !== "staging" && sub !== "www") subdomain = sub;
}
if (!subdomain) subdomain = url.searchParams.get("tenant");
// Admin accessible uniquement depuis wari.pro (sans sous-domaine)
if (url.pathname.startsWith("/admin") && subdomain) {
return NextResponse.redirect(new URL("https://" + (process.env.DOMAIN || "localhost") + "/admin/login", request.url));
}
// Routes admin publiques (pas d'auth requise)
const isPublicAdmin =
url.pathname.startsWith("/admin/login") ||
url.pathname.startsWith("/admin/setup") ||
url.pathname.startsWith("/admin/reset-mdp");
// Vérification session admin tenant
if (url.pathname.startsWith("/admin") && !isPublicAdmin) {
const token = request.cookies.get("superapp_session")?.value;
if (!token) return NextResponse.redirect(new URL("/admin/login", request.url));
try {
const { payload } = await jwtVerify(token, secret);
const session = payload as { role: string; tenantId: string | null };
if (session.role !== "TENANT_ADMIN" || !session.tenantId) {
return NextResponse.redirect(new URL("/admin/login", request.url));
}
const requestHeaders = new Headers(request.headers);
if (subdomain) requestHeaders.set("x-tenant-subdomain", subdomain);
requestHeaders.set("x-session-tenant-id", session.tenantId);
requestHeaders.set("x-pathname", url.pathname);
return NextResponse.next({ request: { headers: requestHeaders } });
} catch {
return NextResponse.redirect(new URL("/admin/login", request.url));
}
}
if (url.pathname.startsWith("/superadmin")) {
// Inject x-pathname pour que le layout superadmin sache exclure le shell
// sur /superadmin/login. Auth gérée par le layout server component.
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-pathname", url.pathname);
return NextResponse.next({ request: { headers: requestHeaders } });
}
if (url.pathname.startsWith("/admin")) {
// Pour /admin/login + /admin/reset-mdp : injecter x-pathname pour que le
// layout admin sache exclure le shell.
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-pathname", url.pathname);
return NextResponse.next({ request: { headers: requestHeaders } });
}
if (url.pathname.startsWith("/api")) return NextResponse.next();
if (url.pathname.startsWith("/auth")) return NextResponse.next();
if (subdomain) {
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-tenant-subdomain", subdomain);
const preview = url.searchParams.get("preview");
if (preview) requestHeaders.set("x-preview", "true");
return NextResponse.next({ request: { headers: requestHeaders } });
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
import { NextRequest, NextResponse } from "next/server";
import { jwtVerify } from "jose";
const NEXTAUTH_SECRET = process.env.NEXTAUTH_SECRET;
if (!NEXTAUTH_SECRET) {
throw new Error("NEXTAUTH_SECRET is required");
}
const secret = new TextEncoder().encode(NEXTAUTH_SECRET);
export async function proxy(request: NextRequest) {
const hostname = request.headers.get("host") || "";
const url = request.nextUrl.clone();
const baseDomain = process.env.DOMAIN || "localhost";
// Extraire le sous-domaine
let subdomain: string | null = null;
if (hostname.endsWith("." + baseDomain)) {
const sub = hostname.split(".")[0];
if (sub !== "staging" && sub !== "www") subdomain = sub;
}
if (!subdomain) subdomain = url.searchParams.get("tenant");
// Admin accessible uniquement depuis wari.pro (sans sous-domaine)
if (url.pathname.startsWith("/admin") && subdomain) {
return NextResponse.redirect(new URL("https://" + (process.env.DOMAIN || "localhost") + "/admin/login", request.url));
}
// Routes admin publiques (pas d'auth requise)
const isPublicAdmin =
url.pathname.startsWith("/admin/login") ||
url.pathname.startsWith("/admin/setup") ||
url.pathname.startsWith("/admin/reset-mdp");
// Vérification session admin tenant
if (url.pathname.startsWith("/admin") && !isPublicAdmin) {
const token = request.cookies.get("superapp_session")?.value;
if (!token) return NextResponse.redirect(new URL("/admin/login", request.url));
try {
const { payload } = await jwtVerify(token, secret);
const session = payload as { role: string; tenantId: string | null };
if (session.role !== "TENANT_ADMIN" || !session.tenantId) {
return NextResponse.redirect(new URL("/admin/login", request.url));
}
const requestHeaders = new Headers(request.headers);
if (subdomain) requestHeaders.set("x-tenant-subdomain", subdomain);
requestHeaders.set("x-session-tenant-id", session.tenantId);
requestHeaders.set("x-pathname", url.pathname);
return NextResponse.next({ request: { headers: requestHeaders } });
} catch {
return NextResponse.redirect(new URL("/admin/login", request.url));
}
}
if (url.pathname.startsWith("/superadmin")) {
// Inject x-pathname pour que le layout superadmin sache exclure le shell
// sur /superadmin/login. Auth gérée par le layout server component.
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-pathname", url.pathname);
return NextResponse.next({ request: { headers: requestHeaders } });
}
if (url.pathname.startsWith("/admin")) {
// Pour /admin/login + /admin/reset-mdp : injecter x-pathname pour que le
// layout admin sache exclure le shell.
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-pathname", url.pathname);
return NextResponse.next({ request: { headers: requestHeaders } });
}
if (url.pathname.startsWith("/api")) return NextResponse.next();
if (url.pathname.startsWith("/auth")) return NextResponse.next();
if (subdomain) {
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-tenant-subdomain", subdomain);
const preview = url.searchParams.get("preview");
if (preview) requestHeaders.set("x-preview", "true");
return NextResponse.next({ request: { headers: requestHeaders } });
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};