feat: update to the v8 middleware api

This commit is contained in:
Aarnav Tale
2026-06-20 12:38:09 -04:00
parent 3f9dcd5eb4
commit 5d6eef5843
30 changed files with 467 additions and 239 deletions
+16 -9
View File
@@ -6,18 +6,22 @@ import Notice from "~/components/notice";
import StatusCircle from "~/components/status-circle";
import Text from "~/components/text";
import Title from "~/components/title";
import { agentsContext, authContext } from "~/server/context";
import { formatTimeDelta } from "~/utils/time";
import type { Route } from "./+types/agent";
export async function loader({ request, context }: Route.LoaderArgs) {
await context.auth.require(request);
const agents = context.get(agentsContext);
const auth = context.get(authContext);
if (context.agents.state !== "enabled") {
return { enabled: false as const, reason: context.agents.reason };
await auth.require(request);
if (agents.state !== "enabled") {
return { enabled: false as const, reason: agents.reason };
}
const sync = context.agents.value.lastSync();
const sync = agents.value.lastSync();
return {
enabled: true as const,
syncedAt: sync.syncedAt?.toISOString() ?? null,
@@ -27,14 +31,17 @@ export async function loader({ request, context }: Route.LoaderArgs) {
}
export async function action({ request, context }: Route.ActionArgs) {
await context.auth.require(request);
const agents = context.get(agentsContext);
const auth = context.get(authContext);
if (context.agents.state !== "enabled") {
return { success: false, error: context.agents.reason };
await auth.require(request);
if (agents.state !== "enabled") {
return { success: false, error: agents.reason };
}
await context.agents.value.triggerSync();
const sync = context.agents.value.lastSync();
await agents.value.triggerSync();
const sync = agents.value.lastSync();
return { success: !sync.error, error: sync.error };
}
+7 -3
View File
@@ -1,5 +1,6 @@
import { data } from "react-router";
import { authContext, requestApiContext } from "~/server/context";
import { isUserPrincipal } from "~/server/web/auth";
import { getOidcSubject } from "~/server/web/headscale-identity";
import { Capabilities } from "~/server/web/roles";
@@ -8,10 +9,13 @@ import type { PreAuthKey } from "~/types";
import type { Route } from "./+types/overview";
export async function authKeysAction({ request, context }: Route.ActionArgs) {
const { principal, api } = await context.apiForRequest(request);
const auth = context.get(authContext);
const getRequestApi = context.get(requestApiContext);
const canGenerateAny = context.auth.can(principal, Capabilities.generate_authkeys);
const canGenerateOwn = context.auth.can(principal, Capabilities.generate_own_authkeys);
const { principal, api } = await getRequestApi(request);
const canGenerateAny = auth.can(principal, Capabilities.generate_authkeys);
const canGenerateOwn = auth.can(principal, Capabilities.generate_own_authkeys);
if (!canGenerateAny && !canGenerateOwn) {
throw data("You do not have permission to manage pre-auth keys", {
+16 -5
View File
@@ -6,6 +6,12 @@ import Link from "~/components/link";
import Notice from "~/components/notice";
import Select from "~/components/select";
import TableList from "~/components/table-list";
import {
appConfigContext,
authContext,
headscaleLiveStoreContext,
requestApiContext,
} from "~/server/context";
import { usersResource } from "~/server/headscale/live-store";
import { isUserPrincipal } from "~/server/web/auth";
import { Capabilities } from "~/server/web/roles";
@@ -20,9 +26,14 @@ import AuthKeyRow from "./auth-key-row";
import AddAuthKey from "./dialogs/add-auth-key";
export async function loader({ request, context }: Route.LoaderArgs) {
const { principal, api } = await context.apiForRequest(request);
const auth = context.get(authContext);
const config = context.get(appConfigContext);
const getRequestApi = context.get(requestApiContext);
const headscaleLiveStore = context.get(headscaleLiveStoreContext);
const usersSnap = await context.hsLive.get(usersResource, api);
const { principal, api } = await getRequestApi(request);
const usersSnap = await headscaleLiveStore.get(usersResource, api);
const users = usersSnap.data;
let keys: { user: User | null; preAuthKeys: PreAuthKey[] }[];
@@ -86,8 +97,8 @@ export async function loader({ request, context }: Route.LoaderArgs) {
.map(({ user, error }) => ({ error, user }));
}
const canGenerateAny = context.auth.can(principal, Capabilities.generate_authkeys);
const canGenerateOwn = context.auth.can(principal, Capabilities.generate_own_authkeys);
const canGenerateAny = auth.can(principal, Capabilities.generate_authkeys);
const canGenerateOwn = auth.can(principal, Capabilities.generate_own_authkeys);
return {
access: canGenerateAny || canGenerateOwn,
@@ -96,7 +107,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
keys,
missing,
selfServiceOnly: !canGenerateAny && canGenerateOwn,
url: context.config.headscale.public_url ?? context.config.headscale.url,
url: config.headscale.public_url ?? config.headscale.url,
users,
};
}
+6 -3
View File
@@ -2,14 +2,17 @@ import { ArrowRight } from "lucide-react";
import Link from "~/components/link";
import PageError from "~/components/page-error";
import { headscaleConfigContext, oidcContext } from "~/server/context";
import type { Route } from "./+types/overview";
export async function loader({ context }: Route.LoaderArgs) {
const headscaleConfig = context.get(headscaleConfigContext);
const oidc = context.get(oidcContext);
return {
config: context.hs.writable(),
isOidcEnabled:
context.oidc.state === "enabled" && context.oidc.value.status().state === "ready",
config: headscaleConfig.writable(),
isOidcEnabled: oidc.state === "enabled" && oidc.value.status().state === "ready",
};
}
+32 -21
View File
@@ -1,12 +1,23 @@
import { data } from "react-router";
import {
authContext,
headscaleConfigContext,
headscaleContext,
integrationContext,
} from "~/server/context";
import { Capabilities } from "~/server/web/roles";
import type { Route } from "./+types/overview";
export async function restrictionAction({ request, context }: Route.ActionArgs) {
const principal = await context.auth.require(request);
const check = context.auth.can(principal, Capabilities.configure_iam);
const auth = context.get(authContext);
const headscale = context.get(headscaleContext);
const headscaleConfig = context.get(headscaleConfigContext);
const integration = context.get(integrationContext);
const principal = await auth.require(request);
const check = auth.can(principal, Capabilities.configure_iam);
if (!check) {
throw data("You do not have permission to modify IAM settings.", {
@@ -14,7 +25,7 @@ export async function restrictionAction({ request, context }: Route.ActionArgs)
});
}
if (!context.hs.writable()) {
if (!headscaleConfig.writable()) {
throw data("The Headscale configuration file is not editable.", {
status: 403,
});
@@ -37,16 +48,16 @@ export async function restrictionAction({ request, context }: Route.ActionArgs)
});
}
const domains = [...new Set([...(context.hs.c?.oidc?.allowed_domains ?? []), domain])];
const domains = [...new Set([...(headscaleConfig.c?.oidc?.allowed_domains ?? []), domain])];
await context.hs.patch([
await headscaleConfig.patch([
{
path: "oidc.allowed_domains",
value: domains,
},
]);
context.integration?.onConfigChange(context.headscale);
integration?.onConfigChange(headscale);
return data("Domain added successfully.");
}
@@ -58,7 +69,7 @@ export async function restrictionAction({ request, context }: Route.ActionArgs)
});
}
const storedDomains = context.hs.c?.oidc?.allowed_domains ?? [];
const storedDomains = headscaleConfig.c?.oidc?.allowed_domains ?? [];
if (!storedDomains.includes(domain)) {
// Domain not found in the list
throw data(`Domain "${domain}" not found in allowed domains.`, {
@@ -68,13 +79,13 @@ export async function restrictionAction({ request, context }: Route.ActionArgs)
// Filter out the domain to remove it from the list
const domains = storedDomains.filter((d: string) => d !== domain);
await context.hs.patch([
await headscaleConfig.patch([
{
path: "oidc.allowed_domains",
value: domains,
},
]);
context.integration?.onConfigChange(context.headscale);
integration?.onConfigChange(headscale);
return data("Domain removed successfully.");
}
@@ -86,16 +97,16 @@ export async function restrictionAction({ request, context }: Route.ActionArgs)
});
}
const groups = [...new Set([...(context.hs.c?.oidc?.allowed_groups ?? []), group])];
const groups = [...new Set([...(headscaleConfig.c?.oidc?.allowed_groups ?? []), group])];
await context.hs.patch([
await headscaleConfig.patch([
{
path: "oidc.allowed_groups",
value: groups,
},
]);
context.integration?.onConfigChange(context.headscale);
integration?.onConfigChange(headscale);
return data("Group added successfully.");
}
@@ -107,7 +118,7 @@ export async function restrictionAction({ request, context }: Route.ActionArgs)
});
}
const storedGroups = context.hs.c?.oidc?.allowed_groups ?? [];
const storedGroups = headscaleConfig.c?.oidc?.allowed_groups ?? [];
if (!storedGroups.includes(group)) {
// Group not found in the list
throw data(`Group "${group}" not found in allowed groups.`, {
@@ -117,14 +128,14 @@ export async function restrictionAction({ request, context }: Route.ActionArgs)
// Filter out the group to remove it from the list
const groups = storedGroups.filter((d: string) => d !== group);
await context.hs.patch([
await headscaleConfig.patch([
{
path: "oidc.allowed_groups",
value: groups,
},
]);
context.integration?.onConfigChange(context.headscale);
integration?.onConfigChange(headscale);
return data("Group removed successfully.");
}
@@ -136,16 +147,16 @@ export async function restrictionAction({ request, context }: Route.ActionArgs)
});
}
const users = [...new Set([...(context.hs.c?.oidc?.allowed_users ?? []), user])];
const users = [...new Set([...(headscaleConfig.c?.oidc?.allowed_users ?? []), user])];
await context.hs.patch([
await headscaleConfig.patch([
{
path: "oidc.allowed_users",
value: users,
},
]);
context.integration?.onConfigChange(context.headscale);
integration?.onConfigChange(headscale);
return data("User added successfully.");
}
@@ -157,7 +168,7 @@ export async function restrictionAction({ request, context }: Route.ActionArgs)
});
}
const storedUsers = context.hs.c?.oidc?.allowed_users ?? [];
const storedUsers = headscaleConfig.c?.oidc?.allowed_users ?? [];
if (!storedUsers.includes(user)) {
// User not found in the list
throw data(`User "${user}" not found in allowed users.`, {
@@ -167,14 +178,14 @@ export async function restrictionAction({ request, context }: Route.ActionArgs)
// Filter out the user to remove it from the list
const users = storedUsers.filter((d: string) => d !== user);
await context.hs.patch([
await headscaleConfig.patch([
{
path: "oidc.allowed_users",
value: users,
},
]);
context.integration?.onConfigChange(context.headscale);
integration?.onConfigChange(headscale);
return data("User removed successfully.");
}
+12 -8
View File
@@ -2,6 +2,7 @@ import { data } from "react-router";
import Link from "~/components/link";
import Notice from "~/components/notice";
import { authContext, headscaleConfigContext } from "~/server/context";
import { Capabilities } from "~/server/web/roles";
import type { Route } from "./+types/overview";
@@ -12,28 +13,31 @@ import AddUser from "./dialogs/add-user";
import RestrictionTable from "./table";
export async function loader({ request, context }: Route.LoaderArgs) {
const principal = await context.auth.require(request);
const check = context.auth.can(principal, Capabilities.read_users);
const auth = context.get(authContext);
const headscaleConfig = context.get(headscaleConfigContext);
const principal = await auth.require(request);
const check = auth.can(principal, Capabilities.read_users);
if (!check) {
throw data("You do not have permission to view IAM settings.", {
status: 403,
});
}
if (!context.hs.c?.oidc) {
if (!headscaleConfig.c?.oidc) {
throw data("OIDC is not configured on this Headscale instance.", {
status: 501,
});
}
return {
access: context.auth.can(principal, Capabilities.configure_iam),
access: auth.can(principal, Capabilities.configure_iam),
settings: {
domains: [...new Set(context.hs.c.oidc.allowed_domains)],
groups: [...new Set(context.hs.c.oidc.allowed_groups)],
users: [...new Set(context.hs.c.oidc.allowed_users)],
domains: [...new Set(headscaleConfig.c.oidc.allowed_domains)],
groups: [...new Set(headscaleConfig.c.oidc.allowed_groups)],
users: [...new Set(headscaleConfig.c.oidc.allowed_users)],
},
writable: context.hs.writable(),
writable: headscaleConfig.writable(),
};
}