mirror of
https://github.com/tale/headplane.git
synced 2026-07-27 16:18:57 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 623e7c03f1 |
@@ -1,7 +1,6 @@
|
|||||||
import type { Machine } from "~/types";
|
import type { Machine } from "~/types";
|
||||||
|
|
||||||
import type { HeadscaleApiInterface } from "..";
|
import type { HeadscaleApiInterface } from "..";
|
||||||
|
|
||||||
import { defineApiEndpoints } from "../factory";
|
import { defineApiEndpoints } from "../factory";
|
||||||
|
|
||||||
interface RawMachine extends Omit<Machine, "tags"> {
|
interface RawMachine extends Omit<Machine, "tags"> {
|
||||||
@@ -146,7 +145,11 @@ export default defineApiEndpoints<NodeEndpoints>((client, apiKey) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renameNode: async (nodeId, newName) => {
|
renameNode: async (nodeId, newName) => {
|
||||||
await client.apiFetch<void>("POST", `v1/node/${nodeId}/rename/${newName}`, apiKey);
|
await client.apiFetch<void>(
|
||||||
|
"POST",
|
||||||
|
`v1/node/${nodeId}/rename/${encodeURIComponent(newName)}`,
|
||||||
|
apiKey,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
setNodeTags: async (nodeId, tags) => {
|
setNodeTags: async (nodeId, tags) => {
|
||||||
|
|||||||
@@ -1,88 +1,87 @@
|
|||||||
import type { User } from '~/types';
|
import type { User } from "~/types";
|
||||||
import { defineApiEndpoints } from '../factory';
|
|
||||||
|
import { defineApiEndpoints } from "../factory";
|
||||||
|
|
||||||
export interface UserEndpoints {
|
export interface UserEndpoints {
|
||||||
/**
|
/**
|
||||||
* Retrieves users from the Headscale instance, optionally filtering by ID, name, or email.
|
* Retrieves users from the Headscale instance, optionally filtering by ID, name, or email.
|
||||||
*
|
*
|
||||||
* @param id Optional ID of the user to retrieve.
|
* @param id Optional ID of the user to retrieve.
|
||||||
* @param name Optional name of the user to retrieve.
|
* @param name Optional name of the user to retrieve.
|
||||||
* @param email Optional email of the user to retrieve.
|
* @param email Optional email of the user to retrieve.
|
||||||
* @returns An array of `User` objects representing the users.
|
* @returns An array of `User` objects representing the users.
|
||||||
*/
|
*/
|
||||||
getUsers(id?: string, name?: string, email?: string): Promise<User[]>;
|
getUsers(id?: string, name?: string, email?: string): Promise<User[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new user in the Headscale instance.
|
* Creates a new user in the Headscale instance.
|
||||||
*
|
*
|
||||||
* @param username The username of the new user.
|
* @param username The username of the new user.
|
||||||
* @param email Optional email of the new user.
|
* @param email Optional email of the new user.
|
||||||
* @param displayName Optional display name of the new user.
|
* @param displayName Optional display name of the new user.
|
||||||
* @param pictureUrl Optional picture URL of the new user.
|
* @param pictureUrl Optional picture URL of the new user.
|
||||||
* @returns A `User` object representing the newly created user.
|
* @returns A `User` object representing the newly created user.
|
||||||
*/
|
*/
|
||||||
createUser(
|
createUser(
|
||||||
username: string,
|
username: string,
|
||||||
email?: string,
|
email?: string,
|
||||||
displayName?: string,
|
displayName?: string,
|
||||||
pictureUrl?: string,
|
pictureUrl?: string,
|
||||||
): Promise<User>;
|
): Promise<User>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a specific user by its ID.
|
* Deletes a specific user by its ID.
|
||||||
*
|
*
|
||||||
* @param id The ID of the user to delete.
|
* @param id The ID of the user to delete.
|
||||||
*/
|
*/
|
||||||
deleteUser(id: string): Promise<void>;
|
deleteUser(id: string): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renames a specific user by its ID.
|
* Renames a specific user by its ID.
|
||||||
*
|
*
|
||||||
* @param id The ID of the user to rename.
|
* @param id The ID of the user to rename.
|
||||||
* @param newName The new name for the user.
|
* @param newName The new name for the user.
|
||||||
*/
|
*/
|
||||||
renameUser(id: string, newName: string): Promise<void>;
|
renameUser(id: string, newName: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineApiEndpoints<UserEndpoints>((client, apiKey) => ({
|
export default defineApiEndpoints<UserEndpoints>((client, apiKey) => ({
|
||||||
getUsers: async (id, name, email) => {
|
getUsers: async (id, name, email) => {
|
||||||
const moreThanOneFilter =
|
const moreThanOneFilter = [id, name, email].filter((v) => v !== undefined).length > 1;
|
||||||
[id, name, email].filter((v) => v !== undefined).length > 1;
|
|
||||||
|
|
||||||
if (moreThanOneFilter) {
|
if (moreThanOneFilter) {
|
||||||
throw new Error('Only one of id, name, or email filters can be provided');
|
throw new Error("Only one of id, name, or email filters can be provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { users } = await client.apiFetch<{ users: User[] }>(
|
const { users } = await client.apiFetch<{ users: User[] }>("GET", "v1/user", apiKey, {
|
||||||
'GET',
|
id,
|
||||||
'v1/user',
|
name,
|
||||||
apiKey,
|
email,
|
||||||
{ id, name, email },
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
},
|
},
|
||||||
|
|
||||||
createUser: async (username, email, displayName, pictureUrl) => {
|
createUser: async (username, email, displayName, pictureUrl) => {
|
||||||
const { user } = await client.apiFetch<{ user: User }>(
|
const { user } = await client.apiFetch<{ user: User }>("POST", "v1/user", apiKey, {
|
||||||
'POST',
|
name: username,
|
||||||
'v1/user',
|
email,
|
||||||
apiKey,
|
displayName,
|
||||||
{ name: username, email, displayName, pictureUrl },
|
pictureUrl,
|
||||||
);
|
});
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteUser: async (id) => {
|
deleteUser: async (id) => {
|
||||||
await client.apiFetch<void>('DELETE', `v1/user/${id}`, apiKey);
|
await client.apiFetch<void>("DELETE", `v1/user/${id}`, apiKey);
|
||||||
},
|
},
|
||||||
|
|
||||||
renameUser: async (oldId, newName) => {
|
renameUser: async (oldId, newName) => {
|
||||||
await client.apiFetch<void>(
|
await client.apiFetch<void>(
|
||||||
'POST',
|
"POST",
|
||||||
`v1/user/${oldId}/rename/${newName}`,
|
`v1/user/${oldId}/rename/${encodeURIComponent(newName)}`,
|
||||||
apiKey,
|
apiKey,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "headplane",
|
"name": "headplane",
|
||||||
"version": "0.6.2",
|
"version": "0.6.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import type { HeadscaleApiInterface } from "~/server/headscale/api";
|
||||||
|
import nodeEndpoints from "~/server/headscale/api/endpoints/nodes";
|
||||||
|
import userEndpoints from "~/server/headscale/api/endpoints/users";
|
||||||
|
|
||||||
|
type ApiFetchArgs = Parameters<HeadscaleApiInterface["clientHelpers"]["apiFetch"]>;
|
||||||
|
|
||||||
|
function createApiClientRecorder() {
|
||||||
|
const calls: Array<{
|
||||||
|
method: ApiFetchArgs[0];
|
||||||
|
apiPath: ApiFetchArgs[1];
|
||||||
|
apiKey: ApiFetchArgs[2];
|
||||||
|
bodyOrQuery: ApiFetchArgs[3];
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
const client = {
|
||||||
|
isAtleast: () => false,
|
||||||
|
rawFetch: async () => {
|
||||||
|
throw new Error("rawFetch should not be called");
|
||||||
|
},
|
||||||
|
apiFetch: async <T>(
|
||||||
|
method: ApiFetchArgs[0],
|
||||||
|
apiPath: ApiFetchArgs[1],
|
||||||
|
apiKey: ApiFetchArgs[2],
|
||||||
|
bodyOrQuery?: ApiFetchArgs[3],
|
||||||
|
) => {
|
||||||
|
calls.push({ method, apiPath, apiKey, bodyOrQuery });
|
||||||
|
return undefined as T;
|
||||||
|
},
|
||||||
|
} as HeadscaleApiInterface["clientHelpers"];
|
||||||
|
|
||||||
|
return { calls, client };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Headscale API path encoding", () => {
|
||||||
|
test("encodes node rename names as a single URL segment", async () => {
|
||||||
|
const { calls, client } = createApiClientRecorder();
|
||||||
|
|
||||||
|
await nodeEndpoints(client, "api-key").renameNode("2", "../../1/expire");
|
||||||
|
|
||||||
|
expect(calls).toEqual([
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
apiPath: "v1/node/2/rename/..%2F..%2F1%2Fexpire",
|
||||||
|
apiKey: "api-key",
|
||||||
|
bodyOrQuery: undefined,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("encodes user rename names as a single URL segment", async () => {
|
||||||
|
const { calls, client } = createApiClientRecorder();
|
||||||
|
|
||||||
|
await userEndpoints(client, "api-key").renameUser("3", "../../../user/4/rename/pwned-user@");
|
||||||
|
|
||||||
|
expect(calls).toEqual([
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
apiPath: "v1/user/3/rename/..%2F..%2F..%2Fuser%2F4%2Frename%2Fpwned-user%40",
|
||||||
|
apiKey: "api-key",
|
||||||
|
bodyOrQuery: undefined,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user