mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
137 lines
4.2 KiB
JavaScript
137 lines
4.2 KiB
JavaScript
import { NextResponse } from "next/server";
|
|
import { jwtVerify } from "jose";
|
|
import { getSettings } from "@/lib/localDb";
|
|
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
|
|
|
const SECRET = new TextEncoder().encode(
|
|
process.env.JWT_SECRET || "9router-default-secret-change-me"
|
|
);
|
|
|
|
const CLI_TOKEN_HEADER = "x-9r-cli-token";
|
|
const CLI_TOKEN_SALT = "9r-cli-auth";
|
|
|
|
let cachedCliToken = null;
|
|
async function getCliToken() {
|
|
if (!cachedCliToken) cachedCliToken = await getConsistentMachineId(CLI_TOKEN_SALT);
|
|
return cachedCliToken;
|
|
}
|
|
|
|
async function hasValidCliToken(request) {
|
|
const token = request.headers.get(CLI_TOKEN_HEADER);
|
|
if (!token) return false;
|
|
return token === await getCliToken();
|
|
}
|
|
|
|
// Always require JWT token regardless of requireLogin setting
|
|
const ALWAYS_PROTECTED = [
|
|
"/api/shutdown",
|
|
"/api/settings/database",
|
|
];
|
|
|
|
// Require auth, but allow through if requireLogin is disabled
|
|
const PROTECTED_API_PATHS = [
|
|
"/api/settings",
|
|
"/api/keys",
|
|
"/api/providers/client",
|
|
"/api/provider-nodes/validate",
|
|
];
|
|
|
|
async function hasValidToken(request) {
|
|
const token = request.cookies.get("auth_token")?.value;
|
|
if (!token) return false;
|
|
try {
|
|
await jwtVerify(token, SECRET);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Read settings directly from DB to avoid self-fetch deadlock in proxy
|
|
async function loadSettings() {
|
|
try {
|
|
return await getSettings();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function isAuthenticated(request) {
|
|
if (await hasValidToken(request)) return true;
|
|
const settings = await loadSettings();
|
|
if (settings && settings.requireLogin === false) return true;
|
|
return false;
|
|
}
|
|
|
|
export async function proxy(request) {
|
|
const { pathname } = request.nextUrl;
|
|
|
|
// Always protected - require valid JWT or local CLI token (machineId-based)
|
|
if (ALWAYS_PROTECTED.some((p) => pathname.startsWith(p))) {
|
|
if (await hasValidCliToken(request) || await hasValidToken(request))
|
|
return NextResponse.next();
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
// Protect sensitive API endpoints (allow CLI token, JWT, or requireLogin=false)
|
|
if (PROTECTED_API_PATHS.some((p) => pathname.startsWith(p))) {
|
|
if (pathname === "/api/settings/require-login") return NextResponse.next();
|
|
if (await hasValidCliToken(request) || await isAuthenticated(request))
|
|
return NextResponse.next();
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
// Protect all dashboard routes
|
|
if (pathname.startsWith("/dashboard")) {
|
|
let requireLogin = true;
|
|
let tunnelDashboardAccess = true;
|
|
|
|
try {
|
|
const settings = await loadSettings();
|
|
if (settings) {
|
|
requireLogin = settings.requireLogin !== false;
|
|
tunnelDashboardAccess = settings.tunnelDashboardAccess === true;
|
|
|
|
// Block tunnel/tailscale access if disabled (redirect to login)
|
|
if (!tunnelDashboardAccess) {
|
|
const host = (request.headers.get("host") || "").split(":")[0].toLowerCase();
|
|
const tunnelHost = settings.tunnelUrl ? new URL(settings.tunnelUrl).hostname.toLowerCase() : "";
|
|
const tailscaleHost = settings.tailscaleUrl ? new URL(settings.tailscaleUrl).hostname.toLowerCase() : "";
|
|
if ((tunnelHost && host === tunnelHost) || (tailscaleHost && host === tailscaleHost)) {
|
|
return NextResponse.redirect(new URL("/login", request.url));
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// On error, keep defaults (require login, block tunnel)
|
|
}
|
|
|
|
// If login not required, allow through
|
|
if (!requireLogin) return NextResponse.next();
|
|
|
|
// Verify JWT token
|
|
const token = request.cookies.get("auth_token")?.value;
|
|
if (token) {
|
|
try {
|
|
await jwtVerify(token, SECRET);
|
|
return NextResponse.next();
|
|
} catch {
|
|
return NextResponse.redirect(new URL("/login", request.url));
|
|
}
|
|
}
|
|
|
|
return NextResponse.redirect(new URL("/login", request.url));
|
|
}
|
|
|
|
// Redirect / to /dashboard if logged in, or /dashboard if it's the root
|
|
if (pathname === "/") {
|
|
return NextResponse.redirect(new URL("/dashboard", request.url));
|
|
}
|
|
|
|
return NextResponse.next();
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ["/", "/dashboard/:path*"],
|
|
};
|