headplane/app/layouts/shell.tsx

120 lines
3.4 KiB
TypeScript
Raw Normal View History

import { eq } from 'drizzle-orm';
import { CircleCheckIcon } from 'lucide-react';
2025-02-13 12:35:12 -05:00
import {
LoaderFunctionArgs,
Outlet,
redirect,
useLoaderData,
} from 'react-router';
import Button from '~/components/Button';
2025-03-29 14:12:15 -04:00
import Card from '~/components/Card';
import Footer from '~/components/Footer';
2025-02-13 12:35:12 -05:00
import Header from '~/components/Header';
2025-03-22 01:36:27 -04:00
import type { LoadContext } from '~/server';
import { users } from '~/server/db/schema';
2025-03-29 14:12:15 -04:00
import { Capabilities } from '~/server/web/roles';
import toast from '~/utils/toast';
// This loads the bare minimum for the application to function
// So we know that if context fails to load then well, oops?
2025-03-22 01:36:27 -04:00
export async function loader({
request,
context,
}: LoaderFunctionArgs<LoadContext>) {
try {
const session = await context.sessions.auth(request);
if (
context.oidc &&
session.user.subject !== 'unknown-non-oauth' &&
!request.url.endsWith('/onboarding')
) {
const [user] = await context.db
.select()
.from(users)
.where(eq(users.sub, session.user.subject))
.limit(1);
if (!user?.onboarded) {
return redirect('/onboarding');
}
}
2025-03-29 14:12:15 -04:00
const check = await context.sessions.check(request, Capabilities.ui_access);
2025-03-22 01:36:27 -04:00
return {
config: context.hs.c,
url: context.config.headscale.public_url ?? context.config.headscale.url,
configAvailable: context.hs.readable(),
debug: context.config.debug,
user: session.user,
2025-03-29 14:12:15 -04:00
uiAccess: check,
access: {
ui: await context.sessions.check(request, Capabilities.ui_access),
dns: await context.sessions.check(request, Capabilities.read_network),
users: await context.sessions.check(request, Capabilities.read_users),
policy: await context.sessions.check(request, Capabilities.read_policy),
machines: await context.sessions.check(
request,
Capabilities.read_machines,
),
settings: await context.sessions.check(
request,
Capabilities.read_feature,
),
},
onboarding: request.url.endsWith('/onboarding'),
2025-05-04 14:43:40 -04:00
healthy: await context.client.healthcheck(),
2025-03-22 01:36:27 -04:00
};
} catch {
return redirect('/login', {
headers: {
'Set-Cookie': await context.sessions.destroySession(),
},
});
}
}
export default function Shell() {
const data = useLoaderData<typeof loader>();
2025-03-29 14:12:15 -04:00
return (
<>
<Header {...data} />
2025-04-02 20:34:32 -04:00
{/* Always show the outlet if we are onboarding */}
2025-04-03 22:58:40 -04:00
{(data.onboarding ? true : data.uiAccess) ? (
2025-03-29 14:12:15 -04:00
<Outlet />
) : (
<Card className="mx-auto w-fit mt-24">
<div className="flex items-center justify-between">
<Card.Title className="text-3xl mb-0">Connected</Card.Title>
<CircleCheckIcon className="w-10 h-10" />
2025-03-29 14:12:15 -04:00
</div>
<Card.Text className="my-4 text-lg">
Connect to Tailscale with your devices to access this Tailnet. Use
this command to help you get started:
2025-03-29 14:12:15 -04:00
</Card.Text>
<Button
className="flex text-md font-mono"
onPress={async () => {
await navigator.clipboard.writeText(
`tailscale up --login-server=${data.url}`,
);
toast('Copied to clipboard');
}}
>
tailscale up --login-server={data.url}
</Button>
<p className="text-xs mt-1 opacity-50 text-center">
Click this button to copy the command.
</p>
<p className="mt-4 text-sm opacity-50">
Your account does not have access to the UI. Please contact your
administrator if you believe this is a mistake.
</p>
2025-03-29 14:12:15 -04:00
</Card>
)}
<Footer {...data} />
</>
2025-02-13 12:35:12 -05:00
);
}