Compare commits

...

5 Commits

Author SHA1 Message Date
Aarnav Tale 41b1d3c847 chore: v0.3.7 2024-11-30 15:34:54 -05:00
Aarnav Tale 712fc28683 feat: allow a public headscale URL separate from the main one 2024-11-30 15:33:58 -05:00
Aarnav Tale 320dab1d4f fix: join children in copyable codeblock 2024-11-30 15:05:07 -05:00
Aarnav Tale da0ee1382b feat: allow setting OIDC_CLIENT_SECRET_METHOD 2024-11-30 15:00:51 -05:00
Aarnav Tale a7d127c7bf feat: add health check route 2024-11-27 11:23:42 -05:00
8 changed files with 62 additions and 5 deletions
+6
View File
@@ -1,3 +1,9 @@
### 0.3.7 (November 30, 2024)
- Allow customizing the OIDC token endpoint auth method via `OIDC_CLIENT_SECRET_METHOD` (fixes [#57](https://github.com/tale/headplane/issues/57))
- Added a `/healthz` endpoint for Kubernetes and other health checks (fixes [#59](https://github.com/tale/headplane/issues/59))
- Allow `HEADSCALE_PUBLIC_URL` to be set if `HEADSCALE_URL` points to a different internal address (fixes [#60](https://github.com/tale/headplane/issues/60))
- Fixed an issue where the copy machine registration command had a typo.
### 0.3.6 (November 20, 2024)
- Fixed an issue where select dropdowns would not scroll (fixes [#53](https://github.com/tale/headplane/issues/53))
- Added a button to copy the machine registration command to the clipboard (fixes [#52](https://github.com/tale/headplane/issues/52))
+1 -1
View File
@@ -28,7 +28,7 @@ export default function Code(props: Props) {
'inline-flex items-center justify-center'
)}
onClick={() => {
navigator.clipboard.writeText(props.children as string)
navigator.clipboard.writeText(props.children.join(''))
toast('Copied to clipboard')
setIsCopied(true)
setTimeout(() => setIsCopied(false), 1000)
+5 -1
View File
@@ -46,6 +46,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
users: users.users,
magic,
server: context.headscaleUrl,
publicServer: context.headscalePublicUrl,
}
}
@@ -73,7 +74,10 @@ export default function Page() {
</Link>
</p>
</div>
<NewMachine server={data.server} users={data.users} />
<NewMachine
server={data.publicServer ?? data.server}
users={data.users}
/>
</div>
<table className="table-auto w-full rounded-lg">
<thead className="text-gray-500 dark:text-gray-400">
@@ -102,7 +102,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
return {
keys: preAuthKeys.flatMap(keys => keys.preAuthKeys),
users: users.users,
server: context.headscaleUrl,
server: context.headscalePublicUrl ?? context.headscaleUrl,
}
}
+29
View File
@@ -0,0 +1,29 @@
import { loadContext } from '~/utils/config/headplane'
import { HeadscaleError, pull } from '~/utils/headscale'
import log from '~/utils/log'
export async function loader() {
const context = await loadContext()
try {
// Doesn't matter, we just need a 401
await pull('v1/', 'wrongkey')
} catch (e) {
if (!(e instanceof HeadscaleError)) {
log.debug('Healthz', 'Headscale is not reachable')
return new Response('Headscale is not reachable', {
status: 500,
headers: {
'Content-Type': 'text/plain',
},
})
}
}
return new Response('OK', {
status: 200,
headers: {
'Content-Type': 'text/plain',
},
})
}
+16
View File
@@ -15,6 +15,7 @@ import log from '~/utils/log'
export interface HeadplaneContext {
debug: boolean
headscaleUrl: string
headscalePublicUrl?: string
cookieSecret: string
integration: IntegrationFactory | undefined
@@ -28,6 +29,7 @@ export interface HeadplaneContext {
client: string
secret: string
rootKey: string
method: string
disableKeyLogin: boolean
}
}
@@ -55,12 +57,18 @@ export async function loadContext(): Promise<HeadplaneContext> {
const { config, contextData } = await checkConfig(path)
let headscaleUrl = process.env.HEADSCALE_URL
let headscalePublicUrl = process.env.HEADSCALE_PUBLIC_URL
if (!headscaleUrl && !config) {
throw new Error('HEADSCALE_URL not set')
}
if (config) {
headscaleUrl = headscaleUrl ?? config.server_url
if (!headscalePublicUrl) {
// Fallback to the config value if the env var is not set
headscalePublicUrl = config.public_url
}
}
if (!headscaleUrl) {
@@ -75,6 +83,7 @@ export async function loadContext(): Promise<HeadplaneContext> {
context = {
debug,
headscaleUrl,
headscalePublicUrl,
cookieSecret,
integration: await loadIntegration(),
config: contextData,
@@ -83,6 +92,10 @@ export async function loadContext(): Promise<HeadplaneContext> {
log.info('CTXT', 'Starting Headplane with Context')
log.info('CTXT', 'HEADSCALE_URL: %s', headscaleUrl)
if (headscalePublicUrl) {
log.info('CTXT', 'HEADSCALE_PUBLIC_URL: %s', headscalePublicUrl)
}
log.info('CTXT', 'Integration: %s', context.integration?.name ?? 'None')
log.info('CTXT', 'Config: %s', contextData.read
? `Found ${contextData.write ? '' : '(Read Only)'}`
@@ -143,6 +156,7 @@ async function checkOidc(config?: HeadscaleConfig) {
let issuer = process.env.OIDC_ISSUER
let client = process.env.OIDC_CLIENT_ID
let secret = process.env.OIDC_CLIENT_SECRET
let method = process.env.OIDC_CLIENT_SECRET_METHOD ?? 'client_secret_basic'
log.debug('CTXT', 'Checking OIDC environment variables')
log.debug('CTXT', 'Issuer: %s', issuer)
@@ -161,6 +175,7 @@ async function checkOidc(config?: HeadscaleConfig) {
issuer,
client,
secret,
method,
rootKey,
disableKeyLogin,
}
@@ -204,6 +219,7 @@ async function checkOidc(config?: HeadscaleConfig) {
client,
secret,
rootKey,
method,
disableKeyLogin,
}
}
+2 -2
View File
@@ -36,7 +36,7 @@ export async function startOidc(oidc: OidcConfig, req: Request) {
const issuerUrl = new URL(oidc.issuer)
const oidcClient = {
client_id: oidc.client,
token_endpoint_auth_method: 'client_secret_basic',
token_endpoint_auth_method: oidc.method,
} satisfies Client
const response = await discoveryRequest(issuerUrl)
@@ -91,7 +91,7 @@ export async function finishOidc(oidc: OidcConfig, req: Request) {
const oidcClient = {
client_id: oidc.client,
client_secret: oidc.secret,
token_endpoint_auth_method: 'client_secret_basic',
token_endpoint_auth_method: oidc.method,
} satisfies Client
const response = await discoveryRequest(issuerUrl)
+2
View File
@@ -9,6 +9,7 @@ You can configure Headplane using environment variables.
#### Optional Variables
- **`HEADSCALE_PUBLIC_URL`**: The public URL of your Headscale server (if different from `HEADSCALE_URL`).
- **`DEBUG`**: Enable debug logging (default: `false`).
- **`HOST`**: The host to bind the server to (default: `0.0.0.0`).
- **`PORT`**: The port to bind the server to (default: `3000`).
@@ -34,6 +35,7 @@ If you use the Headscale configuration integration, these are not required.
- **`OIDC_ISSUER`**: The issuer URL of your OIDC provider.
- **`OIDC_CLIENT_ID`**: The client ID of your OIDC provider.
- **`OIDC_CLIENT_SECRET`**: The client secret of your OIDC provider.
- **`OIDC_CLIENT_SECRET_METHOD`**: The method used to send the client secret (default: `client_secret_basic`).
- **`ROOT_API_KEY`**: An API key used to issue new ones for sessions (keep expiry fairly long).
- **`DISABLE_API_KEY_LOGIN`**: If you want to disable API key login, set this to `true`.