mirror of
https://github.com/tale/headplane.git
synced 2026-08-19 17:36:19 +00:00
25dc09e025
Previously we would use naive revalidators which would invalidate EVERY SINGLE action loader every 3 seconds, resulting in several fetches. It would also bubble fetches across the layout actions into the individual pages. This new approach selectively has live stores of resources which then poll for changes on the server side and then dispatches updates to the client via a new /events/live SSE endpoint.
172 lines
5.6 KiB
TypeScript
172 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 apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey);
|
|
const api = context.hsApi.getRuntimeClient(apiKey);
|
|
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.createUser(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.deleteUser(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.getUsers(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.renameUser(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.getUsers(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.getUsers(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.getUsers(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,
|
|
});
|
|
}
|
|
}
|