diff --git a/package.json b/package.json index a65af32..1b20c29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.3.87", + "version": "0.3.88", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js index 3a7673d..e7b8601 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js @@ -121,8 +121,8 @@ export default function APIPageClient({ machineId }) { // Ping once to verify reachable const healthUrl = `${tPublicUrl || tUrl}/api/health`; try { - const ping = await fetch(healthUrl, { mode: "no-cors", cache: "no-store" }); - if (ping.ok || ping.type === "opaque") { + const ping = await fetch(healthUrl, { cache: "no-store" }); + if (ping.ok) { setTunnelEnabled(true); } else { pingTunnelHealth(tPublicUrl || tUrl); @@ -769,7 +769,7 @@ export default function APIPageClient({ machineId }) { />

Allow dashboard access via tunnel

- +
)} diff --git a/src/app/api/auth/login/route.js b/src/app/api/auth/login/route.js index eda29f8..c8bee2d 100644 --- a/src/app/api/auth/login/route.js +++ b/src/app/api/auth/login/route.js @@ -8,11 +8,23 @@ const SECRET = new TextEncoder().encode( process.env.JWT_SECRET || "9router-default-secret-change-me" ); +function isTunnelRequest(request, settings) { + 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() : ""; + return (tunnelHost && host === tunnelHost) || (tailscaleHost && host === tailscaleHost); +} + export async function POST(request) { try { const { password } = await request.json(); const settings = await getSettings(); + // Block login via tunnel/tailscale if dashboard access is disabled + if (isTunnelRequest(request, settings) && settings.tunnelDashboardAccess !== true) { + return NextResponse.json({ error: "Dashboard access via tunnel is disabled" }, { status: 403 }); + } + // Default password is '123456' if not set const storedHash = settings.password; diff --git a/src/app/api/settings/require-login/route.js b/src/app/api/settings/require-login/route.js index 08f0661..92b39f2 100644 --- a/src/app/api/settings/require-login/route.js +++ b/src/app/api/settings/require-login/route.js @@ -6,7 +6,9 @@ export async function GET() { const settings = await getSettings(); const requireLogin = settings.requireLogin !== false; const tunnelDashboardAccess = settings.tunnelDashboardAccess === true; - return NextResponse.json({ requireLogin, tunnelDashboardAccess }); + const tunnelUrl = settings.tunnelUrl || ""; + const tailscaleUrl = settings.tailscaleUrl || ""; + return NextResponse.json({ requireLogin, tunnelDashboardAccess, tunnelUrl, tailscaleUrl }); } catch (error) { return NextResponse.json({ requireLogin: true }, { status: 200 }); } diff --git a/src/dashboardGuard.js b/src/dashboardGuard.js index 15b2c66..f8bf963 100644 --- a/src/dashboardGuard.js +++ b/src/dashboardGuard.js @@ -81,15 +81,20 @@ export async function proxy(request) { const data = await res.json(); requireLogin = data.requireLogin !== false; tunnelDashboardAccess = data.tunnelDashboardAccess === true; + + // Block tunnel/tailscale access if disabled (redirect to login) + if (!tunnelDashboardAccess) { + const host = (request.headers.get("host") || "").split(":")[0].toLowerCase(); + const tunnelHost = data.tunnelUrl ? new URL(data.tunnelUrl).hostname.toLowerCase() : ""; + const tailscaleHost = data.tailscaleUrl ? new URL(data.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) } - // Block tunnel access if disabled (checked before token to enforce the setting) - if (!isLocalRequest(request) && !tunnelDashboardAccess) { - return NextResponse.redirect(new URL("/login", request.url)); - } - // If login not required, allow through if (!requireLogin) return NextResponse.next();