From 270b99f0632c44af523ff79ee642072f07f91af8 Mon Sep 17 00:00:00 2001
From: Aarnav Tale
Date: Sun, 8 Mar 2026 01:21:11 -0500
Subject: [PATCH] fix: show linked user in onboarding + pending approval screen
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Onboarding now shows which Headscale user was auto-linked
- Members (no ui_access) see a 'Pending Approval' page instead of
being silently logged out — their session stays valid
- Sign out button on the pending page so users can switch accounts
Co-authored-by: Amp
Amp-Thread-ID: https://ampcode.com/threads/T-019cce57-c9e1-7732-9709-8288127573a9
---
app/layouts/shell.tsx | 37 ++++++++++++++++++++++++++++-----
app/routes/users/onboarding.tsx | 14 ++++++++++++-
2 files changed, 45 insertions(+), 6 deletions(-)
diff --git a/app/layouts/shell.tsx b/app/layouts/shell.tsx
index eb0c85d..5ec00be 100644
--- a/app/layouts/shell.tsx
+++ b/app/layouts/shell.tsx
@@ -1,5 +1,7 @@
-import { Outlet, redirect } from "react-router";
+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 { Capabilities } from "~/server/web/roles";
@@ -25,10 +27,6 @@ export async function loader({ request, context }: Route.LoaderArgs) {
const api = context.hsApi.getRuntimeClient(apiKey);
const check = context.auth.can(principal, Capabilities.ui_access);
- if (!check && principal.kind === "oidc" && !request.url.endsWith("/onboarding")) {
- throw new Error("You do not have permission to access the UI");
- }
-
const user =
principal.kind === "oidc"
? {
@@ -55,6 +53,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
settings: context.auth.can(principal, Capabilities.read_feature),
},
onboarding: request.url.endsWith("/onboarding"),
+ pendingApproval: !check && principal.kind === "oidc",
healthy: await api.isHealthy(),
};
} catch {
@@ -67,6 +66,34 @@ export async function loader({ request, context }: Route.LoaderArgs) {
}
export default function Shell({ loaderData }: Route.ComponentProps) {
+ if (loaderData.pendingApproval && !loaderData.onboarding) {
+ return (
+ <>
+
+
+
+
+ Pending Approval
+
+ Your account has been created but you don't have access to the UI yet. An
+ administrator needs to assign you a role before you can continue.
+
+
+ If you believe this is a mistake, please contact your administrator.
+
+
+
+
+
+
+ >
+ );
+ }
+
return (
<>
diff --git a/app/routes/users/onboarding.tsx b/app/routes/users/onboarding.tsx
index 48beb6e..d2d1d10 100644
--- a/app/routes/users/onboarding.tsx
+++ b/app/routes/users/onboarding.tsx
@@ -55,12 +55,15 @@ export async function loader({ request, context }: Route.LoaderArgs) {
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(
@@ -71,6 +74,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
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;
@@ -98,12 +102,13 @@ export async function loader({ request, context }: Route.LoaderArgs) {
osValue,
firstMachine,
needsUserLink,
+ linkedUserName,
headscaleUsers,
};
}
export default function Page({
- loaderData: { user, osValue, firstMachine, needsUserLink, headscaleUsers },
+ loaderData: { user, osValue, firstMachine, needsUserLink, linkedUserName, headscaleUsers },
}: Route.ComponentProps) {
const { pause, resume } = useLiveData();
useEffect(() => {
@@ -168,6 +173,13 @@ export default function Page({
) : undefined}
+ {linkedUserName && !needsUserLink ? (
+
+
+ ✓ Your account has been linked to Headscale user {linkedUserName}.
+
+
+ ) : undefined}
Welcome!