mirror of
https://github.com/tale/headplane.git
synced 2026-08-12 06:46:51 +00:00
feat: create type-safe API object for Headscale
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import type { Key } from '~/types';
|
||||
import { defineApiEndpoints } from '../factory';
|
||||
|
||||
export interface ApiKeyEndpoints {
|
||||
/**
|
||||
* Retrieves all API keys from the Headscale instance.
|
||||
*
|
||||
* @returns An array of `Key` objects representing the API keys.
|
||||
*/
|
||||
getApiKeys(): Promise<Key[]>;
|
||||
}
|
||||
|
||||
export default defineApiEndpoints<ApiKeyEndpoints>((client, apiKey) => ({
|
||||
getApiKeys: async () => {
|
||||
if (client.isAtleast('0.27.0')) {
|
||||
console.log('wow we are at least 0.27.0!');
|
||||
}
|
||||
|
||||
const { apiKeys } = await client.apiFetch<{ apiKeys: Key[] }>(
|
||||
'GET',
|
||||
'v1/apikey',
|
||||
apiKey,
|
||||
);
|
||||
|
||||
return apiKeys;
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
composeEndpoints,
|
||||
type ExtractApiEndpoints,
|
||||
type UnionToIntersection,
|
||||
} from '../factory';
|
||||
import type { HeadscaleApiInterface } from '../index';
|
||||
import apiKeyEndpoints from './api-keys';
|
||||
import nodeEndpoints from './nodes';
|
||||
import policyEndpoints from './policy';
|
||||
import preAuthKeyEndpoints from './pre-auth-keys';
|
||||
import userEndpoints from './users';
|
||||
|
||||
/**
|
||||
* A constant list of all endpoint groups.
|
||||
* Add new endpoint groups here.
|
||||
*/
|
||||
export const endpointSets = [
|
||||
apiKeyEndpoints,
|
||||
nodeEndpoints,
|
||||
policyEndpoints,
|
||||
preAuthKeyEndpoints,
|
||||
userEndpoints,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* All of the available API methods when interacting with Headscale's API.
|
||||
* We have wrapped each operation with nice methods and parameters to make it
|
||||
* easier to do integration testing by spinning up an actual Headscale instance
|
||||
* and calling these methods against it.
|
||||
*
|
||||
* We also have the benefit of supporting multiple Headscale versions by
|
||||
* passing in different internal implementations based on the OpenAPI spec.
|
||||
*/
|
||||
export type RuntimeApiClient = UnionToIntersection<
|
||||
ExtractApiEndpoints<(typeof endpointSets)[number]>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Composes all endpoint groups into a single runtime API client.
|
||||
*
|
||||
* @param client - The client helpers for making API requests.
|
||||
* @param apiKey - The API key for authentication.
|
||||
* @returns A fully composed runtime API client.
|
||||
*/
|
||||
export default (
|
||||
client: HeadscaleApiInterface['clientHelpers'],
|
||||
apiKey: string,
|
||||
) => composeEndpoints(endpointSets, client, apiKey);
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { Machine } from '~/types';
|
||||
import { defineApiEndpoints } from '../factory';
|
||||
|
||||
export interface NodeEndpoints {
|
||||
/**
|
||||
* Retrieves all nodes (machines) from the Headscale instance.
|
||||
*
|
||||
* @returns An array of `Machine` objects representing the nodes.
|
||||
*/
|
||||
getNodes(): Promise<Machine[]>;
|
||||
|
||||
/**
|
||||
* Retrieves a specific node (machine) by its ID.
|
||||
*
|
||||
* @param id The ID of the node to retrieve.
|
||||
* @returns A `Machine` object representing the node.
|
||||
*/
|
||||
getNode(id: string): Promise<Machine>;
|
||||
|
||||
/**
|
||||
* Deletes a specific node (machine) by its ID.
|
||||
*
|
||||
* @param id The ID of the node to delete.
|
||||
*/
|
||||
deleteNode(id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Registers a new node (machine) with the given user and key.
|
||||
*
|
||||
* @param user The user to associate with the node.
|
||||
* @param key The registration key for the node.
|
||||
* @returns A `Machine` object representing the newly registered node.
|
||||
*/
|
||||
registerNode(user: string, key: string): Promise<Machine>;
|
||||
|
||||
/**
|
||||
* Approves routes for a specific node (machine) by its ID.
|
||||
*
|
||||
* @param id The ID of the node.
|
||||
* @param routes An array of routes to approve for the node.
|
||||
*/
|
||||
approveNodeRoutes(id: string, routes: string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Expires a specific node (machine) by its ID.
|
||||
*
|
||||
* @param id The ID of the node to expire.
|
||||
*/
|
||||
expireNode(id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Renames a specific node (machine) by its ID.
|
||||
*
|
||||
* @param id The ID of the node to rename.
|
||||
* @param newName The new name for the node.
|
||||
*/
|
||||
renameNode(id: string, newName: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Sets tags for a specific node (machine) by its ID.
|
||||
*
|
||||
* @param id The ID of the node.
|
||||
* @param tags An array of tags to set for the node.
|
||||
*/
|
||||
setNodeTags(id: string, tags: string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Sets the user for a specific node (machine) by its ID.
|
||||
*
|
||||
* @param id The ID of the node.
|
||||
* @param user The user to set for the node.
|
||||
*/
|
||||
setNodeUser(id: string, user: string): Promise<void>;
|
||||
}
|
||||
|
||||
export default defineApiEndpoints<NodeEndpoints>((client, apiKey) => ({
|
||||
getNodes: async () => {
|
||||
const { nodes } = await client.apiFetch<{ nodes: Machine[] }>(
|
||||
'GET',
|
||||
'v1/node',
|
||||
apiKey,
|
||||
);
|
||||
|
||||
return nodes;
|
||||
},
|
||||
|
||||
getNode: async (nodeId) => {
|
||||
const { node } = await client.apiFetch<{ node: Machine }>(
|
||||
'GET',
|
||||
`v1/node/${nodeId}`,
|
||||
apiKey,
|
||||
);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
deleteNode: async (nodeId) => {
|
||||
await client.apiFetch<void>('DELETE', `v1/node/${nodeId}`, apiKey);
|
||||
},
|
||||
|
||||
registerNode: async (user, key) => {
|
||||
const qp = new URLSearchParams();
|
||||
qp.append('user', user);
|
||||
qp.append('key', key);
|
||||
const { node } = await client.apiFetch<{ node: Machine }>(
|
||||
'POST',
|
||||
`v1/node/register?${qp.toString()}`,
|
||||
apiKey,
|
||||
{
|
||||
user,
|
||||
key,
|
||||
},
|
||||
);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
approveNodeRoutes: async (nodeId, routes) => {
|
||||
await client.apiFetch<void>(
|
||||
'POST',
|
||||
`v1/node/${nodeId}/approve_routes`,
|
||||
apiKey,
|
||||
{ routes },
|
||||
);
|
||||
},
|
||||
|
||||
expireNode: async (nodeId) => {
|
||||
await client.apiFetch<void>('POST', `v1/node/${nodeId}/expire`, apiKey);
|
||||
},
|
||||
|
||||
renameNode: async (nodeId, newName) => {
|
||||
await client.apiFetch<void>(
|
||||
'POST',
|
||||
`v1/node/${nodeId}/rename/${newName}`,
|
||||
apiKey,
|
||||
);
|
||||
},
|
||||
|
||||
setNodeTags: async (nodeId, tags) => {
|
||||
await client.apiFetch<void>('POST', `v1/node/${nodeId}/tags`, apiKey, {
|
||||
tags,
|
||||
});
|
||||
},
|
||||
|
||||
setNodeUser: async (nodeId, user) => {
|
||||
await client.apiFetch<void>('POST', `v1/node/${nodeId}/user`, apiKey, {
|
||||
user,
|
||||
});
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,41 @@
|
||||
import { defineApiEndpoints } from '../factory';
|
||||
|
||||
export interface PolicyEndpoints {
|
||||
/**
|
||||
* Retrieves the current ACL policy from the Headscale instance.
|
||||
*
|
||||
* @returns The ACL policy as a string.
|
||||
*/
|
||||
getPolicy(): Promise<string>;
|
||||
|
||||
/**
|
||||
* Sets the ACL policy for the Headscale instance.
|
||||
*
|
||||
* @param policy The ACL policy as a string.
|
||||
* @returns The expiration date of the new policy.
|
||||
*/
|
||||
setPolicy(policy: string): Promise<Date>;
|
||||
}
|
||||
|
||||
export default defineApiEndpoints<PolicyEndpoints>((client, apiKey) => ({
|
||||
getPolicy: async () => {
|
||||
const { policy } = await client.apiFetch<{ policy: string }>(
|
||||
'GET',
|
||||
'v1/policy',
|
||||
apiKey,
|
||||
);
|
||||
|
||||
return policy;
|
||||
},
|
||||
|
||||
setPolicy: async (policy) => {
|
||||
const { updatedAt } = await client.apiFetch<{ updatedAt: string }>(
|
||||
'PUT',
|
||||
'v1/policy',
|
||||
apiKey,
|
||||
{ policy },
|
||||
);
|
||||
|
||||
return new Date(updatedAt);
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { PreAuthKey } from '~/types';
|
||||
import { defineApiEndpoints } from '../factory';
|
||||
|
||||
export interface PreAuthKeyEndpoints {
|
||||
/**
|
||||
* Retrieves all pre-authentication keys for a specific user.
|
||||
*
|
||||
* @param user The user to retrieve pre-authentication keys for.
|
||||
* @returns An array of `PreAuthKey` objects representing the pre-authentication keys.
|
||||
*/
|
||||
getPreAuthKeys(user: string): Promise<PreAuthKey[]>;
|
||||
|
||||
/**
|
||||
* Creates a new pre-authentication key for a specific user.
|
||||
*
|
||||
* @param user The user to create the pre-authentication key for.
|
||||
* @param ephemeral Whether the key is ephemeral.
|
||||
* @param uses The number of uses for the key.
|
||||
* @param expiration The expiration date of the key, or `null` for no expiration.
|
||||
* @param aclTags An array of ACL tags to associate with the key, or `null` for none.
|
||||
* @returns A `PreAuthKey` object representing the newly created pre-authentication key.
|
||||
*/
|
||||
createPreAuthKey(
|
||||
user: string,
|
||||
ephemeral: boolean,
|
||||
uses: number,
|
||||
expiration: Date | null,
|
||||
aclTags: string[] | null,
|
||||
): Promise<PreAuthKey>;
|
||||
|
||||
/**
|
||||
* Expires a specific pre-authentication key for a user.
|
||||
*
|
||||
* @param user The user associated with the pre-authentication key.
|
||||
* @param key The pre-authentication key to expire.
|
||||
*/
|
||||
expirePreAuthKey(user: string, key: string): Promise<void>;
|
||||
}
|
||||
|
||||
export default defineApiEndpoints<PreAuthKeyEndpoints>((client, apiKey) => ({
|
||||
getPreAuthKeys: async (user) => {
|
||||
const { preAuthKeys } = await client.apiFetch<{
|
||||
preAuthKeys: PreAuthKey[];
|
||||
}>('GET', 'v1/preauthkey', apiKey, { user });
|
||||
|
||||
return preAuthKeys;
|
||||
},
|
||||
|
||||
createPreAuthKey: async (user, ephemeral, uses, expiration, aclTags) => {
|
||||
const { preAuthKey } = await client.apiFetch<{
|
||||
preAuthKey: PreAuthKey;
|
||||
}>('POST', 'v1/preauthkey', apiKey, {
|
||||
user,
|
||||
ephemeral,
|
||||
uses,
|
||||
expiration: expiration ? expiration.toISOString() : null,
|
||||
aclTags,
|
||||
});
|
||||
|
||||
return preAuthKey;
|
||||
},
|
||||
|
||||
expirePreAuthKey: async (user, key) => {
|
||||
await client.apiFetch<void>('POST', 'v1/preauthkey/expire', apiKey, {
|
||||
user,
|
||||
key,
|
||||
});
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { User } from '~/types';
|
||||
import { defineApiEndpoints } from '../factory';
|
||||
|
||||
export interface UserEndpoints {
|
||||
/**
|
||||
* Retrieves users from the Headscale instance, optionally filtering by ID, name, or email.
|
||||
*
|
||||
* @param id Optional ID of the user to retrieve.
|
||||
* @param name Optional name of the user to retrieve.
|
||||
* @param email Optional email of the user to retrieve.
|
||||
* @returns An array of `User` objects representing the users.
|
||||
*/
|
||||
getUsers(id?: string, name?: string, email?: string): Promise<User[]>;
|
||||
|
||||
/**
|
||||
* Creates a new user in the Headscale instance.
|
||||
*
|
||||
* @param username The username of the new user.
|
||||
* @param email Optional email of the new user.
|
||||
* @param displayName Optional display name of the new user.
|
||||
* @param pictureUrl Optional picture URL of the new user.
|
||||
* @returns A `User` object representing the newly created user.
|
||||
*/
|
||||
createUser(
|
||||
username: string,
|
||||
email?: string,
|
||||
displayName?: string,
|
||||
pictureUrl?: string,
|
||||
): Promise<User>;
|
||||
|
||||
/**
|
||||
* Deletes a specific user by its ID.
|
||||
*
|
||||
* @param id The ID of the user to delete.
|
||||
*/
|
||||
deleteUser(id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Renames a specific user by its ID.
|
||||
*
|
||||
* @param id The ID of the user to rename.
|
||||
* @param newName The new name for the user.
|
||||
*/
|
||||
renameUser(id: string, newName: string): Promise<void>;
|
||||
}
|
||||
|
||||
export default defineApiEndpoints<UserEndpoints>((client, apiKey) => ({
|
||||
getUsers: async (id, name, email) => {
|
||||
const moreThanOneFilter =
|
||||
[id, name, email].filter((v) => v !== undefined).length > 1;
|
||||
|
||||
if (moreThanOneFilter) {
|
||||
throw new Error('Only one of id, name, or email filters can be provided');
|
||||
}
|
||||
|
||||
const { users } = await client.apiFetch<{ users: User[] }>(
|
||||
'GET',
|
||||
'v1/user',
|
||||
apiKey,
|
||||
{ id, name, email },
|
||||
);
|
||||
|
||||
return users;
|
||||
},
|
||||
|
||||
createUser: async (username, email, displayName, pictureUrl) => {
|
||||
const { user } = await client.apiFetch<{ user: User }>(
|
||||
'POST',
|
||||
'v1/user',
|
||||
apiKey,
|
||||
{ name: username, email, displayName, pictureUrl },
|
||||
);
|
||||
|
||||
return user;
|
||||
},
|
||||
|
||||
deleteUser: async (id) => {
|
||||
await client.apiFetch<void>('DELETE', `v1/user/${id}`, apiKey);
|
||||
},
|
||||
|
||||
renameUser: async (oldId, newName) => {
|
||||
await client.apiFetch<void>(
|
||||
'POST',
|
||||
`v1/user/${oldId}/rename/${newName}`,
|
||||
apiKey,
|
||||
);
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user