headscale: device invites — admin surface for offscale enrollment
This commit is contained in:
@@ -26,26 +26,31 @@ import { badRequest, methodNotAllowed, notFound, type OfficerContext } from './r
|
||||
* companion is a state to render, not a request that failed — the admin API on the same domain is
|
||||
* independent and may still be working, so this must not surface as an error the UI swallows.
|
||||
*/
|
||||
const unavailable = (reason: string) => ({ available: false as const, reason });
|
||||
export const unavailable = (reason: string) => ({ available: false as const, reason });
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 20_000;
|
||||
|
||||
type CompanionCall = { path: string; method?: string; timeoutMs?: number; signal?: AbortSignal };
|
||||
type CompanionCall = { path: string; method?: string; body?: unknown; timeoutMs?: number; signal?: AbortSignal };
|
||||
|
||||
/**
|
||||
* One request to a server's companion. Returns the raw Response, or a reason string when the companion
|
||||
* itself could not be reached — the caller decides how to present that, because for this feature
|
||||
* "unreachable" is information rather than a failure.
|
||||
*/
|
||||
async function callCompanion(
|
||||
export async function callCompanion(
|
||||
creds: HeadscaleServerCredentials,
|
||||
{ path, method = 'GET', timeoutMs = DEFAULT_TIMEOUT_MS, signal }: CompanionCall,
|
||||
{ path, method = 'GET', body, timeoutMs = DEFAULT_TIMEOUT_MS, signal }: CompanionCall,
|
||||
): Promise<Response | string> {
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${creds.url}/officer-api${path}`, {
|
||||
method,
|
||||
headers: { authorization: `Bearer ${creds.apiKey}`, accept: 'application/json' },
|
||||
headers: {
|
||||
authorization: `Bearer ${creds.apiKey}`,
|
||||
accept: 'application/json',
|
||||
...(body === undefined ? {} : { 'content-type': 'application/json' }),
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal: signal ?? AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -67,7 +72,7 @@ async function callCompanion(
|
||||
}
|
||||
|
||||
/** Parse a companion JSON body, or a reason when it isn't JSON after all. */
|
||||
async function readBody(res: Response): Promise<Record<string, unknown> | string> {
|
||||
export async function readBody(res: Response): Promise<Record<string, unknown> | string> {
|
||||
const text = await res.text().catch(() => '');
|
||||
if (!text) return 'the companion returned an empty body';
|
||||
try {
|
||||
@@ -80,7 +85,7 @@ async function readBody(res: Response): Promise<Record<string, unknown> | string
|
||||
}
|
||||
|
||||
/** The active server's credentials, or a 409 the UI already knows how to render. */
|
||||
async function activeCreds(userId: number): Promise<HeadscaleServerCredentials | Response> {
|
||||
export async function activeCreds(userId: number): Promise<HeadscaleServerCredentials | Response> {
|
||||
const creds = await getActiveHeadscaleCredentials(userId);
|
||||
if (!creds) {
|
||||
return Response.json({ error: 'no active Headscale server', code: 'no_active_server' }, { status: 409 });
|
||||
|
||||
Reference in New Issue
Block a user