mirror of
https://github.com/tale/headplane.git
synced 2026-08-29 00:17:13 +00:00
chore: general cleanup/pr improvements
This commit is contained in:
@@ -47,9 +47,8 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
configAvailable: context.hs.readable(),
|
configAvailable: context.hs.readable(),
|
||||||
debug: context.config.debug,
|
debug: context.config.debug,
|
||||||
user: session.user,
|
user: session.user,
|
||||||
uiAccess: check,
|
|
||||||
access: {
|
access: {
|
||||||
ui: await context.sessions.check(request, Capabilities.ui_access),
|
ui: check,
|
||||||
dns: await context.sessions.check(request, Capabilities.read_network),
|
dns: await context.sessions.check(request, Capabilities.read_network),
|
||||||
users: await context.sessions.check(request, Capabilities.read_users),
|
users: await context.sessions.check(request, Capabilities.read_users),
|
||||||
policy: await context.sessions.check(request, Capabilities.read_policy),
|
policy: await context.sessions.check(request, Capabilities.read_policy),
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { ClockIcon, LogOut, RefreshCw, UserCheck } from "lucide-react";
|
import { ClockIcon, LogOut, RefreshCw, UserCheck } from "lucide-react";
|
||||||
import { Form, redirect } from "react-router";
|
import { Form, redirect } from "react-router";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import Card from "~/components/Card";
|
import Card from "~/components/Card";
|
||||||
import { users } from "~/server/db/schema";
|
|
||||||
import { Capabilities } from "~/server/web/roles";
|
import { Capabilities } from "~/server/web/roles";
|
||||||
import toast from "~/utils/toast";
|
import toast from "~/utils/toast";
|
||||||
|
|
||||||
@@ -24,18 +22,11 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
return redirect("/machines");
|
return redirect("/machines");
|
||||||
}
|
}
|
||||||
|
|
||||||
const [user] = await context.db
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.sub, session.user.subject))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const url = context.config.headscale.public_url ?? context.config.headscale.url;
|
const url = context.config.headscale.public_url ?? context.config.headscale.url;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: session.user,
|
user: session.user,
|
||||||
url,
|
url,
|
||||||
exists: !!user,
|
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return redirect("/login", {
|
return redirect("/login", {
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { ChevronDown, Copy } from "lucide-react";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
import type { User } from "~/types";
|
|
||||||
|
|
||||||
import Chip from "~/components/Chip";
|
import Chip from "~/components/Chip";
|
||||||
import Menu from "~/components/Menu";
|
import Menu from "~/components/Menu";
|
||||||
import StatusCircle from "~/components/StatusCircle";
|
import StatusCircle from "~/components/StatusCircle";
|
||||||
@@ -12,11 +10,13 @@ import { ExpiryTag } from "~/components/tags/Expiry";
|
|||||||
import { HeadplaneAgentTag } from "~/components/tags/HeadplaneAgent";
|
import { HeadplaneAgentTag } from "~/components/tags/HeadplaneAgent";
|
||||||
import { SubnetTag } from "~/components/tags/Subnet";
|
import { SubnetTag } from "~/components/tags/Subnet";
|
||||||
import { TailscaleSSHTag } from "~/components/tags/TailscaleSSH";
|
import { TailscaleSSHTag } from "~/components/tags/TailscaleSSH";
|
||||||
|
import type { User } from "~/types";
|
||||||
import cn from "~/utils/cn";
|
import cn from "~/utils/cn";
|
||||||
import * as hinfo from "~/utils/host-info";
|
import * as hinfo from "~/utils/host-info";
|
||||||
import { PopulatedNode } from "~/utils/node-info";
|
import { PopulatedNode } from "~/utils/node-info";
|
||||||
import { formatTimeDelta } from "~/utils/time";
|
import { formatTimeDelta } from "~/utils/time";
|
||||||
import toast from "~/utils/toast";
|
import toast from "~/utils/toast";
|
||||||
|
import { getUserDisplayName } from "~/utils/user";
|
||||||
|
|
||||||
import MenuOptions from "./menu";
|
import MenuOptions from "./menu";
|
||||||
|
|
||||||
@@ -63,9 +63,7 @@ export default function MachineRow({
|
|||||||
{node.givenName}
|
{node.givenName}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm opacity-50">
|
<p className="text-sm opacity-50">
|
||||||
{node.user
|
{node.user ? getUserDisplayName(node.user) : "Tag-owned"}
|
||||||
? node.user.name || node.user.displayName || node.user.email || node.user.id
|
|
||||||
: "Tag-owned"}
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-1.5 flex flex-wrap gap-1">
|
<div className="mt-1.5 flex flex-wrap gap-1">
|
||||||
{mapTagsToComponents(node, uiTags)}
|
{mapTagsToComponents(node, uiTags)}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Key, useState } from "react";
|
import { Key, useState } from "react";
|
||||||
|
|
||||||
import type { Machine, User } from "~/types";
|
|
||||||
|
|
||||||
import Dialog from "~/components/Dialog";
|
import Dialog from "~/components/Dialog";
|
||||||
import Select from "~/components/Select";
|
import Select from "~/components/Select";
|
||||||
|
import type { Machine, User } from "~/types";
|
||||||
|
import { getUserDisplayName } from "~/utils/user";
|
||||||
|
|
||||||
interface MoveProps {
|
interface MoveProps {
|
||||||
machine: Machine;
|
machine: Machine;
|
||||||
@@ -34,9 +34,7 @@ export default function Move({ machine, users, isOpen, setIsOpen }: MoveProps) {
|
|||||||
placeholder="Select a user"
|
placeholder="Select a user"
|
||||||
>
|
>
|
||||||
{users.map((user) => (
|
{users.map((user) => (
|
||||||
<Select.Item key={user.id}>
|
<Select.Item key={user.id}>{getUserDisplayName(user)}</Select.Item>
|
||||||
{user.name || user.displayName || user.email || user.id}
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
|||||||
@@ -1,92 +1,86 @@
|
|||||||
import { Computer, FileKey2 } from 'lucide-react';
|
import { Computer, FileKey2 } from "lucide-react";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from "react-router";
|
||||||
import Code from '~/components/Code';
|
|
||||||
import Dialog from '~/components/Dialog';
|
import Code from "~/components/Code";
|
||||||
import Input from '~/components/Input';
|
import Dialog from "~/components/Dialog";
|
||||||
import Menu from '~/components/Menu';
|
import Input from "~/components/Input";
|
||||||
import Select from '~/components/Select';
|
import Menu from "~/components/Menu";
|
||||||
import type { User } from '~/types';
|
import Select from "~/components/Select";
|
||||||
|
import type { User } from "~/types";
|
||||||
|
import { getUserDisplayName } from "~/utils/user";
|
||||||
|
|
||||||
export interface NewMachineProps {
|
export interface NewMachineProps {
|
||||||
server: string;
|
server: string;
|
||||||
users: User[];
|
users: User[];
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
disabledKeys?: string[];
|
disabledKeys?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NewMachine(data: NewMachineProps) {
|
export default function NewMachine(data: NewMachineProps) {
|
||||||
const [pushDialog, setPushDialog] = useState(false);
|
const [pushDialog, setPushDialog] = useState(false);
|
||||||
const [mkey, setMkey] = useState('');
|
const [mkey, setMkey] = useState("");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const isMkeyInvalid = mkey.length > 0 && mkey.length !== 24;
|
const isMkeyInvalid = mkey.length > 0 && mkey.length !== 24;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog isOpen={pushDialog} onOpenChange={setPushDialog}>
|
<Dialog isOpen={pushDialog} onOpenChange={setPushDialog}>
|
||||||
<Dialog.Panel isDisabled={mkey.length !== 24}>
|
<Dialog.Panel isDisabled={mkey.length !== 24}>
|
||||||
<Dialog.Title>Register Machine Key</Dialog.Title>
|
<Dialog.Title>Register Machine Key</Dialog.Title>
|
||||||
<Dialog.Text className="mb-4">
|
<Dialog.Text className="mb-4">
|
||||||
The machine key is given when you run{' '}
|
The machine key is given when you run{" "}
|
||||||
<Code isCopyable>tailscale up --login-server={data.server}</Code> on
|
<Code isCopyable>tailscale up --login-server={data.server}</Code> on your device.
|
||||||
your device.
|
</Dialog.Text>
|
||||||
</Dialog.Text>
|
<input name="action_id" type="hidden" value="register" />
|
||||||
<input name="action_id" type="hidden" value="register" />
|
<Input
|
||||||
<Input
|
errorMessage="Machine key must be exactly 24 characters"
|
||||||
errorMessage="Machine key must be exactly 24 characters"
|
isInvalid={isMkeyInvalid}
|
||||||
isInvalid={isMkeyInvalid}
|
isRequired
|
||||||
isRequired
|
label="Machine Key"
|
||||||
label="Machine Key"
|
name="register_key"
|
||||||
name="register_key"
|
onChange={setMkey}
|
||||||
onChange={setMkey}
|
placeholder="AbCd..."
|
||||||
placeholder="AbCd..."
|
validationBehavior="native"
|
||||||
validationBehavior="native"
|
/>
|
||||||
/>
|
<Select isRequired label="Owner" name="user" placeholder="Select a user">
|
||||||
<Select
|
{data.users.map((user) => (
|
||||||
isRequired
|
<Select.Item key={user.id}>{getUserDisplayName(user)}</Select.Item>
|
||||||
label="Owner"
|
))}
|
||||||
name="user"
|
</Select>
|
||||||
placeholder="Select a user"
|
</Dialog.Panel>
|
||||||
>
|
</Dialog>
|
||||||
{data.users.map((user) => (
|
<Menu disabledKeys={data.disabledKeys} isDisabled={data.isDisabled}>
|
||||||
<Select.Item key={user.id}>
|
<Menu.Button variant="heavy">Add Device</Menu.Button>
|
||||||
{user.name || user.displayName || user.email || user.id}
|
<Menu.Panel
|
||||||
</Select.Item>
|
onAction={(key) => {
|
||||||
))}
|
if (key === "register") {
|
||||||
</Select>
|
setPushDialog(true);
|
||||||
</Dialog.Panel>
|
return;
|
||||||
</Dialog>
|
}
|
||||||
<Menu disabledKeys={data.disabledKeys} isDisabled={data.isDisabled}>
|
|
||||||
<Menu.Button variant="heavy">Add Device</Menu.Button>
|
|
||||||
<Menu.Panel
|
|
||||||
onAction={(key) => {
|
|
||||||
if (key === 'register') {
|
|
||||||
setPushDialog(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'pre-auth') {
|
if (key === "pre-auth") {
|
||||||
navigate('/settings/auth-keys');
|
navigate("/settings/auth-keys");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu.Section>
|
<Menu.Section>
|
||||||
<Menu.Item key="register" textValue="Register Machine Key">
|
<Menu.Item key="register" textValue="Register Machine Key">
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
<Computer className="w-4" />
|
<Computer className="w-4" />
|
||||||
Register Machine Key
|
Register Machine Key
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="pre-auth" textValue="Generate Pre-auth Key">
|
<Menu.Item key="pre-auth" textValue="Generate Pre-auth Key">
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
<FileKey2 className="w-4" />
|
<FileKey2 className="w-4" />
|
||||||
Generate Pre-auth Key
|
Generate Pre-auth Key
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Section>
|
</Menu.Section>
|
||||||
</Menu.Panel>
|
</Menu.Panel>
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,9 +60,10 @@ export async function machineAction({ request, context }: Route.ActionArgs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tag-only nodes (Headscale 0.28+) have no user, so we rely on role-based permissions
|
// Tag-only nodes (Headscale 0.28+) have no user — only role-based permissions apply
|
||||||
const nodeOwnerId = node.user?.providerId?.split("/").pop();
|
const nodeOwnerId = node.user?.providerId?.split("/").pop();
|
||||||
if (nodeOwnerId !== session.user.subject && !check) {
|
const isOwner = nodeOwnerId !== undefined && nodeOwnerId === session.user.subject;
|
||||||
|
if (!isOwner && !check) {
|
||||||
throw data("You do not have permission to act on this machine", {
|
throw data("You do not have permission to act on this machine", {
|
||||||
status: 403,
|
status: 403,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import Tooltip from "~/components/Tooltip";
|
|||||||
import cn from "~/utils/cn";
|
import cn from "~/utils/cn";
|
||||||
import { getOSInfo, getTSVersion } from "~/utils/host-info";
|
import { getOSInfo, getTSVersion } from "~/utils/host-info";
|
||||||
import { mapNodes, sortNodeTags } from "~/utils/node-info";
|
import { mapNodes, sortNodeTags } from "~/utils/node-info";
|
||||||
|
import { getUserDisplayName } from "~/utils/user";
|
||||||
|
|
||||||
import type { Route } from "./+types/machine";
|
import type { Route } from "./+types/machine";
|
||||||
|
|
||||||
import { mapTagsToComponents, uiTagsForNode } from "./components/machine-row";
|
import { mapTagsToComponents, uiTagsForNode } from "./components/machine-row";
|
||||||
import MenuOptions from "./components/menu";
|
import MenuOptions from "./components/menu";
|
||||||
import Routes from "./dialogs/routes";
|
import Routes from "./dialogs/routes";
|
||||||
@@ -109,9 +109,7 @@ export default function Page({
|
|||||||
</span>
|
</span>
|
||||||
<div className="mt-1 flex items-center gap-x-2.5">
|
<div className="mt-1 flex items-center gap-x-2.5">
|
||||||
<UserCircle />
|
<UserCircle />
|
||||||
{node.user
|
{node.user ? getUserDisplayName(node.user) : "Tag-owned"}
|
||||||
? node.user.name || node.user.displayName || node.user.email || node.user.id
|
|
||||||
: "Tag-owned"}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 pl-4">
|
<div className="p-2 pl-4">
|
||||||
@@ -243,11 +241,7 @@ export default function Page({
|
|||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<Attribute
|
<Attribute
|
||||||
name="Creator"
|
name="Creator"
|
||||||
value={
|
value={node.user ? getUserDisplayName(node.user) : "Tag-owned"}
|
||||||
node.user
|
|
||||||
? node.user.name || node.user.displayName || node.user.email || node.user.id
|
|
||||||
: "Tag-owned"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Attribute name="Machine name" value={node.givenName} />
|
<Attribute name="Machine name" value={node.givenName} />
|
||||||
<Attribute
|
<Attribute
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ export async function authKeysAction({ request, context }: Route.ActionArgs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = targetUser.providerId?.split("/").pop();
|
||||||
|
if (targetSubject !== session.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 formData = await request.formData();
|
||||||
const action = formData.get("action_id")?.toString();
|
const action = formData.get("action_id")?.toString();
|
||||||
if (!action) {
|
if (!action) {
|
||||||
@@ -40,18 +54,8 @@ export async function authKeysAction({ request, context }: Route.ActionArgs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow self-service users to create keys for themselves
|
if (user) {
|
||||||
if (!canGenerateAny && canGenerateOwn && user) {
|
await checkSelfServiceOwnership(user);
|
||||||
const [targetUser] = await api.getUsers(user);
|
|
||||||
if (!targetUser) {
|
|
||||||
return data("User not found.", { status: 404 });
|
|
||||||
}
|
|
||||||
const targetSubject = targetUser.providerId?.split("/").pop();
|
|
||||||
if (targetSubject !== session.user.subject) {
|
|
||||||
throw data("You can only create pre-auth keys for your own user", {
|
|
||||||
status: 403,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const expiry = formData.get("expiry")?.toString();
|
const expiry = formData.get("expiry")?.toString();
|
||||||
@@ -104,19 +108,7 @@ export async function authKeysAction({ request, context }: Route.ActionArgs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow self-service users to expire their own keys
|
await checkSelfServiceOwnership(user);
|
||||||
if (!canGenerateAny && canGenerateOwn) {
|
|
||||||
const [targetUser] = await api.getUsers(user);
|
|
||||||
if (!targetUser) {
|
|
||||||
return data("User not found.", { status: 404 });
|
|
||||||
}
|
|
||||||
const targetSubject = targetUser.providerId?.split("/").pop();
|
|
||||||
if (targetSubject !== session.user.subject) {
|
|
||||||
throw data("You can only expire pre-auth keys for your own user", {
|
|
||||||
status: 403,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.expirePreAuthKey(user, key);
|
await api.expirePreAuthKey(user, key);
|
||||||
return data("Pre-auth key expired");
|
return data("Pre-auth key expired");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PreAuthKey, User } from "~/types";
|
|
||||||
|
|
||||||
import Attribute from "~/components/Attribute";
|
import Attribute from "~/components/Attribute";
|
||||||
|
import type { PreAuthKey, User } from "~/types";
|
||||||
|
import { getUserDisplayName } from "~/utils/user";
|
||||||
|
|
||||||
import ExpireAuthKey from "./dialogs/expire-auth-key";
|
import ExpireAuthKey from "./dialogs/expire-auth-key";
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export default function AuthKeyRow({ authKey, user }: Props) {
|
|||||||
const expiration = new Date(authKey.expiration).toLocaleString();
|
const expiration = new Date(authKey.expiration).toLocaleString();
|
||||||
const isExpired =
|
const isExpired =
|
||||||
(authKey.used && !authKey.reusable) || new Date(authKey.expiration) < new Date();
|
(authKey.used && !authKey.reusable) || new Date(authKey.expiration) < new Date();
|
||||||
const userDisplay = user ? user.name || user.displayName || user.email || user.id : "(Tag Only)";
|
const userDisplay = user ? getUserDisplayName(user) : "(Tag Only)";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Key, useEffect, useRef, useState } from "react";
|
import { Key, useEffect, useRef, useState } from "react";
|
||||||
import { useFetcher } from "react-router";
|
import { useFetcher } from "react-router";
|
||||||
|
|
||||||
import type { User } from "~/types";
|
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import Code from "~/components/Code";
|
import Code from "~/components/Code";
|
||||||
import Dialog from "~/components/Dialog";
|
import Dialog from "~/components/Dialog";
|
||||||
@@ -11,7 +9,9 @@ import Link from "~/components/Link";
|
|||||||
import NumberInput from "~/components/NumberInput";
|
import NumberInput from "~/components/NumberInput";
|
||||||
import Select from "~/components/Select";
|
import Select from "~/components/Select";
|
||||||
import Switch from "~/components/Switch";
|
import Switch from "~/components/Switch";
|
||||||
|
import type { User } from "~/types";
|
||||||
import toast from "~/utils/toast";
|
import toast from "~/utils/toast";
|
||||||
|
import { getUserDisplayName } from "~/utils/user";
|
||||||
|
|
||||||
interface AddAuthKeyProps {
|
interface AddAuthKeyProps {
|
||||||
users: User[];
|
users: User[];
|
||||||
@@ -152,9 +152,7 @@ export default function AddAuthKey({
|
|||||||
selectedKey={userId}
|
selectedKey={userId}
|
||||||
>
|
>
|
||||||
{availableUsers.map((user) => (
|
{availableUsers.map((user) => (
|
||||||
<Select.Item key={user.id}>
|
<Select.Item key={user.id}>{getUserDisplayName(user)}</Select.Item>
|
||||||
{user.name || user.displayName || user.email || user.id}
|
|
||||||
</Select.Item>
|
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,20 +2,18 @@ import { FileKey2 } from "lucide-react";
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Link as RemixLink } from "react-router";
|
import { Link as RemixLink } from "react-router";
|
||||||
|
|
||||||
import type { PreAuthKey } from "~/types";
|
|
||||||
import type { User } from "~/types/User";
|
|
||||||
|
|
||||||
import Code from "~/components/Code";
|
import Code from "~/components/Code";
|
||||||
import Link from "~/components/Link";
|
import Link from "~/components/Link";
|
||||||
import Notice from "~/components/Notice";
|
import Notice from "~/components/Notice";
|
||||||
import Select from "~/components/Select";
|
import Select from "~/components/Select";
|
||||||
import TableList from "~/components/TableList";
|
import TableList from "~/components/TableList";
|
||||||
import { Capabilities } from "~/server/web/roles";
|
import { Capabilities } from "~/server/web/roles";
|
||||||
|
import type { PreAuthKey } from "~/types";
|
||||||
|
import type { User } from "~/types/User";
|
||||||
import log from "~/utils/log";
|
import log from "~/utils/log";
|
||||||
import { filterUsersWithValidIds, getUserDisplayName } from "~/utils/user";
|
import { getUserDisplayName } from "~/utils/user";
|
||||||
|
|
||||||
import type { Route } from "./+types/overview";
|
import type { Route } from "./+types/overview";
|
||||||
|
|
||||||
import { authKeysAction } from "./actions";
|
import { authKeysAction } from "./actions";
|
||||||
import AuthKeyRow from "./auth-key-row";
|
import AuthKeyRow from "./auth-key-row";
|
||||||
import AddAuthKey from "./dialogs/add-auth-key";
|
import AddAuthKey from "./dialogs/add-auth-key";
|
||||||
@@ -63,15 +61,17 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
| { success: false; user: User; error: unknown; preAuthKeys: [] };
|
| { success: false; user: User; error: unknown; preAuthKeys: [] };
|
||||||
|
|
||||||
const results: FetchResult[] = await Promise.all(
|
const results: FetchResult[] = await Promise.all(
|
||||||
filterUsersWithValidIds(users).map(async (user) => {
|
users
|
||||||
try {
|
.filter((u) => u.id?.length > 0)
|
||||||
const preAuthKeys = await api.getPreAuthKeys(user.id);
|
.map(async (user) => {
|
||||||
return { success: true as const, user, preAuthKeys };
|
try {
|
||||||
} catch (error) {
|
const preAuthKeys = await api.getPreAuthKeys(user.id);
|
||||||
log.error("api", "GET /v1/preauthkey for %s: %o", user.name, error);
|
return { success: true as const, user, preAuthKeys };
|
||||||
return { success: false as const, user, error, preAuthKeys: [] as const };
|
} catch (error) {
|
||||||
}
|
log.error("api", "GET /v1/preauthkey for %s: %o", user.name, error);
|
||||||
}),
|
return { success: false as const, user, error, preAuthKeys: [] as const };
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
keys = results
|
keys = results
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import type { User } from "~/types/User";
|
import type { User } from "~/types/User";
|
||||||
|
|
||||||
// Filter users with valid IDs (OIDC users may not have a name)
|
|
||||||
export function filterUsersWithValidIds(users: User[]): User[] {
|
|
||||||
return users.filter((user) => user.id?.length > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get display name with fallback: name -> displayName -> email -> id
|
|
||||||
export function getUserDisplayName(user: User): string {
|
export function getUserDisplayName(user: User): string {
|
||||||
return user.name || user.displayName || user.email || user.id;
|
return user.name || user.displayName || user.email || user.id;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user