- Replace pdf2pic/GraphicsMagick with pdfjs-dist + @napi-rs/canvas for Vercel-compatible PDF rasterization - Replace MinIO with Supabase Storage (S3-compatible); rename minio.ts to storage.ts and update all imports - Replace in-memory job queue with Upstash QStash; upload route now persists files to storage before enqueuing, /api/jobs/process handles the QStash callback - Convert email watcher from persistent IMAP connection to stateless scanInbox() polled by Vercel Cron every 2 minutes - Add FTP watcher (basic-ftp) with cron polling for scanner integration via Dreamhost FTP drop directory - Add FTP config fields to AppSettings schema - Remove folder watcher (chokidar), standalone output, Docker-only code - Update next.config.ts, middleware, instrumentation for serverless - Add vercel.json with cron schedules for email and FTP polling - Add migration scripts for database (pg_dump/restore) and storage (S3-to-S3 copy) with verification Made-with: Cursor
81 lines
1.8 KiB
TypeScript
81 lines
1.8 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { getToken } from "next-auth/jwt";
|
|
|
|
const publicPaths = [
|
|
"/login",
|
|
"/signup",
|
|
"/setup",
|
|
"/api/auth",
|
|
"/api/health",
|
|
"/api/setup",
|
|
"/api/onboarding",
|
|
"/api/jobs/process",
|
|
"/api/email-watch/poll",
|
|
"/api/ftp-watch/poll",
|
|
];
|
|
|
|
const onboardingExemptPaths = [
|
|
"/onboarding",
|
|
"/api/onboarding",
|
|
"/api/auth",
|
|
];
|
|
|
|
function isPublic(pathname: string) {
|
|
return publicPaths.some(
|
|
(p) => pathname === p || pathname.startsWith(p + "/")
|
|
);
|
|
}
|
|
|
|
function isOnboardingExempt(pathname: string) {
|
|
return onboardingExemptPaths.some(
|
|
(p) => pathname === p || pathname.startsWith(p + "/")
|
|
);
|
|
}
|
|
|
|
export async function middleware(req: NextRequest) {
|
|
const { pathname } = req.nextUrl;
|
|
|
|
if (
|
|
pathname.startsWith("/_next") ||
|
|
pathname.startsWith("/favicon") ||
|
|
pathname.includes(".")
|
|
) {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
if (isPublic(pathname)) {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
const secureCookie =
|
|
req.headers.get("x-forwarded-proto") === "https" ||
|
|
req.nextUrl.protocol === "https:";
|
|
const token = await getToken({
|
|
req,
|
|
secret: process.env.AUTH_SECRET,
|
|
secureCookie,
|
|
});
|
|
|
|
if (!token) {
|
|
const loginUrl = new URL("/login", req.url);
|
|
loginUrl.searchParams.set("callbackUrl", pathname);
|
|
return NextResponse.redirect(loginUrl);
|
|
}
|
|
|
|
const hasOrg = !!token.orgId;
|
|
const onboardingDone = token.onboardingComplete === true;
|
|
|
|
if (!hasOrg && !isOnboardingExempt(pathname)) {
|
|
return NextResponse.redirect(new URL("/onboarding", req.url));
|
|
}
|
|
|
|
if (hasOrg && !onboardingDone && !isOnboardingExempt(pathname)) {
|
|
return NextResponse.redirect(new URL("/onboarding", req.url));
|
|
}
|
|
|
|
return NextResponse.next();
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
};
|