import { Icon } from "@iconify/react"; import { ArrowRight } from "lucide-react"; import { useEffect } from "react"; import { Form, NavLink } from "react-router"; import Button from "~/components/Button"; import Card from "~/components/Card"; import Link from "~/components/Link"; import Options from "~/components/Options"; import StatusCircle from "~/components/StatusCircle"; import { findHeadscaleUserBySubject } from "~/server/web/headscale-identity"; import { Machine } from "~/types"; import cn from "~/utils/cn"; import { useLiveData } from "~/utils/live-data"; import log from "~/utils/log"; import toast from "~/utils/toast"; import { getUserDisplayName } from "~/utils/user"; import type { Route } from "./+types/onboarding"; export async function loader({ request, context }: Route.LoaderArgs) { const principal = await context.auth.require(request); if (principal.kind !== "oidc") { throw new Error("Onboarding is only available for OIDC users."); } const userAgent = request.headers.get("user-agent"); const os = userAgent?.match(/(Linux|Windows|Mac OS X|iPhone|iPad|Android)/); let osValue = "linux"; 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; } const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); const api = context.hsApi.getRuntimeClient(apiKey); const hsUserId = principal.user.headscaleUserId; let firstMachine: Machine | undefined; let needsUserLink = false; let linkedUserName: string | undefined; let headscaleUsers: { id: string; name: string }[] = []; try { const [nodes, apiUsers] = await Promise.all([api.getNodes(), api.getUsers()]); if (hsUserId) { const hsUser = apiUsers.find((u) => u.id === hsUserId); linkedUserName = hsUser ? getUserDisplayName(hsUser) : undefined; firstMachine = nodes.find((n) => n.user?.id === hsUserId); } else { const matched = findHeadscaleUserBySubject( apiUsers, principal.user.subject, principal.profile.email, ); if (matched) { await context.auth.linkHeadscaleUser(principal.user.id, matched.id); linkedUserName = getUserDisplayName(matched); firstMachine = nodes.find((n) => n.user?.id === matched.id); } else { needsUserLink = true; const claimed = await context.auth.claimedHeadscaleUserIds(); headscaleUsers = apiUsers .filter((u) => !claimed.has(u.id)) .map((u) => ({ id: u.id, name: getUserDisplayName(u), })); } } } catch (e) { log.debug("api", "Failed to lookup nodes %o", e); } return { user: { subject: principal.user.subject, name: principal.profile.name, email: principal.profile.email, username: principal.profile.username, picture: principal.profile.picture, }, osValue, firstMachine, needsUserLink, linkedUserName, headscaleUsers, }; } export default function Page({ loaderData: { user, osValue, firstMachine, needsUserLink, linkedUserName, headscaleUsers }, }: Route.ComponentProps) { const { pause, resume } = useLiveData(); useEffect(() => { if (firstMachine) { pause(); } else { resume(); } }, [firstMachine]); const subject = user.email ? ( <> as {user.email} ) : ( "with your OIDC provider" ); return (
{needsUserLink ? ( Link your Headscale account Headplane couldn't automatically match your SSO identity to a Headscale user. {headscaleUsers.length > 0 ? " Select which Headscale user you are, or skip to continue without linking." : " All Headscale users are already linked. You can skip this step and ask an admin to link your account later."} {headscaleUsers.length > 0 ? (
) : undefined}

Without linking, you won't be able to see your own machines or generate pre-auth keys. An admin can link your account later from the Users page.

) : undefined} {linkedUserName && !needsUserLink ? (

✓ Your account has been linked to Headscale user {linkedUserName}.

) : undefined} Welcome!
Let's get set up
Install Tailscale and sign in {subject}. Once you sign in on a device, it will be automatically added to your Headscale network. 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.

{firstMachine ? (
Success!
We found your first device

{firstMachine.givenName}

{firstMachine.name}

IP Addresses

{firstMachine.ipAddresses.map((ip) => (

{ip}

))}
) : (

Waiting for your first device...

)}
); }