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
+25 -12
View File
@@ -1,6 +1,13 @@
import { createHash } from "node:crypto";
import PageError from "~/components/page-error";
import {
appConfigContext,
authContext,
headscaleConfigContext,
headscaleLiveStoreContext,
requestApiContext,
} from "~/server/context";
import { nodesResource, usersResource } from "~/server/headscale/live-store";
import { isUserPrincipal } from "~/server/web/auth";
import { Capabilities, Roles } from "~/server/web/roles";
@@ -36,18 +43,24 @@ export interface UnlinkedHeadscaleUser extends User {
}
export async function loader({ request, context }: Route.LoaderArgs) {
const principal = await context.auth.require(request);
const check = await context.auth.can(principal, Capabilities.read_users);
const auth = context.get(authContext);
const config = context.get(appConfigContext);
const getRequestApi = context.get(requestApiContext);
const headscaleConfig = context.get(headscaleConfigContext);
const headscaleLiveStore = context.get(headscaleLiveStoreContext);
const principal = await auth.require(request);
const check = await auth.can(principal, Capabilities.read_users);
if (!check) {
throw new Error(
"You do not have permission to view this page. Please contact your administrator.",
);
}
const writablePermission = await context.auth.can(principal, Capabilities.write_users);
const writablePermission = await auth.can(principal, Capabilities.write_users);
// Primary data: Headplane users from the database (always available)
const hpUsers = await context.auth.listUsers();
const hpUsers = await auth.listUsers();
// Secondary data: Headscale API (may fail)
let apiUsers: User[] = [];
@@ -55,10 +68,10 @@ export async function loader({ request, context }: Route.LoaderArgs) {
let apiError: string | undefined;
try {
const { api } = await context.apiForRequest(request);
const { api } = await getRequestApi(request);
const [nodesSnap, usersSnap] = await Promise.all([
context.hsLive.get(nodesResource, api),
context.hsLive.get(usersResource, api),
headscaleLiveStore.get(nodesResource, api),
headscaleLiveStore.get(usersResource, api),
]);
nodes = nodesSnap.data;
apiUsers = usersSnap.data;
@@ -68,7 +81,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
"Could not connect to the Headscale API. Headscale user data and machine information are unavailable.";
}
const useGravatar = context.config.oidc?.profile_picture_source === "gravatar";
const useGravatar = config.oidc?.profile_picture_source === "gravatar";
function resolveProfilePic(email?: string, profilePicUrl?: string): string | undefined {
if (!useGravatar) return profilePicUrl;
@@ -126,9 +139,9 @@ export async function loader({ request, context }: Route.LoaderArgs) {
}));
let magic: string | undefined;
if (context.hs.readable()) {
if (context.hs.c?.dns.magic_dns) {
magic = context.hs.c.dns.base_domain;
if (headscaleConfig.readable()) {
if (headscaleConfig.c?.dns.magic_dns) {
magic = headscaleConfig.c.dns.base_domain;
}
}
@@ -138,7 +151,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
writable: writablePermission,
currentUserId: isUserPrincipal(principal) ? principal.user.id : undefined,
isOwner,
oidc: context.config.oidc ? { issuer: context.config.oidc.issuer } : undefined,
oidc: config.oidc ? { issuer: config.oidc.issuer } : undefined,
magic,
apiError,
headplaneUsers,
+14 -9
View File
@@ -1,5 +1,6 @@
import { data } from "react-router";
import { 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";
@@ -8,8 +9,12 @@ import type { Role } from "~/server/web/roles";
import type { Route } from "./+types/overview";
export async function userAction({ request, context }: Route.ActionArgs) {
const principal = await context.auth.require(request);
const check = await context.auth.can(principal, Capabilities.write_users);
const auth = context.get(authContext);
const getRequestApi = context.get(requestApiContext);
const headscaleLiveStore = context.get(headscaleLiveStoreContext);
const principal = await auth.require(request);
const check = await auth.can(principal, Capabilities.write_users);
if (!check) {
throw data("You do not have permission to update users", {
status: 403,
@@ -24,7 +29,7 @@ export async function userAction({ request, context }: Route.ActionArgs) {
});
}
const { api } = await context.apiForRequest(request);
const { api } = await getRequestApi(request);
switch (action) {
case "create_user": {
const name = formData.get("username")?.toString();
@@ -38,7 +43,7 @@ export async function userAction({ request, context }: Route.ActionArgs) {
}
await api.users.create({ name, email, displayName });
await context.hsLive.refresh(usersResource, api);
await headscaleLiveStore.refresh(usersResource, api);
return { message: "User created successfully" };
}
case "delete_user": {
@@ -50,7 +55,7 @@ export async function userAction({ request, context }: Route.ActionArgs) {
}
await api.users.delete(headscaleUserId);
await context.hsLive.refresh(usersResource, api);
await headscaleLiveStore.refresh(usersResource, api);
return { message: "User deleted successfully" };
}
case "rename_user": {
@@ -74,7 +79,7 @@ export async function userAction({ request, context }: Route.ActionArgs) {
}
await api.users.rename(headscaleUserId, newName);
await context.hsLive.refresh(usersResource, api);
await headscaleLiveStore.refresh(usersResource, api);
return { message: "User renamed successfully" };
}
case "reassign_user": {
@@ -86,7 +91,7 @@ export async function userAction({ request, context }: Route.ActionArgs) {
});
}
const result = await context.auth.reassignUser(headplaneUserId, newRole as Role);
const result = await auth.reassignUser(headplaneUserId, newRole as Role);
if (!result) {
throw data("Failed to reassign user role.", { status: 500 });
}
@@ -103,7 +108,7 @@ export async function userAction({ request, context }: Route.ActionArgs) {
throw data("Missing `headplane_user_id` in the form data.", { status: 400 });
}
const result = await context.auth.transferOwnership(principal.user.id, headplaneUserId);
const result = await auth.transferOwnership(principal.user.id, headplaneUserId);
if (!result) {
throw data("Failed to transfer ownership.", { status: 500 });
}
@@ -119,7 +124,7 @@ export async function userAction({ request, context }: Route.ActionArgs) {
});
}
const linked = await context.auth.linkHeadscaleUser(headplaneUserId, headscaleUserId);
const linked = await auth.linkHeadscaleUser(headplaneUserId, headscaleUserId);
if (!linked) {
throw data("That Headscale user is already linked to another account.", { status: 409 });
}