feat: respect context in server

This commit is contained in:
Aarnav Tale
2025-02-19 18:09:42 -05:00
parent 06049169a2
commit f5436f5ee3
32 changed files with 359 additions and 204 deletions
+3 -2
View File
@@ -7,17 +7,18 @@ import Link from '~/components/Link';
import Notice from '~/components/Notice';
import Spinner from '~/components/Spinner';
import Tabs from '~/components/Tabs';
import { hs_getConfig } from '~/utils/config/loader';
import { HeadscaleError, pull, put } from '~/utils/headscale';
import log from '~/utils/log';
import { send } from '~/utils/res';
import { getSession } from '~/utils/sessions.server';
import { hs_getConfig } from '~/utils/state';
import toast from '~/utils/toast';
import type { AppContext } from '~server/context/app';
import { Differ, Editor } from './components/cm.client';
import { ErrorView } from './components/error';
import { Unavailable } from './components/unavailable';
export async function loader({ request }: LoaderFunctionArgs) {
export async function loader({ request }: LoaderFunctionArgs<AppContext>) {
const session = await getSession(request.headers.get('Cookie'));
// The way policy is handled in 0.23 of Headscale and later is verbose.
+22 -9
View File
@@ -10,10 +10,14 @@ import Code from '~/components/Code';
import Input from '~/components/Input';
import type { Key } from '~/types';
import { pull } from '~/utils/headscale';
import { noContext } from '~/utils/log';
import { commitSession, getSession } from '~/utils/sessions.server';
import { hp_getConfig } from '~/utils/state';
import type { AppContext } from '~server/context/app';
export async function loader({ request }: LoaderFunctionArgs) {
export async function loader({
request,
context,
}: LoaderFunctionArgs<AppContext>) {
const session = await getSession(request.headers.get('Cookie'));
if (session.has('hsApiKey')) {
return redirect('/machines', {
@@ -23,28 +27,37 @@ export async function loader({ request }: LoaderFunctionArgs) {
});
}
const context = hp_getConfig();
if (!context) {
throw noContext();
}
// Only set if OIDC is properly enabled anyways
if (context.oidc?.disable_api_key_login) {
const ctx = context.context;
if (ctx.oidc?.disable_api_key_login) {
return redirect('/oidc/start');
}
return {
oidc: context.oidc?.issuer,
apiKey: !context.oidc?.disable_api_key_login,
oidc: ctx.oidc?.issuer,
apiKey: !ctx.oidc?.disable_api_key_login,
};
}
export async function action({ request }: ActionFunctionArgs) {
export async function action({
request,
context,
}: ActionFunctionArgs<AppContext>) {
const formData = await request.formData();
const oidcStart = formData.get('oidc-start');
const session = await getSession(request.headers.get('Cookie'));
if (oidcStart) {
const context = hp_getConfig();
if (!context) {
throw noContext();
}
if (!context.oidc) {
const ctx = context.context;
if (!ctx.oidc) {
throw new Error('An invalid OIDC configuration was provided');
}
+12 -4
View File
@@ -1,10 +1,14 @@
import { type LoaderFunctionArgs, redirect } from 'react-router';
import { finishAuthFlow, formatError, getRedirectUri } from '~/utils/oidc';
import { noContext } from '~/utils/log';
import { finishAuthFlow, formatError } from '~/utils/oidc';
import { send } from '~/utils/res';
import { commitSession, getSession } from '~/utils/sessions.server';
import { hp_getConfig } from '~/utils/state';
import type { AppContext } from '~server/context/app';
export async function loader({ request }: LoaderFunctionArgs) {
export async function loader({
request,
context,
}: LoaderFunctionArgs<AppContext>) {
// Check if we have 0 query parameters
const url = new URL(request.url);
if (url.searchParams.toString().length === 0) {
@@ -16,7 +20,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
return redirect('/machines');
}
const { oidc } = hp_getConfig();
if (!context) {
throw noContext();
}
const { oidc } = context.context;
if (!oidc) {
throw new Error('An invalid OIDC configuration was provided');
}
+11 -5
View File
@@ -1,17 +1,23 @@
import { type LoaderFunctionArgs, redirect } from 'react-router';
import { noContext } from '~/utils/log';
import { beginAuthFlow, getRedirectUri } from '~/utils/oidc';
import { commitSession, getSession } from '~/utils/sessions.server';
import { hp_getConfig } from '~/utils/state';
import type { AppContext } from '~server/context/app';
export async function loader({ request }: LoaderFunctionArgs) {
export async function loader({
request,
context,
}: LoaderFunctionArgs<AppContext>) {
const session = await getSession(request.headers.get('Cookie'));
if (session.has('hsApiKey')) {
return redirect('/machines');
}
// This is a hold-over from the old code
// TODO: Rewrite checkOIDC in the context loader
const { oidc } = hp_getConfig();
if (!context) {
throw noContext();
}
const { oidc } = context.context;
if (!oidc) {
throw new Error('An invalid OIDC configuration was provided');
}
+1 -2
View File
@@ -1,7 +1,6 @@
import { ActionFunctionArgs, data } from 'react-router';
import { hs_patchConfig } from '~/utils/config/loader';
import { hs_getConfig, hs_patchConfig } from '~/utils/config/loader';
import { auth } from '~/utils/sessions.server';
import { hs_getConfig } from '~/utils/state';
export async function dnsAction({ request }: ActionFunctionArgs) {
const session = await auth(request);
+1 -1
View File
@@ -2,7 +2,7 @@ import type { ActionFunctionArgs } from 'react-router';
import { useLoaderData } from 'react-router';
import Code from '~/components/Code';
import Notice from '~/components/Notice';
import { hs_getConfig } from '~/utils/state';
import { hs_getConfig } from '~/utils/config/loader';
import ManageDomains from './components/manage-domains';
import ManageNS from './components/manage-ns';
import ManageRecords from './components/manage-records';
+6 -2
View File
@@ -109,9 +109,13 @@ export async function menuAction(request: ActionFunctionArgs['request']) {
const to = String(data.get('to'));
try {
await post(`v1/node/${id}/user?user=${to}`, session.get('hsApiKey')!);
await post(`v1/node/${id}/user`, session.get('hsApiKey')!, {
user: to,
});
return { message: `Moved node ${id} to ${to}` };
} catch {
} catch (error) {
console.error(error);
return send(
{ message: `Failed to move node ${id} to ${to}` },
{
+1 -1
View File
@@ -11,9 +11,9 @@ import StatusCircle from '~/components/StatusCircle';
import Tooltip from '~/components/Tooltip';
import type { Machine, Route, User } from '~/types';
import cn from '~/utils/cn';
import { hs_getConfig } from '~/utils/config/loader';
import { pull } from '~/utils/headscale';
import { getSession } from '~/utils/sessions.server';
import { hs_getConfig } from '~/utils/state';
import { menuAction } from './action';
import MenuOptions from './components/menu';
import Routes from './dialogs/routes';
+15 -6
View File
@@ -12,12 +12,17 @@ import { getSession } from '~/utils/sessions.server';
import { initAgentSocket, queryAgent } from '~/utils/ws-agent';
import Tooltip from '~/components/Tooltip';
import { hp_getConfig, hs_getConfig } from '~/utils/state';
import { hs_getConfig } from '~/utils/config/loader';
import { noContext } from '~/utils/log';
import { AppContext } from '~server/context/app';
import { menuAction } from './action';
import MachineRow from './components/machine';
import NewMachine from './dialogs/new';
export async function loader({ request, context: lC }: LoaderFunctionArgs) {
export async function loader({
request,
context,
}: LoaderFunctionArgs<AppContext>) {
const session = await getSession(request.headers.get('Cookie'));
const [machines, routes, users] = await Promise.all([
pull<{ nodes: Machine[] }>('v1/node', session.get('hsApiKey')!),
@@ -25,10 +30,14 @@ export async function loader({ request, context: lC }: LoaderFunctionArgs) {
pull<{ users: User[] }>('v1/user', session.get('hsApiKey')!),
]);
initAgentSocket(lC);
if (!context) {
throw noContext();
}
initAgentSocket(context);
const stats = await queryAgent(machines.nodes.map((node) => node.nodeKey));
const context = hp_getConfig();
const ctx = context.context;
const { mode, config } = hs_getConfig();
let magic: string | undefined;
@@ -45,8 +54,8 @@ export async function loader({ request, context: lC }: LoaderFunctionArgs) {
users: users.users,
magic,
stats,
server: context.headscale.url,
publicServer: context.headscale.public_url,
server: ctx.headscale.url,
publicServer: ctx.headscale.public_url,
};
}
+12 -4
View File
@@ -7,9 +7,10 @@ import Select from '~/components/Select';
import TableList from '~/components/TableList';
import type { PreAuthKey, User } from '~/types';
import { post, pull } from '~/utils/headscale';
import { noContext } from '~/utils/log';
import { send } from '~/utils/res';
import { getSession } from '~/utils/sessions.server';
import { hp_getConfig } from '~/utils/state';
import type { AppContext } from '~server/context/app';
import AuthKeyRow from './components/key';
import AddPreAuthKey from './dialogs/new';
@@ -90,14 +91,21 @@ export async function action({ request }: ActionFunctionArgs) {
}
}
export async function loader({ request }: LoaderFunctionArgs) {
const context = hp_getConfig();
export async function loader({
request,
context,
}: LoaderFunctionArgs<AppContext>) {
const session = await getSession(request.headers.get('Cookie'));
const users = await pull<{ users: User[] }>(
'v1/user',
session.get('hsApiKey')!,
);
if (!context) {
throw noContext();
}
const ctx = context.context;
const preAuthKeys = await Promise.all(
users.users.map((user) => {
const qp = new URLSearchParams();
@@ -113,7 +121,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
return {
keys: preAuthKeys.flatMap((keys) => keys.preAuthKeys),
users: users.users,
server: context.headscale.public_url ?? context.headscale.url,
server: ctx.headscale.public_url ?? ctx.headscale.url,
};
}
+14 -16
View File
@@ -1,22 +1,23 @@
import { useMemo } from 'react';
import { useLoaderData, type LoaderFunctionArgs } from 'react-router';
import { getSession, commitSession } from '~/utils/sessions.server'
import { queryAgent } from '~/utils/ws-agent'
import AgentManagement from './components/agent/manage'
import { type LoaderFunctionArgs, useLoaderData } from 'react-router';
import { commitSession, getSession } from '~/utils/sessions.server';
import { queryAgent } from '~/utils/ws-agent';
import AgentManagement from './components/agent/manage';
export async function loader({ request, context }: LoaderFunctionArgs) {
const { ws, wsAuthKey } = context;
const session = await getSession(request.headers.get('Cookie'));
const onboarding = session.get('agent_onboarding') ?? false;
const nodeKey = 'nodekey:542dad28354eb8d51e240aada7adf0222ba3ecc74af0bbd56123f03eefdb391b'
const nodeKey =
'nodekey:542dad28354eb8d51e240aada7adf0222ba3ecc74af0bbd56123f03eefdb391b';
const stats = await queryAgent([nodeKey]);
return {
configured: wsAuthKey !== undefined,
onboarding,
stats: stats[nodeKey]
}
stats: stats?.[nodeKey],
};
}
export default function Page() {
@@ -24,21 +25,18 @@ export default function Page() {
// Whether we show the onboarding or management UI
const management = useMemo(() => {
return data.configured && (data.onboarding === false)
return data.configured && data.onboarding === false;
}, [data.configured, data.onboarding]);
return (
<div className="flex flex-col gap-8 max-w-screen-lg">
{management ? (
<AgentManagement
reachable={true}
hostInfo={data.stats}
/>
<AgentManagement reachable={true} hostInfo={data.stats} />
) : (
<div>
<h1>Local Agent Coming Soon</h1>
</div>
<div>
<h1>Local Agent Coming Soon</h1>
</div>
)}
</div>
)
);
}
@@ -1,11 +1,11 @@
import { Building2, House, Key } from 'lucide-react';
import Card from '~/components/Card';
import Link from '~/components/Link';
import { HeadplaneConfig } from '~/utils/state';
import type { AppContext } from '~server/context/app';
import CreateUser from '../dialogs/create-user';
interface Props {
oidc?: NonNullable<HeadplaneConfig['oidc']>;
oidc?: NonNullable<AppContext['context']['oidc']>;
}
export default function ManageBanner({ oidc }: Props) {
+12 -4
View File
@@ -14,14 +14,22 @@ import cn from '~/utils/cn';
import { pull } from '~/utils/headscale';
import { getSession } from '~/utils/sessions.server';
import { hp_getConfig, hs_getConfig } from '~/utils/state';
import { hs_getConfig } from '~/utils/config/loader';
import { noContext } from '~/utils/log';
import type { AppContext } from '~server/context/app';
import ManageBanner from './components/manage-banner';
import DeleteUser from './dialogs/delete-user';
import RenameUser from './dialogs/rename-user';
import { userAction } from './user-actions';
export async function loader({ request }: LoaderFunctionArgs) {
export async function loader({
request,
context,
}: LoaderFunctionArgs<AppContext>) {
const session = await getSession(request.headers.get('Cookie'));
if (!context) {
throw noContext();
}
const [machines, apiUsers] = await Promise.all([
pull<{ nodes: Machine[] }>('v1/node', session.get('hsApiKey')!),
@@ -33,7 +41,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
machines: machines.nodes.filter((machine) => machine.user.id === user.id),
}));
const context = hp_getConfig();
const ctx = context.context;
const { mode, config } = hs_getConfig();
let magic: string | undefined;
@@ -44,7 +52,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
}
return {
oidc: context.oidc,
oidc: ctx.oidc,
magic,
users,
};