From a967731461c84647d6b0a56bbf8ff3a4efdbf8ab Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Sun, 8 Mar 2026 17:18:28 -0400 Subject: [PATCH] feat: add a temp no access page --- app/layouts/shell.tsx | 228 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 217 insertions(+), 11 deletions(-) diff --git a/app/layouts/shell.tsx b/app/layouts/shell.tsx index 1eb7d85..76b4240 100644 --- a/app/layouts/shell.tsx +++ b/app/layouts/shell.tsx @@ -1,10 +1,15 @@ +import { Icon } from "@iconify/react"; import { Form, Outlet, redirect } from "react-router"; import Button from "~/components/Button"; import Card from "~/components/Card"; import Footer from "~/components/Footer"; import Header from "~/components/Header"; +import Link from "~/components/Link"; +import Options from "~/components/Options"; import { Capabilities } from "~/server/web/roles"; +import toast from "~/utils/toast"; +import { getUserDisplayName } from "~/utils/user"; import { Route } from "./+types/shell"; @@ -26,6 +31,7 @@ export async function loader({ request, context }: Route.LoaderArgs) { const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); const api = context.hsApi.getRuntimeClient(apiKey); const check = context.auth.can(principal, Capabilities.ui_access); + const noAccess = !check && principal.kind === "oidc"; const user = principal.kind === "oidc" @@ -38,6 +44,43 @@ export async function loader({ request, context }: Route.LoaderArgs) { } : { subject: "api_key", name: principal.displayName }; + let linkedUserName: string | undefined; + let osValue: string | undefined; + + if (noAccess && principal.kind === "oidc") { + const hsUserId = principal.user.headscaleUserId; + if (hsUserId) { + try { + const apiUsers = await api.getUsers(); + const hsUser = apiUsers.find((u) => u.id === hsUserId); + linkedUserName = hsUser ? getUserDisplayName(hsUser) : undefined; + } catch { + // API unavailable, skip linked user resolution + } + } + + const userAgent = request.headers.get("user-agent"); + const os = userAgent?.match(/(Linux|Windows|Mac OS X|iPhone|iPad|Android)/); + switch (os?.[0]) { + case "Windows": + osValue = "windows"; + break; + case "Mac OS X": + osValue = "macos"; + break; + case "iPhone": + case "iPad": + osValue = "ios"; + break; + case "Android": + osValue = "android"; + break; + default: + osValue = "linux"; + break; + } + } + return { config: context.hs.c, url: context.config.headscale.public_url ?? context.config.headscale.url, @@ -53,7 +96,9 @@ export async function loader({ request, context }: Route.LoaderArgs) { settings: context.auth.can(principal, Capabilities.read_feature), }, onboarding: request.url.endsWith("/onboarding"), - noAccess: !check && principal.kind === "oidc", + noAccess, + linkedUserName, + osValue, healthy: await api.isHealthy(), }; } catch { @@ -71,18 +116,179 @@ export default function Shell({ loaderData }: Route.ComponentProps) { <>
-
- - No Access +
+ {loaderData.linkedUserName ? ( + +

+ ✓ Your account is linked to Headscale user{" "} + {loaderData.linkedUserName}. +

+
+ ) : undefined} + + + Access your network +
+ via Tailscale +
- Your account doesn't have permission to access the dashboard. Contact an - administrator if you need access. + You don't have dashboard access, but you can still connect to your Headscale + network. Install Tailscale on your device to get started. -
- -
+ + + + + Linux +
+ } + > + +

+ Click this button to copy the command.{" "} + + View script source + +

+ + + + Windows +
+ } + > + + + +

+ Requires Windows 10 or later. +

+ + + + macOS + + } + > + + + +

+ Requires macOS Big Sur 11.0 or later. +
+ You can also download Tailscale on the{" "} + + macOS App Store + + {"."} +

+
+ + + iOS + + } + > + + + +

+ Requires iOS 15 or later. +

+
+ + + Android + + } + > + + + +

+ Requires Android 8 or later. +

+
+ + + +
+
+ Need dashboard access? + + Your account is signed in but doesn't have permission to manage the dashboard. + Contact an administrator to request access. + +
+
+ +
+