import { data } from "react-router"; import { getOidcSubject } from "~/server/web/headscale-identity"; import { Capabilities } from "~/server/web/roles"; import type { Route } from "./+types/overview"; export async function authKeysAction({ request, context }: Route.ActionArgs) { const principal = await context.auth.require(request); const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); const api = context.hsApi.getRuntimeClient(apiKey); 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.getUsers(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.createPreAuthKey( user, ephemeral === "on", reusable === "on", date, aclTags.length > 0 ? aclTags : null, ); return data({ success: true as const, key: key.key }); } case "expire_preauthkey": { const key = formData.get("key")?.toString(); if (!key) { return data("Missing `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.expirePreAuthKey(user, key); return data("Pre-auth key expired"); } default: return data("Invalid action", { status: 400, }); } }