mirror of
https://github.com/tale/headplane.git
synced 2026-08-28 16:07:07 +00:00
feat: implement onboarding for non-registered users
This commit is contained in:
+68
-6
@@ -1,15 +1,19 @@
|
||||
import { BanIcon } from 'lucide-react';
|
||||
import { BanIcon, CircleCheckIcon } from 'lucide-react';
|
||||
import {
|
||||
LoaderFunctionArgs,
|
||||
Outlet,
|
||||
redirect,
|
||||
useLoaderData,
|
||||
} from 'react-router';
|
||||
import Button from '~/components/Button';
|
||||
import Card from '~/components/Card';
|
||||
import Code from '~/components/Code';
|
||||
import Footer from '~/components/Footer';
|
||||
import Header from '~/components/Header';
|
||||
import type { LoadContext } from '~/server';
|
||||
import { Capabilities } from '~/server/web/roles';
|
||||
import { User } from '~/types';
|
||||
import log from '~/utils/log';
|
||||
|
||||
// This loads the bare minimum for the application to function
|
||||
// So we know that if context fails to load then well, oops?
|
||||
@@ -28,6 +32,54 @@ export async function loader({
|
||||
});
|
||||
}
|
||||
|
||||
// Onboarding is only a feature of the OIDC flow
|
||||
if (context.oidc && !request.url.endsWith('/onboarding')) {
|
||||
let onboarded = true;
|
||||
|
||||
try {
|
||||
const { users } = await context.client.get<{ users: User[] }>(
|
||||
'v1/user',
|
||||
session.get('api_key')!,
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
onboarded = false;
|
||||
}
|
||||
|
||||
const user = users.find((u) => {
|
||||
if (u.provider !== 'oidc') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For some reason, headscale makes providerID a url where the
|
||||
// last component is the subject, so we need to strip that out
|
||||
const subject = u.providerId?.split('/').pop();
|
||||
if (!subject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sessionUser = session.get('user');
|
||||
if (!sessionUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return subject === sessionUser.subject;
|
||||
});
|
||||
|
||||
if (user) {
|
||||
onboarded = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// If we cannot lookup users, just assume our user is onboarded
|
||||
log.debug('api', 'Failed to lookup users %o', e);
|
||||
onboarded = true;
|
||||
}
|
||||
|
||||
if (!onboarded) {
|
||||
return redirect('/onboarding');
|
||||
}
|
||||
}
|
||||
|
||||
const check = await context.sessions.check(request, Capabilities.ui_access);
|
||||
return {
|
||||
config: context.hs.c,
|
||||
@@ -36,6 +88,7 @@ export async function loader({
|
||||
debug: context.config.debug,
|
||||
user: session.get('user'),
|
||||
uiAccess: check,
|
||||
onboarding: request.url.endsWith('/onboarding'),
|
||||
};
|
||||
} catch {
|
||||
// No session, so we can just return
|
||||
@@ -54,13 +107,22 @@ export default function Shell() {
|
||||
) : (
|
||||
<Card className="mx-auto w-fit mt-24">
|
||||
<div className="flex items-center justify-between">
|
||||
<Card.Title className="text-3xl mb-0">Access Denied</Card.Title>
|
||||
<BanIcon className="w-10 h-10" />
|
||||
<Card.Title className="text-3xl mb-0">Connected</Card.Title>
|
||||
<CircleCheckIcon className="w-10 h-10" />
|
||||
</div>
|
||||
<Card.Text className="mt-4 text-lg">
|
||||
Your account does not have access to the UI. Please contact your
|
||||
administrator.
|
||||
<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:
|
||||
</Card.Text>
|
||||
<Button className="pointer-events-none text-md hover:bg-initial focus:ring-0">
|
||||
<Code className="pointer-events-auto bg-transparent" isCopyable>
|
||||
tailscale up --login-server={data.url}
|
||||
</Code>
|
||||
</Button>
|
||||
<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>
|
||||
</Card>
|
||||
)}
|
||||
<Footer {...data} />
|
||||
|
||||
Reference in New Issue
Block a user