import { Check, Copy } from "lucide-react"; import { redirect } from "react-router"; import androidSvg from "~/assets/android.svg"; import iosSvg from "~/assets/ios.svg"; import linuxSvg from "~/assets/linux.svg"; import macosSvg from "~/assets/macos.svg"; import windowsSvg from "~/assets/windows.svg"; import Button from "~/components/Button"; import Card from "~/components/Card"; import Link from "~/components/link"; import LinkAccount from "~/layout/link-account"; import { Capabilities } from "~/server/web/roles"; import cn from "~/utils/cn"; import toast from "~/utils/toast"; import { getUserDisplayName } from "~/utils/user"; import type { Route } from "./+types/home"; export async function loader({ request, context }: Route.LoaderArgs) { const principal = await context.auth.require(request); // If the OIDC user has no linked Headscale user, check for // Unclaimed users they can pick from before anything else. let unlinked = false; if ( typeof context.oidc === "object" && principal.kind === "oidc" && !principal.user.headscaleUserId ) { const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); const api = context.hsApi.getRuntimeClient(apiKey); let headscaleUsers: { id: string; name: string }[] = []; try { const [apiUsers, claimed] = await Promise.all([ api.getUsers(), context.auth.claimedHeadscaleUserIds(), ]); headscaleUsers = apiUsers .filter((u) => !claimed.has(u.id)) .map((u) => ({ id: u.id, name: getUserDisplayName(u) })); } catch { // API unavailable, skip the link picker } if (headscaleUsers.length > 0) { return { headscaleUsers, status: "needs_link" as const }; } // No unclaimed users, fall through to no-access page. // Only warn if Headscale isn't using OIDC — if it is, the user // Just needs to connect a device and Headscale will auto-create // Their account, at which point auto-link will pick it up. if (!context.hs.c?.oidc) { unlinked = true; } } if (context.auth.can(principal, Capabilities.ui_access)) { return redirect("/machines"); } // No UI access — show the download/connect page const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); const api = context.hsApi.getRuntimeClient(apiKey); let linkedUserName: string | undefined; if (principal.kind === "oidc" && principal.user.headscaleUserId) { try { const users = await api.getUsers(); const hsUser = users.find((u) => u.id === principal.user.headscaleUserId); linkedUserName = hsUser?.name; } catch { // API unavailable, skip linked user resolution } } return { linkedUserName, status: "no_access" as const, unlinked }; } export async function action({ request, context }: Route.ActionArgs) { const principal = await context.auth.require(request); if (principal.kind !== "oidc") { return redirect("/"); } const formData = await request.formData(); const headscaleUserId = formData.get("headscale_user_id")?.toString(); if (headscaleUserId) { await context.auth.linkHeadscaleUser(principal.user.id, headscaleUserId); } return redirect("/"); } const downloads = [ { href: "https://pkgs.tailscale.com/stable/tailscale-setup-latest.exe", icon: windowsSvg, name: "Windows", note: "Windows 10+", }, { href: "https://pkgs.tailscale.com/stable/Tailscale-latest-macos.pkg", icon: macosSvg, name: "macOS", note: "macOS Big Sur+", }, { href: "https://apps.apple.com/us/app/tailscale/id1470499037", icon: iosSvg, name: "iOS", note: "iOS 15+", }, { href: "https://play.google.com/store/apps/details?id=com.tailscale.ipn", icon: androidSvg, name: "Android", note: "Android 8+", }, ]; export default function Home({ loaderData }: Route.ComponentProps) { if (loaderData.status === "needs_link") { return ; } return (
{loaderData.linkedUserName && ( Your account is linked to Headscale user {loaderData.linkedUserName}. )} Access your network via Tailscale You've successfully authenticated but don't have access to the dashboard. You can still connect to your Headscale network by installing Tailscale.
Linux Linux
curl -fsSL https://tailscale.com/install.sh | sh

View script source

{downloads.map((dl) => ( {dl.name} {dl.name} {dl.note} ))}
{loaderData.unlinked ? "Your account isn't linked to a Headscale user. Ask your administrator to create one for you." : "Need access to the dashboard? Contact your administrator to request access."}
); }