mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
0512565f8e
Apparently I didn't use my brain cells and rely on the /version endpoint that Headscale has exposed since 0.26 (our lowest supported version). Switching to that significantly simplifies the API surface.
129 lines
3.7 KiB
TypeScript
129 lines
3.7 KiB
TypeScript
import { data } from "react-router";
|
|
|
|
import { getOidcSubject } from "~/server/web/headscale-identity";
|
|
import { Capabilities } from "~/server/web/roles";
|
|
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 canGenerateAny = context.auth.can(principal, Capabilities.generate_authkeys);
|
|
const canGenerateOwn = context.auth.can(principal, Capabilities.generate_own_authkeys);
|
|
|
|
if (!canGenerateAny && !canGenerateOwn) {
|
|
throw data("You do not have permission to manage pre-auth keys", {
|
|
status: 403,
|
|
});
|
|
}
|
|
|
|
async function checkSelfServiceOwnership(userId: string) {
|
|
if (canGenerateAny || !canGenerateOwn) return;
|
|
const [targetUser] = await api.users.list({ id: userId });
|
|
if (!targetUser) {
|
|
throw data("User not found.", { status: 404 });
|
|
}
|
|
const targetSubject = getOidcSubject(targetUser);
|
|
if (principal.kind !== "oidc" || targetSubject !== principal.user.subject) {
|
|
throw data("You do not have permission to manage this user's pre-auth keys", {
|
|
status: 403,
|
|
});
|
|
}
|
|
}
|
|
|
|
const formData = await request.formData();
|
|
const action = formData.get("action_id")?.toString();
|
|
if (!action) {
|
|
throw data("Missing `action_id` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
switch (action) {
|
|
case "add_preauthkey": {
|
|
const user = formData.get("user_id")?.toString() || null;
|
|
const aclTagsRaw = formData.get("acl_tags")?.toString() || "";
|
|
const aclTags = aclTagsRaw
|
|
.split(",")
|
|
.map((t) => t.trim())
|
|
.filter((t) => t.length > 0);
|
|
|
|
if (!user && aclTags.length === 0) {
|
|
return data("Must specify either a user or ACL tags.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
if (user) {
|
|
await checkSelfServiceOwnership(user);
|
|
}
|
|
|
|
const expiry = formData.get("expiry")?.toString();
|
|
if (!expiry) {
|
|
return data("Missing `expiry` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const reusable = formData.get("reusable")?.toString();
|
|
if (!reusable) {
|
|
return data("Missing `reusable` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const ephemeral = formData.get("ephemeral")?.toString();
|
|
if (!ephemeral) {
|
|
return data("Missing `ephemeral` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const day = Number(expiry.toString().split(" ")[0]);
|
|
const date = new Date();
|
|
date.setDate(date.getDate() + day);
|
|
|
|
const key = await api.preAuthKeys.create({
|
|
user,
|
|
ephemeral: ephemeral === "on",
|
|
reusable: reusable === "on",
|
|
expiration: date,
|
|
aclTags: aclTags.length > 0 ? aclTags : null,
|
|
});
|
|
|
|
return data({ success: true as const, key: key.key });
|
|
}
|
|
|
|
case "expire_preauthkey": {
|
|
const keyId = formData.get("key_id")?.toString();
|
|
const key = formData.get("key")?.toString();
|
|
if (!keyId || !key) {
|
|
return data("Missing `key_id` or `key` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const user = formData.get("user_id")?.toString();
|
|
if (!user) {
|
|
return data("Missing `user_id` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
await checkSelfServiceOwnership(user);
|
|
await api.preAuthKeys.expire({
|
|
id: keyId,
|
|
key,
|
|
user: { name: user },
|
|
} as unknown as PreAuthKey);
|
|
return data("Pre-auth key expired");
|
|
}
|
|
|
|
default:
|
|
return data("Invalid action", {
|
|
status: 400,
|
|
});
|
|
}
|
|
}
|