headplane/app/routes/users/overview.tsx

139 lines
4.1 KiB
TypeScript
Raw Normal View History

import { useEffect, useState } from 'react';
2024-12-31 10:31:50 +05:30
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
import { useLoaderData } from 'react-router';
2025-03-22 01:36:27 -04:00
import type { LoadContext } from '~/server';
import { Capabilities } from '~/server/web/roles';
import { Machine, User } from '~/types';
2025-01-28 17:46:16 -05:00
import cn from '~/utils/cn';
2025-02-19 13:01:20 -05:00
import ManageBanner from './components/manage-banner';
2025-03-29 14:12:15 -04:00
import UserRow from './components/user-row';
2025-02-19 13:01:20 -05:00
import { userAction } from './user-actions';
2024-05-15 21:52:19 -04:00
interface UserMachine extends User {
machines: Machine[];
}
2025-03-22 01:36:27 -04:00
export async function loader({
request,
context,
}: LoaderFunctionArgs<LoadContext>) {
const session = await context.sessions.auth(request);
const check = await context.sessions.check(request, Capabilities.read_users);
if (!check) {
// Not authorized to view this page
throw new Error(
'You do not have permission to view this page. Please contact your administrator.',
);
}
const writablePermission = await context.sessions.check(
request,
Capabilities.write_users,
);
2024-05-15 21:52:19 -04:00
const [machines, apiUsers] = await Promise.all([
context.client.get<{ nodes: Machine[] }>('v1/node', session.api_key),
context.client.get<{ users: User[] }>('v1/user', session.api_key),
2024-12-31 10:30:14 +05:30
]);
2024-05-15 21:52:19 -04:00
2024-12-31 10:30:14 +05:30
const users = apiUsers.users.map((user) => ({
2024-05-15 21:52:19 -04:00
...user,
2024-12-31 10:30:14 +05:30
machines: machines.nodes.filter((machine) => machine.user.id === user.id),
}));
2024-05-15 21:52:19 -04:00
const roles = await Promise.all(
users
.sort((a, b) => a.name.localeCompare(b.name))
.map(async (user) => {
if (user.provider !== 'oidc') {
return 'no-oidc';
2025-03-29 14:12:15 -04:00
}
if (user.provider === 'oidc' && user.providerId) {
// For some reason, headscale makes providerID a url where the
// last component is the subject, so we need to strip that out
const subject = user.providerId.split('/').pop();
if (!subject) {
return 'invalid-oidc';
}
const role = await context.sessions.roleForSubject(subject);
return role ?? 'no-role';
}
2025-03-29 14:12:15 -04:00
// No role means the user is not registered in Headplane, but they
// are in Headscale. We also need to handle what happens if someone
// logs into the UI and they don't have a Headscale setup.
return 'no-role';
}),
);
2025-03-29 14:12:15 -04:00
2024-12-31 10:30:14 +05:30
let magic: string | undefined;
2025-03-22 01:36:27 -04:00
if (context.hs.readable()) {
if (context.hs.c?.dns.magic_dns) {
magic = context.hs.c.dns.base_domain;
2024-05-15 21:52:19 -04:00
}
}
return {
writable: writablePermission, // whether the user can write to the API
2025-03-22 01:36:27 -04:00
oidc: context.config.oidc,
2025-03-29 14:12:15 -04:00
roles,
2024-05-15 21:52:19 -04:00
magic,
users,
2024-12-31 10:30:14 +05:30
};
2024-05-15 21:52:19 -04:00
}
2025-02-19 13:01:20 -05:00
export async function action(data: ActionFunctionArgs) {
return userAction(data);
2024-05-15 21:52:19 -04:00
}
export default function Page() {
2024-12-31 10:30:14 +05:30
const data = useLoaderData<typeof loader>();
2025-02-19 13:01:20 -05:00
const [users, setUsers] = useState<UserMachine[]>(data.users);
2024-05-15 21:52:19 -04:00
2025-02-19 13:01:20 -05:00
// This useEffect is entirely for the purpose of updating the users when the
// drag and drop changes the machines between users. It's pretty hacky, but
// the idea is to treat data.users as the source of truth and update the
// local state when it changes.
2024-05-15 21:52:19 -04:00
useEffect(() => {
2024-12-31 10:30:14 +05:30
setUsers(data.users);
}, [data.users]);
2024-05-15 21:52:19 -04:00
return (
<>
2024-12-31 10:30:14 +05:30
<h1 className="text-2xl font-medium mb-1.5">Users</h1>
2024-05-15 21:52:19 -04:00
<p className="mb-8 text-md">
2025-06-09 17:48:55 -04:00
Manage the users in your network and their permissions.
2024-05-15 21:52:19 -04:00
</p>
<ManageBanner isDisabled={!data.writable} oidc={data.oidc} />
2025-03-29 14:12:15 -04:00
<table className="table-auto w-full rounded-lg">
<thead className="text-headplane-600 dark:text-headplane-300">
<tr className="text-left px-0.5">
<th className="uppercase text-xs font-bold pb-2">User</th>
<th className="uppercase text-xs font-bold pb-2">Role</th>
<th className="uppercase text-xs font-bold pb-2">Created At</th>
<th className="uppercase text-xs font-bold pb-2">Last Seen</th>
</tr>
</thead>
<tbody
className={cn(
'divide-y divide-headplane-100 dark:divide-headplane-800 align-top',
'border-t border-headplane-100 dark:border-headplane-800',
)}
>
{users
.sort((a, b) => a.name.localeCompare(b.name))
.map((user) => (
<UserRow
key={user.id}
role={data.roles[users.indexOf(user)]}
user={user}
2025-03-29 14:12:15 -04:00
/>
))}
</tbody>
</table>
2024-05-15 21:52:19 -04:00
</>
2024-12-31 10:30:14 +05:30
);
2024-05-15 21:52:19 -04:00
}