diff --git a/app/entry.server.tsx b/app/entry.server.tsx
new file mode 100644
index 0000000..f6c9843
--- /dev/null
+++ b/app/entry.server.tsx
@@ -0,0 +1,62 @@
+import { PassThrough } from 'node:stream'
+
+import type { AppLoadContext, EntryContext } from '@remix-run/node'
+import { createReadableStreamFromReadable } from '@remix-run/node'
+import { RemixServer } from '@remix-run/react'
+import { isbot } from 'isbot'
+import { renderToPipeableStream } from 'react-dom/server'
+
+import { loadContext } from './utils/config/headplane'
+
+await loadContext()
+
+export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _loadContext: AppLoadContext,
+) {
+ const ua = request.headers.get('user-agent')
+ const isBot = ua ? isbot(ua) : false
+
+ return new Promise((resolve, reject) => {
+ let shellRendered = false
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ [isBot ? 'onAllReady' : 'onShellReady']() {
+ shellRendered = true
+ const body = new PassThrough()
+ const stream = createReadableStreamFromReadable(body)
+ responseHeaders.set('Content-Type', 'text/html')
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ }),
+ )
+
+ pipe(body)
+ },
+ onShellError(error: unknown) {
+ reject(error as Error)
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500
+ if (shellRendered) {
+ console.error(error)
+ }
+ },
+ },
+ )
+
+ setTimeout(abort, 5000)
+ })
+}
diff --git a/app/utils/config/headplane.ts b/app/utils/config/headplane.ts
index 83646fe..de31b46 100644
--- a/app/utils/config/headplane.ts
+++ b/app/utils/config/headplane.ts
@@ -43,12 +43,8 @@ export async function loadContext(): Promise {
return context
}
- let config: HeadscaleConfig | undefined
- try {
- config = await loadConfig()
- } catch {}
-
const path = resolve(process.env.CONFIG_FILE ?? '/etc/headscale/config.yaml')
+ const { config, contextData } = await checkConfig(path)
let headscaleUrl = process.env.HEADSCALE_URL
if (!headscaleUrl && !config) {
@@ -72,7 +68,7 @@ export async function loadContext(): Promise {
headscaleUrl,
cookieSecret,
integration: await checkIntegration(),
- config: await checkConfig(path, config),
+ config: contextData,
acl: await checkAcl(config),
oidc: await checkOidc(config),
}
@@ -121,7 +117,20 @@ export async function patchAcl(data: string) {
await writeFile(path, data, 'utf8')
}
-async function checkConfig(path: string, config?: HeadscaleConfig) {
+async function checkConfig(path: string) {
+ let config: HeadscaleConfig | undefined
+ try {
+ config = await loadConfig(path)
+ } catch {
+ return {
+ config: undefined,
+ contextData: {
+ read: false,
+ write: false,
+ },
+ }
+ }
+
let write = false
try {
await access(path, constants.W_OK)
@@ -129,8 +138,11 @@ async function checkConfig(path: string, config?: HeadscaleConfig) {
} catch {}
return {
- read: config ? true : false,
- write,
+ config,
+ contextData: {
+ read: true,
+ write,
+ },
}
}
diff --git a/app/utils/config/headscale.ts b/app/utils/config/headscale.ts
index fd6f61f..4bd3e81 100644
--- a/app/utils/config/headscale.ts
+++ b/app/utils/config/headscale.ts
@@ -158,16 +158,40 @@ export type HeadscaleConfig = z.infer
export let configYaml: Document | undefined
export let config: HeadscaleConfig | undefined
-export async function loadConfig() {
+export async function loadConfig(path?: string) {
if (config) {
return config
}
- const path = resolve(process.env.CONFIG_FILE ?? '/etc/headscale/config.yaml')
- const data = await readFile(path, 'utf8')
+ if (!path) {
+ throw new Error('Path is required to lazy load config')
+ }
+ const data = await readFile(path, 'utf8')
configYaml = parseDocument(data)
- config = await HeadscaleConfig.parseAsync(configYaml.toJSON())
+
+ try {
+ config = await HeadscaleConfig.parseAsync(configYaml.toJSON())
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ console.log('Failed to parse the Headscale configuration file!')
+ console.log('The following schema issues were found:')
+ for (const issue of error.issues) {
+ const path = issue.path.map(String).join('.')
+ const message = issue.message
+
+ console.log(`- '${path}': ${message}`)
+ }
+
+ console.log('')
+ console.log('Please fix the configuration file and try again.')
+ console.log('Headplane will operate as if no config is present.')
+ console.log('')
+ }
+
+ throw error
+ }
+
return config
}