Skip to content

Commit c700a04

Browse files
committed
Add key expiry disable/enable toggle
1 parent 7221f1c commit c700a04

4 files changed

Lines changed: 52 additions & 4 deletions

File tree

app/routes/machines/components/menu.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Cog, Ellipsis, SquareTerminal } from "lucide-react";
22
import { useState } from "react";
3+
import { useSubmit } from "react-router";
34

45
import Button from "~/components/button";
56
import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from "~/components/menu";
67
import type { User } from "~/types";
78
import cn from "~/utils/cn";
8-
import { PopulatedNode } from "~/utils/node-info";
9+
import { isNoExpiry, type PopulatedNode } from "~/utils/node-info";
910

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

@@ -156,15 +158,31 @@ export default function MachineMenu({
156158
</MenuTrigger>
157159
<MenuContent>
158160
<MenuItem onClick={() => setModal("rename")}>Edit machine name</MenuItem>
161+
<MenuItem
162+
onClick={() =>
163+
submit(
164+
{
165+
action_id: "toggle_expiry",
166+
node_id: node.id,
167+
disableExpiry: !isNoExpiry(node.expiry),
168+
},
169+
{ method: "post" },
170+
)
171+
}
172+
>
173+
{isNoExpiry(node.expiry) ? "Enable" : "Disable"} key expiry
174+
</MenuItem>
159175
<MenuItem onClick={() => setModal("routes")}>Edit route settings</MenuItem>
160176
<MenuItem onClick={() => setModal("tags")}>Edit ACL tags</MenuItem>
161177
{supportsNodeOwnerChange && (
162178
<MenuItem onClick={() => setModal("move")}>Change owner</MenuItem>
163179
)}
164180
<MenuSeparator />
165-
<MenuItem variant="danger" disabled={node.expired} onClick={() => setModal("expire")}>
166-
Expire
167-
</MenuItem>
181+
{!isNoExpiry(node.expiry) && (
182+
<MenuItem variant="danger" disabled={node.expired} onClick={() => setModal("expire")}>
183+
Expire
184+
</MenuItem>
185+
)}
168186
<MenuItem variant="danger" onClick={() => setModal("remove")}>
169187
Remove
170188
</MenuItem>

app/routes/machines/machine-actions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ export async function machineAction({ request, context }: Route.ActionArgs) {
9393
return { message: "Machine expired" };
9494
}
9595

96+
case "toggle_expiry": {
97+
const disableExpiry = String(formData.get("disableExpiry")) === "true";
98+
await api.nodes.toggleExpiry(nodeId, disableExpiry);
99+
await context.hsLive.refresh(nodesResource, api);
100+
return { message: "Machine expired" };
101+
}
102+
96103
case "update_tags": {
97104
const tags = formData.get("tags")?.toString().split(",") ?? [];
98105
if (tags.length === 0) {

app/server/headscale/api/resources/nodes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface NodeApi {
1919
expire(id: string): Promise<void>;
2020
rename(id: string, newName: string): Promise<void>;
2121
setTags(id: string, tags: string[]): Promise<void>;
22+
toggleExpiry(nodeId: string, disableExpiry: boolean): Promise<void>;
2223
/**
2324
* Reassign a node to a different user. Only present when
2425
* `capabilities.nodeOwnerIsImmutable` is false (Headscale < 0.28).
@@ -99,6 +100,13 @@ export function makeNodeApi(
99100
body: { tags },
100101
});
101102
},
103+
toggleExpiry: async (nodeId, disableExpiry) => {
104+
await transport.request({
105+
method: "POST",
106+
path: `v1/node/${nodeId}/expire?disableExpiry=${disableExpiry}`,
107+
apiKey,
108+
});
109+
},
102110
};
103111

104112
if (!capabilities.nodeOwnerIsImmutable) {

tests/integration/api/nodes.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,21 @@ describe.sequential.for(HS_VERSIONS)("Headscale %s: Users", (version) => {
6767
expect(expiredNode.expiry).toBeDefined();
6868
});
6969

70+
test("key expiry of nodes can be toggled", async () => {
71+
const client = await getRuntimeClient(version);
72+
await client.toggleExpiry(workingNodeId, true);
73+
74+
const permanentNode = await client.getNode(workingNodeId);
75+
expect(permanentNode).toBeDefined();
76+
expect(permanentNode.expiry).toBeNull();
77+
78+
await client.toggleExpiry(workingNodeId, false);
79+
80+
const node = await client.getNode(workingNodeId);
81+
expect(node).toBeDefined();
82+
expect(node.expiry).not.toBeNull();
83+
});
84+
7085
test("nodes can be deleted", async () => {
7186
const client = await getRuntimeClient(version);
7287
await client.nodes.delete(workingNodeId);

0 commit comments

Comments
 (0)