mirror of
https://github.com/tale/headplane.git
synced 2026-08-28 16:07:07 +00:00
fix: show linked user in onboarding + pending approval screen
- 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@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019cce57-c9e1-7732-9709-8288127573a9
This commit is contained in:
+32
-5
@@ -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 Footer from "~/components/Footer";
|
||||||
import Header from "~/components/Header";
|
import Header from "~/components/Header";
|
||||||
import { Capabilities } from "~/server/web/roles";
|
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 api = context.hsApi.getRuntimeClient(apiKey);
|
||||||
const check = context.auth.can(principal, Capabilities.ui_access);
|
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 =
|
const user =
|
||||||
principal.kind === "oidc"
|
principal.kind === "oidc"
|
||||||
? {
|
? {
|
||||||
@@ -55,6 +53,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
settings: context.auth.can(principal, Capabilities.read_feature),
|
settings: context.auth.can(principal, Capabilities.read_feature),
|
||||||
},
|
},
|
||||||
onboarding: request.url.endsWith("/onboarding"),
|
onboarding: request.url.endsWith("/onboarding"),
|
||||||
|
pendingApproval: !check && principal.kind === "oidc",
|
||||||
healthy: await api.isHealthy(),
|
healthy: await api.isHealthy(),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
@@ -67,6 +66,34 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Shell({ loaderData }: Route.ComponentProps) {
|
export default function Shell({ loaderData }: Route.ComponentProps) {
|
||||||
|
if (loaderData.pendingApproval && !loaderData.onboarding) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header {...loaderData} />
|
||||||
|
<main className="container mx-auto mt-4 mb-24 overscroll-contain">
|
||||||
|
<div className="mx-auto mt-24 max-w-lg">
|
||||||
|
<Card variant="flat">
|
||||||
|
<Card.Title className="mb-4">Pending Approval</Card.Title>
|
||||||
|
<Card.Text>
|
||||||
|
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.
|
||||||
|
</Card.Text>
|
||||||
|
<Card.Text className="mt-2">
|
||||||
|
If you believe this is a mistake, please contact your administrator.
|
||||||
|
</Card.Text>
|
||||||
|
<Form action="/logout" className="mt-6" method="POST">
|
||||||
|
<Button className="w-full" type="submit" variant="light">
|
||||||
|
Sign out
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer {...loaderData} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header {...loaderData} />
|
<Header {...loaderData} />
|
||||||
|
|||||||
@@ -55,12 +55,15 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
const hsUserId = principal.user.headscaleUserId;
|
const hsUserId = principal.user.headscaleUserId;
|
||||||
let firstMachine: Machine | undefined;
|
let firstMachine: Machine | undefined;
|
||||||
let needsUserLink = false;
|
let needsUserLink = false;
|
||||||
|
let linkedUserName: string | undefined;
|
||||||
let headscaleUsers: { id: string; name: string }[] = [];
|
let headscaleUsers: { id: string; name: string }[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [nodes, apiUsers] = await Promise.all([api.getNodes(), api.getUsers()]);
|
const [nodes, apiUsers] = await Promise.all([api.getNodes(), api.getUsers()]);
|
||||||
|
|
||||||
if (hsUserId) {
|
if (hsUserId) {
|
||||||
|
const hsUser = apiUsers.find((u) => u.id === hsUserId);
|
||||||
|
linkedUserName = hsUser ? getUserDisplayName(hsUser) : undefined;
|
||||||
firstMachine = nodes.find((n) => n.user?.id === hsUserId);
|
firstMachine = nodes.find((n) => n.user?.id === hsUserId);
|
||||||
} else {
|
} else {
|
||||||
const matched = findHeadscaleUserBySubject(
|
const matched = findHeadscaleUserBySubject(
|
||||||
@@ -71,6 +74,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
if (matched) {
|
if (matched) {
|
||||||
await context.auth.linkHeadscaleUser(principal.user.id, matched.id);
|
await context.auth.linkHeadscaleUser(principal.user.id, matched.id);
|
||||||
|
linkedUserName = getUserDisplayName(matched);
|
||||||
firstMachine = nodes.find((n) => n.user?.id === matched.id);
|
firstMachine = nodes.find((n) => n.user?.id === matched.id);
|
||||||
} else {
|
} else {
|
||||||
needsUserLink = true;
|
needsUserLink = true;
|
||||||
@@ -98,12 +102,13 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
osValue,
|
osValue,
|
||||||
firstMachine,
|
firstMachine,
|
||||||
needsUserLink,
|
needsUserLink,
|
||||||
|
linkedUserName,
|
||||||
headscaleUsers,
|
headscaleUsers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page({
|
export default function Page({
|
||||||
loaderData: { user, osValue, firstMachine, needsUserLink, headscaleUsers },
|
loaderData: { user, osValue, firstMachine, needsUserLink, linkedUserName, headscaleUsers },
|
||||||
}: Route.ComponentProps) {
|
}: Route.ComponentProps) {
|
||||||
const { pause, resume } = useLiveData();
|
const { pause, resume } = useLiveData();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -168,6 +173,13 @@ export default function Page({
|
|||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
{linkedUserName && !needsUserLink ? (
|
||||||
|
<Card className="col-span-2 mx-auto max-w-lg" variant="flat">
|
||||||
|
<p className="text-sm">
|
||||||
|
✓ Your account has been linked to Headscale user <strong>{linkedUserName}</strong>.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
) : undefined}
|
||||||
<Card className="max-w-lg" variant="flat">
|
<Card className="max-w-lg" variant="flat">
|
||||||
<Card.Title className="mb-8">
|
<Card.Title className="mb-8">
|
||||||
Welcome!
|
Welcome!
|
||||||
|
|||||||
Reference in New Issue
Block a user