mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
feat: reach an initial working stage
This commit is contained in:
+24
-19
@@ -22,24 +22,29 @@ export type ConfigModes =
|
||||
config: undefined;
|
||||
};
|
||||
|
||||
export function hs_getConfig(): ConfigModes {
|
||||
if (runtimeMode === 'no') {
|
||||
return {
|
||||
mode: 'no',
|
||||
config: undefined,
|
||||
};
|
||||
}
|
||||
// export function hs_getConfig(): ConfigModes {
|
||||
// return {
|
||||
// mode: 'no',
|
||||
// config: undefined,
|
||||
// };
|
||||
|
||||
runtimeLock.acquire();
|
||||
// We can assert if mode is not 'no'
|
||||
const config = runtimeConfig!;
|
||||
runtimeLock.release();
|
||||
// if (runtimeMode === 'no') {
|
||||
// return {
|
||||
// mode: 'no',
|
||||
// config: undefined,
|
||||
// };
|
||||
// }
|
||||
|
||||
return {
|
||||
mode: runtimeMode,
|
||||
config: config,
|
||||
};
|
||||
}
|
||||
// runtimeLock.acquire();
|
||||
// // We can assert if mode is not 'no'
|
||||
// const config = runtimeConfig!;
|
||||
// runtimeLock.release();
|
||||
|
||||
// return {
|
||||
// mode: runtimeMode,
|
||||
// config: config,
|
||||
// };
|
||||
// }
|
||||
|
||||
export async function hs_loadConfig(path?: string, strict?: boolean) {
|
||||
if (runtimeConfig !== undefined) {
|
||||
@@ -199,6 +204,6 @@ export async function hs_patchConfig(patches: PatchConfig[]) {
|
||||
|
||||
// IMPORTANT THIS IS A SIDE EFFECT ON INIT
|
||||
// TODO: Replace this into the new singleton system
|
||||
const context = hp_getConfig();
|
||||
hs_loadConfig(context.headscale.config_path, context.headscale.config_strict);
|
||||
hp_getIntegration();
|
||||
// const context = hp_getConfig();
|
||||
// hs_loadConfig(context.headscale.config_path, context.headscale.config_strict);
|
||||
// hp_getIntegration();
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import { request } from 'undici';
|
||||
import { hp_getConfig, hp_getSingleton } from '~server/context/global';
|
||||
import log from '~server/utils/log';
|
||||
|
||||
export class HeadscaleError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(message: string, status: number) {
|
||||
super(message);
|
||||
this.name = 'HeadscaleError';
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
export class FatalError extends Error {
|
||||
constructor() {
|
||||
super(
|
||||
'The Headscale server is not accessible or the supplied API key is invalid',
|
||||
);
|
||||
this.name = 'FatalError';
|
||||
}
|
||||
}
|
||||
|
||||
export async function healthcheck() {
|
||||
log.debug('APIC', 'GET /health');
|
||||
const health = new URL('health', hp_getConfig().headscale.url);
|
||||
const response = await request(health.toString(), {
|
||||
dispatcher: hp_getSingleton('api_agent'),
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Intentionally not catching
|
||||
return response.statusCode === 200;
|
||||
}
|
||||
|
||||
export async function pull<T>(url: string, key: string) {
|
||||
if (!key || key === 'undefined' || key.length === 0) {
|
||||
throw new Error('Missing API key, could this be a cookie setting issue?');
|
||||
}
|
||||
|
||||
const prefix = hp_getConfig().headscale.url;
|
||||
log.debug('APIC', 'GET %s', `${prefix}/api/${url}`);
|
||||
const response = await request(`${prefix}/api/${url}`, {
|
||||
dispatcher: hp_getSingleton('api_agent'),
|
||||
headers: {
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
log.debug(
|
||||
'APIC',
|
||||
'GET %s failed with status %d',
|
||||
`${prefix}/api/${url}`,
|
||||
response.statusCode,
|
||||
);
|
||||
throw new HeadscaleError(await response.body.text(), response.statusCode);
|
||||
}
|
||||
|
||||
return response.body.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export async function post<T>(url: string, key: string, body?: unknown) {
|
||||
if (!key || key === 'undefined' || key.length === 0) {
|
||||
throw new Error('Missing API key, could this be a cookie setting issue?');
|
||||
}
|
||||
|
||||
const prefix = hp_getConfig().headscale.url;
|
||||
log.debug('APIC', 'POST %s', `${prefix}/api/${url}`);
|
||||
const response = await request(`${prefix}/api/${url}`, {
|
||||
dispatcher: hp_getSingleton('api_agent'),
|
||||
method: 'POST',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
log.debug(
|
||||
'APIC',
|
||||
'POST %s failed with status %d',
|
||||
`${prefix}/api/${url}`,
|
||||
response.statusCode,
|
||||
);
|
||||
throw new HeadscaleError(await response.body.text(), response.statusCode);
|
||||
}
|
||||
|
||||
return response.body.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export async function put<T>(url: string, key: string, body?: unknown) {
|
||||
if (!key || key === 'undefined' || key.length === 0) {
|
||||
throw new Error('Missing API key, could this be a cookie setting issue?');
|
||||
}
|
||||
|
||||
const prefix = hp_getConfig().headscale.url;
|
||||
log.debug('APIC', 'PUT %s', `${prefix}/api/${url}`);
|
||||
const response = await request(`${prefix}/api/${url}`, {
|
||||
dispatcher: hp_getSingleton('api_agent'),
|
||||
method: 'PUT',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
log.debug(
|
||||
'APIC',
|
||||
'PUT %s failed with status %d',
|
||||
`${prefix}/api/${url}`,
|
||||
response.statusCode,
|
||||
);
|
||||
throw new HeadscaleError(await response.body.text(), response.statusCode);
|
||||
}
|
||||
|
||||
return response.body.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export async function del<T>(url: string, key: string) {
|
||||
if (!key || key === 'undefined' || key.length === 0) {
|
||||
throw new Error('Missing API key, could this be a cookie setting issue?');
|
||||
}
|
||||
|
||||
const prefix = hp_getConfig().headscale.url;
|
||||
log.debug('APIC', 'DELETE %s', `${prefix}/api/${url}`);
|
||||
const response = await request(`${prefix}/api/${url}`, {
|
||||
dispatcher: hp_getSingleton('api_agent'),
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
log.debug(
|
||||
'APIC',
|
||||
'DELETE %s failed with status %d',
|
||||
`${prefix}/api/${url}`,
|
||||
response.statusCode,
|
||||
);
|
||||
throw new HeadscaleError(await response.body.text(), response.statusCode);
|
||||
}
|
||||
|
||||
return response.body.json() as Promise<T>;
|
||||
}
|
||||
@@ -2,11 +2,11 @@ import { hp_getConfig } from '~server/context/global';
|
||||
import { HeadplaneConfig } from '~server/context/parser';
|
||||
import log from '~server/utils/log';
|
||||
import { Integration } from './abstract';
|
||||
import dockerIntegration from './docker';
|
||||
import kubernetesIntegration from './kubernetes';
|
||||
import procIntegration from './proc';
|
||||
// import dockerIntegration from './docker';
|
||||
// import kubernetesIntegration from './kubernetes';
|
||||
// import procIntegration from './proc';
|
||||
|
||||
let runtimeIntegration: Integration<unknown> | undefined = undefined;
|
||||
const runtimeIntegration: Integration<unknown> | undefined = undefined;
|
||||
|
||||
export function hp_getIntegration() {
|
||||
return runtimeIntegration;
|
||||
@@ -15,24 +15,22 @@ export function hp_getIntegration() {
|
||||
export async function hp_loadIntegration(
|
||||
context: HeadplaneConfig['integration'],
|
||||
) {
|
||||
const integration = getIntegration(context);
|
||||
if (!integration) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await integration.isAvailable();
|
||||
if (!res) {
|
||||
log.error('INTG', 'Integration %s is not available', integration);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('INTG', 'Failed to load integration %s: %s', integration, error);
|
||||
log.debug('INTG', 'Loading error: %o', error);
|
||||
return;
|
||||
}
|
||||
|
||||
runtimeIntegration = integration;
|
||||
// const integration = getIntegration(context);
|
||||
// if (!integration) {
|
||||
// return;
|
||||
// }
|
||||
// try {
|
||||
// const res = await integration.isAvailable();
|
||||
// if (!res) {
|
||||
// log.error('INTG', 'Integration %s is not available', integration);
|
||||
// return;
|
||||
// }
|
||||
// } catch (error) {
|
||||
// log.error('INTG', 'Failed to load integration %s: %s', integration, error);
|
||||
// log.debug('INTG', 'Loading error: %o', error);
|
||||
// return;
|
||||
// }
|
||||
// runtimeIntegration = integration;
|
||||
}
|
||||
|
||||
function getIntegration(integration: HeadplaneConfig['integration']) {
|
||||
@@ -50,23 +48,23 @@ function getIntegration(integration: HeadplaneConfig['integration']) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (docker?.enabled) {
|
||||
log.info('INTG', 'Using Docker integration');
|
||||
return new dockerIntegration(integration?.docker);
|
||||
}
|
||||
// if (docker?.enabled) {
|
||||
// log.info('INTG', 'Using Docker integration');
|
||||
// return new dockerIntegration(integration?.docker);
|
||||
// }
|
||||
|
||||
if (k8s?.enabled) {
|
||||
log.info('INTG', 'Using Kubernetes integration');
|
||||
return new kubernetesIntegration(integration?.kubernetes);
|
||||
}
|
||||
// if (k8s?.enabled) {
|
||||
// log.info('INTG', 'Using Kubernetes integration');
|
||||
// return new kubernetesIntegration(integration?.kubernetes);
|
||||
// }
|
||||
|
||||
if (proc?.enabled) {
|
||||
log.info('INTG', 'Using Proc integration');
|
||||
return new procIntegration(integration?.proc);
|
||||
}
|
||||
// if (proc?.enabled) {
|
||||
// log.info('INTG', 'Using Proc integration');
|
||||
// return new procIntegration(integration?.proc);
|
||||
// }
|
||||
}
|
||||
|
||||
// IMPORTANT THIS IS A SIDE EFFECT ON INIT
|
||||
// TODO: Switch this to the new singleton system
|
||||
const context = hp_getConfig();
|
||||
hp_loadIntegration(context.integration);
|
||||
// hp_loadIntegration(context.integration);
|
||||
|
||||
+13
-10
@@ -1,13 +1,11 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import * as client from 'openid-client';
|
||||
import { Configuration } from 'openid-client';
|
||||
import { hp_getSingleton, hp_setSingleton } from '~server/context/global';
|
||||
import { HeadplaneConfig } from '~server/context/parser';
|
||||
import log from '~server/utils/log';
|
||||
|
||||
type OidcConfig = NonNullable<HeadplaneConfig['oidc']>;
|
||||
declare global {
|
||||
const __PREFIX__: string;
|
||||
}
|
||||
|
||||
// We try our best to infer the callback URI of our Headplane instance
|
||||
// By default it is always /<base_path>/oidc/callback
|
||||
@@ -103,8 +101,11 @@ function clientAuthMethod(
|
||||
}
|
||||
}
|
||||
|
||||
export async function beginAuthFlow(oidc: OidcConfig, redirect_uri: string) {
|
||||
const config = hp_getSingleton('oidc_client');
|
||||
export async function beginAuthFlow(
|
||||
config: Configuration,
|
||||
redirect_uri: string,
|
||||
token_endpoint_auth_method: string,
|
||||
) {
|
||||
const codeVerifier = client.randomPKCECodeVerifier();
|
||||
const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
|
||||
|
||||
@@ -113,7 +114,7 @@ export async function beginAuthFlow(oidc: OidcConfig, redirect_uri: string) {
|
||||
scope: 'openid profile email',
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256',
|
||||
token_endpoint_auth_method: oidc.token_endpoint_auth_method,
|
||||
token_endpoint_auth_method,
|
||||
state: client.randomState(),
|
||||
};
|
||||
|
||||
@@ -134,18 +135,20 @@ export async function beginAuthFlow(oidc: OidcConfig, redirect_uri: string) {
|
||||
|
||||
interface FlowOptions {
|
||||
redirect_uri: string;
|
||||
codeVerifier: string;
|
||||
code_verifier: string;
|
||||
state: string;
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
export async function finishAuthFlow(oidc: OidcConfig, options: FlowOptions) {
|
||||
const config = hp_getSingleton('oidc_client');
|
||||
export async function finishAuthFlow(
|
||||
config: Configuration,
|
||||
options: FlowOptions,
|
||||
) {
|
||||
const tokens = await client.authorizationCodeGrant(
|
||||
config,
|
||||
new URL(options.redirect_uri),
|
||||
{
|
||||
pkceCodeVerifier: options.codeVerifier,
|
||||
pkceCodeVerifier: options.code_verifier,
|
||||
expectedNonce: options.nonce,
|
||||
expectedState: options.state,
|
||||
idTokenExpected: true,
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Session, createCookieSessionStorage } from 'react-router';
|
||||
import { hp_getConfig } from '~server/context/global';
|
||||
|
||||
export type SessionData = {
|
||||
hsApiKey: string;
|
||||
oidc_state: string;
|
||||
oidc_code_verif: string;
|
||||
oidc_nonce: string;
|
||||
oidc_redirect_uri: string;
|
||||
agent_onboarding: boolean;
|
||||
user: {
|
||||
subject: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
username?: string;
|
||||
picture?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type SessionFlashData = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
// TODO: Domain config in cookies
|
||||
// TODO: Move this to the singleton system
|
||||
const context = hp_getConfig();
|
||||
const sessionStorage = createCookieSessionStorage<
|
||||
SessionData,
|
||||
SessionFlashData
|
||||
>({
|
||||
cookie: {
|
||||
name: 'hp_sess',
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 60 * 24, // 24 hours
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
secrets: [context.server.cookie_secret],
|
||||
secure: context.server.cookie_secure,
|
||||
},
|
||||
});
|
||||
|
||||
export function getSession(cookie: string | null) {
|
||||
return sessionStorage.getSession(cookie);
|
||||
}
|
||||
|
||||
export type ServerSession = Session<SessionData, SessionFlashData>;
|
||||
export async function auth(request: Request) {
|
||||
const cookie = request.headers.get('Cookie');
|
||||
const session = await sessionStorage.getSession(cookie);
|
||||
if (!session.has('hsApiKey')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export function destroySession(session: Session) {
|
||||
return sessionStorage.destroySession(session);
|
||||
}
|
||||
|
||||
export function commitSession(session: Session, opts?: { maxAge?: number }) {
|
||||
return sessionStorage.commitSession(session, opts);
|
||||
}
|
||||
Reference in New Issue
Block a user