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();