mirror of
https://github.com/tale/headplane.git
synced 2026-08-22 10:46:38 +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.
171 lines
5.6 KiB
TypeScript
171 lines
5.6 KiB
TypeScript
import { data } from "react-router";
|
|
|
|
import { usersResource } from "~/server/headscale/live-store";
|
|
import { getOidcSubject } from "~/server/web/headscale-identity";
|
|
import { Capabilities } from "~/server/web/roles";
|
|
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);
|
|
if (!check) {
|
|
throw data("You do not have permission to update users", {
|
|
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: 404,
|
|
});
|
|
}
|
|
|
|
const { api } = await context.apiForRequest(request);
|
|
switch (action) {
|
|
case "create_user": {
|
|
const name = formData.get("username")?.toString();
|
|
const displayName = formData.get("display_name")?.toString();
|
|
const email = formData.get("email")?.toString();
|
|
|
|
if (!name) {
|
|
throw data("Missing `username` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
await api.users.create({ name, email, displayName });
|
|
await context.hsLive.refresh(usersResource, api);
|
|
return { message: "User created successfully" };
|
|
}
|
|
case "delete_user": {
|
|
const userId = formData.get("user_id")?.toString();
|
|
if (!userId) {
|
|
throw data("Missing `user_id` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
await api.users.delete(userId);
|
|
await context.hsLive.refresh(usersResource, api);
|
|
return { message: "User deleted successfully" };
|
|
}
|
|
case "rename_user": {
|
|
const userId = formData.get("user_id")?.toString();
|
|
const newName = formData.get("new_name")?.toString();
|
|
if (!userId || !newName) {
|
|
return data({ success: false }, 400);
|
|
}
|
|
|
|
const users = await api.users.list({ id: userId });
|
|
const user = users.find((user) => user.id === userId);
|
|
if (!user) {
|
|
throw data(`No user found with id: ${userId}`, { status: 400 });
|
|
}
|
|
|
|
if (user.provider === "oidc") {
|
|
// OIDC users cannot be renamed via this endpoint, return an error
|
|
throw data("Users managed by OIDC cannot be renamed", {
|
|
status: 403,
|
|
});
|
|
}
|
|
|
|
await api.users.rename(userId, newName);
|
|
await context.hsLive.refresh(usersResource, api);
|
|
return { message: "User renamed successfully" };
|
|
}
|
|
case "reassign_user": {
|
|
const userId = formData.get("user_id")?.toString();
|
|
const newRole = formData.get("new_role")?.toString();
|
|
if (!userId || !newRole) {
|
|
throw data("Missing `user_id` or `new_role` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const users = await api.users.list({ id: userId });
|
|
const user = users.find((user) => user.id === userId);
|
|
if (!user) {
|
|
throw data("Specified user not found", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const subject = getOidcSubject(user);
|
|
if (!subject) {
|
|
throw data("Specified user is not an OIDC user or has no subject.", { status: 400 });
|
|
}
|
|
|
|
const result = await context.auth.reassignSubject(subject, newRole as Role);
|
|
|
|
if (!result) {
|
|
throw data("Failed to reassign user role.", { status: 500 });
|
|
}
|
|
|
|
return { message: "User reassigned successfully" };
|
|
}
|
|
case "transfer_ownership": {
|
|
if (principal.kind !== "oidc" || principal.user.role !== "owner") {
|
|
throw data("Only the owner can transfer ownership.", { status: 403 });
|
|
}
|
|
|
|
const userId = formData.get("user_id")?.toString();
|
|
if (!userId) {
|
|
throw data("Missing `user_id` in the form data.", { status: 400 });
|
|
}
|
|
|
|
const users = await api.users.list({ id: userId });
|
|
const user = users.find((user) => user.id === userId);
|
|
if (!user) {
|
|
throw data("Specified user not found", { status: 400 });
|
|
}
|
|
|
|
const targetSubject = getOidcSubject(user);
|
|
if (!targetSubject) {
|
|
throw data("Target user is not an OIDC user or has no subject.", { status: 400 });
|
|
}
|
|
|
|
const result = await context.auth.transferOwnership(principal.user.subject, targetSubject);
|
|
if (!result) {
|
|
throw data("Failed to transfer ownership.", { status: 500 });
|
|
}
|
|
|
|
return { message: "Ownership transferred successfully" };
|
|
}
|
|
case "link_user": {
|
|
const userId = formData.get("user_id")?.toString();
|
|
const headscaleUserId = formData.get("headscale_user_id")?.toString();
|
|
if (!userId || !headscaleUserId) {
|
|
throw data("Missing `user_id` or `headscale_user_id` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const users = await api.users.list({ id: userId });
|
|
const user = users.find((user) => user.id === userId);
|
|
if (!user) {
|
|
throw data("Specified user not found", { status: 400 });
|
|
}
|
|
|
|
const subject = getOidcSubject(user);
|
|
if (!subject) {
|
|
throw data("Specified user is not an OIDC user or has no subject.", { status: 400 });
|
|
}
|
|
|
|
const linked = await context.auth.linkHeadscaleUserBySubject(subject, headscaleUserId);
|
|
if (!linked) {
|
|
throw data("That Headscale user is already linked to another account.", { status: 409 });
|
|
}
|
|
|
|
return { message: "Headscale user linked successfully" };
|
|
}
|
|
default:
|
|
throw data("Invalid `action_id` provided.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
}
|