Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions app/routes/machines/components/menu.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Cog, Ellipsis, SquareTerminal } from "lucide-react";
import { useState } from "react";
import { useSubmit } from "react-router";

import Button from "~/components/button";
import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from "~/components/menu";
import type { User } from "~/types";
import cn from "~/utils/cn";
import { PopulatedNode } from "~/utils/node-info";
import { isNoExpiry, type PopulatedNode } from "~/utils/node-info";

import Delete from "../dialogs/delete";
import Expire from "../dialogs/expire";
Expand Down Expand Up @@ -35,6 +36,7 @@ export default function MachineMenu({
existingTags,
supportsNodeOwnerChange,
}: MenuProps) {
const submit = useSubmit();
const [modal, setModal] = useState<Modal>(null);
const supportsTailscaleSSH = node.hostInfo?.sshHostKeys && node.hostInfo?.sshHostKeys.length > 0;

Expand Down Expand Up @@ -156,15 +158,31 @@ export default function MachineMenu({
</MenuTrigger>
<MenuContent>
<MenuItem onClick={() => setModal("rename")}>Edit machine name</MenuItem>
<MenuItem
onClick={() =>
submit(
{
action_id: "toggle_expiry",
node_id: node.id,
disableExpiry: !isNoExpiry(node.expiry),
},
{ method: "post" },
)
}
>
{isNoExpiry(node.expiry) ? "Enable" : "Disable"} key expiry
</MenuItem>
<MenuItem onClick={() => setModal("routes")}>Edit route settings</MenuItem>
<MenuItem onClick={() => setModal("tags")}>Edit ACL tags</MenuItem>
{supportsNodeOwnerChange && (
<MenuItem onClick={() => setModal("move")}>Change owner</MenuItem>
)}
<MenuSeparator />
<MenuItem variant="danger" disabled={node.expired} onClick={() => setModal("expire")}>
Expire
</MenuItem>
{!isNoExpiry(node.expiry) && (
<MenuItem variant="danger" disabled={node.expired} onClick={() => setModal("expire")}>
Expire
</MenuItem>
)}
<MenuItem variant="danger" onClick={() => setModal("remove")}>
Remove
</MenuItem>
Expand Down
7 changes: 7 additions & 0 deletions app/routes/machines/machine-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ export async function machineAction({ request, context }: Route.ActionArgs) {
return { message: "Machine expired" };
}

case "toggle_expiry": {
const disableExpiry = String(formData.get("disableExpiry")) === "true";
await api.nodes.toggleExpiry(nodeId, disableExpiry);
await context.hsLive.refresh(nodesResource, api);
return { message: "Machine expired" };
}

case "update_tags": {
const tags = formData.get("tags")?.toString().split(",") ?? [];
if (tags.length === 0) {
Expand Down
8 changes: 8 additions & 0 deletions app/server/headscale/api/resources/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface NodeApi {
expire(id: string): Promise<void>;
rename(id: string, newName: string): Promise<void>;
setTags(id: string, tags: string[]): Promise<void>;
toggleExpiry(nodeId: string, disableExpiry: boolean): Promise<void>;
/**
* Reassign a node to a different user. Only present when
* `capabilities.nodeOwnerIsImmutable` is false (Headscale < 0.28).
Expand Down Expand Up @@ -99,6 +100,13 @@ export function makeNodeApi(
body: { tags },
});
},
toggleExpiry: async (nodeId, disableExpiry) => {
await transport.request({
method: "POST",
path: `v1/node/${nodeId}/expire?disableExpiry=${disableExpiry}`,
apiKey,
});
},
};

if (!capabilities.nodeOwnerIsImmutable) {
Expand Down
15 changes: 15 additions & 0 deletions tests/integration/api/nodes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@
expect(expiredNode.expiry).toBeDefined();
});

test("key expiry of nodes can be toggled", async () => {
const client = await getRuntimeClient(version);
await client.toggleExpiry(workingNodeId, true);

Check failure on line 72 in tests/integration/api/nodes.test.ts

View workflow job for this annotation

GitHub Actions / Build and Test

[integration:api] tests/integration/api/nodes.test.ts > Headscale 0.28.0: Users > key expiry of nodes can be toggled

TypeError: client.toggleExpiry is not a function ❯ tests/integration/api/nodes.test.ts:72:18

Check failure on line 72 in tests/integration/api/nodes.test.ts

View workflow job for this annotation

GitHub Actions / Build and Test

[integration:api] tests/integration/api/nodes.test.ts > Headscale 0.27.1: Users > key expiry of nodes can be toggled

TypeError: client.toggleExpiry is not a function ❯ tests/integration/api/nodes.test.ts:72:18

Check failure on line 72 in tests/integration/api/nodes.test.ts

View workflow job for this annotation

GitHub Actions / Build and Test

[integration:api] tests/integration/api/nodes.test.ts > Headscale 0.27.0: Users > key expiry of nodes can be toggled

TypeError: client.toggleExpiry is not a function ❯ tests/integration/api/nodes.test.ts:72:18

Check failure on line 72 in tests/integration/api/nodes.test.ts

View workflow job for this annotation

GitHub Actions / Build and Test

[integration:api] tests/integration/api/nodes.test.ts > Headscale 0.26.1: Users > key expiry of nodes can be toggled

TypeError: client.toggleExpiry is not a function ❯ tests/integration/api/nodes.test.ts:72:18

const permanentNode = await client.getNode(workingNodeId);
expect(permanentNode).toBeDefined();
expect(permanentNode.expiry).toBeNull();

await client.toggleExpiry(workingNodeId, false);

const node = await client.getNode(workingNodeId);
expect(node).toBeDefined();
expect(node.expiry).not.toBeNull();
});

test("nodes can be deleted", async () => {
const client = await getRuntimeClient(version);
await client.nodes.delete(workingNodeId);
Expand Down
Loading