mirror of
https://github.com/tale/headplane.git
synced 2026-08-28 07:57:03 +00:00
feat: implement context sanity checks
This commit is contained in:
@@ -18,8 +18,12 @@ import RenameModal from './rename'
|
|||||||
|
|
||||||
// We do not want to expose every config value
|
// We do not want to expose every config value
|
||||||
export async function loader() {
|
export async function loader() {
|
||||||
const config = await getConfig()
|
|
||||||
const context = await getContext()
|
const context = await getContext()
|
||||||
|
if (!context.hasConfig) {
|
||||||
|
throw new Error('No configuration is available')
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await getConfig()
|
||||||
|
|
||||||
const dns = {
|
const dns = {
|
||||||
prefixes: config.prefixes,
|
prefixes: config.prefixes,
|
||||||
|
|||||||
+25
-9
@@ -1,9 +1,10 @@
|
|||||||
import { Cog8ToothIcon, CpuChipIcon, GlobeAltIcon, LockClosedIcon, ServerStackIcon, UsersIcon } from '@heroicons/react/24/outline'
|
import { Cog8ToothIcon, CpuChipIcon, GlobeAltIcon, LockClosedIcon, ServerStackIcon, UsersIcon } from '@heroicons/react/24/outline'
|
||||||
import { type LoaderFunctionArgs, redirect } from '@remix-run/node'
|
import { type LoaderFunctionArgs, redirect } from '@remix-run/node'
|
||||||
import { Outlet, useRouteError } from '@remix-run/react'
|
import { Outlet, useLoaderData, useRouteError } from '@remix-run/react'
|
||||||
|
|
||||||
import { ErrorPopup } from '~/components/Error'
|
import { ErrorPopup } from '~/components/Error'
|
||||||
import TabLink from '~/components/TabLink'
|
import TabLink from '~/components/TabLink'
|
||||||
|
import { getContext } from '~/utils/config'
|
||||||
import { HeadscaleError, pull } from '~/utils/headscale'
|
import { HeadscaleError, pull } from '~/utils/headscale'
|
||||||
import { destroySession, getSession } from '~/utils/sessions'
|
import { destroySession, getSession } from '~/utils/sessions'
|
||||||
|
|
||||||
@@ -32,11 +33,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/no-null
|
const context = await getContext()
|
||||||
return null
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
|
const data = useLoaderData<typeof loader>()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className='mb-16 bg-gray-800 text-white dark:bg-gray-700'>
|
<header className='mb-16 bg-gray-800 text-white dark:bg-gray-700'>
|
||||||
@@ -48,9 +50,13 @@ export default function Layout() {
|
|||||||
<div className='flex items-center gap-x-4'>
|
<div className='flex items-center gap-x-4'>
|
||||||
<TabLink to='/machines' name='Machines' icon={<ServerStackIcon className='w-5 h-5'/>}/>
|
<TabLink to='/machines' name='Machines' icon={<ServerStackIcon className='w-5 h-5'/>}/>
|
||||||
<TabLink to='/users' name='Users' icon={<UsersIcon className='w-5 h-5'/>}/>
|
<TabLink to='/users' name='Users' icon={<UsersIcon className='w-5 h-5'/>}/>
|
||||||
<TabLink to='/acls' name='Access Control' icon={<LockClosedIcon className='w-5 h-5'/>}/>
|
{data.hasAcl ? <TabLink to='/acls' name='Access Control' icon={<LockClosedIcon className='w-5 h-5'/>}/> : undefined}
|
||||||
<TabLink to='/dns' name='DNS' icon={<GlobeAltIcon className='w-5 h-5'/>}/>
|
{data.hasConfig ? (
|
||||||
<TabLink to='/settings' name='Settings' icon={<Cog8ToothIcon className='w-5 h-5'/>}/>
|
<>
|
||||||
|
<TabLink to='/dns' name='DNS' icon={<GlobeAltIcon className='w-5 h-5'/>}/>
|
||||||
|
<TabLink to='/settings' name='Settings' icon={<Cog8ToothIcon className='w-5 h-5'/>}/>
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -63,6 +69,12 @@ export default function Layout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBoundary() {
|
export function ErrorBoundary() {
|
||||||
|
const data = useLoaderData<typeof loader>()
|
||||||
|
const error = useRouteError()
|
||||||
|
if (!data) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className='mb-16 bg-gray-800 text-white dark:bg-gray-700'>
|
<header className='mb-16 bg-gray-800 text-white dark:bg-gray-700'>
|
||||||
@@ -74,9 +86,13 @@ export function ErrorBoundary() {
|
|||||||
<div className='flex items-center gap-x-4'>
|
<div className='flex items-center gap-x-4'>
|
||||||
<TabLink to='/machines' name='Machines' icon={<ServerStackIcon className='w-5 h-5'/>}/>
|
<TabLink to='/machines' name='Machines' icon={<ServerStackIcon className='w-5 h-5'/>}/>
|
||||||
<TabLink to='/users' name='Users' icon={<UsersIcon className='w-5 h-5'/>}/>
|
<TabLink to='/users' name='Users' icon={<UsersIcon className='w-5 h-5'/>}/>
|
||||||
<TabLink to='/acls' name='Access Control' icon={<LockClosedIcon className='w-5 h-5'/>}/>
|
{data.hasAcl ? <TabLink to='/acls' name='Access Control' icon={<LockClosedIcon className='w-5 h-5'/>}/> : undefined}
|
||||||
<TabLink to='/dns' name='DNS' icon={<GlobeAltIcon className='w-5 h-5'/>}/>
|
{data.hasConfig ? (
|
||||||
<TabLink to='/settings' name='Settings' icon={<Cog8ToothIcon className='w-5 h-5'/>}/>
|
<>
|
||||||
|
<TabLink to='/dns' name='DNS' icon={<GlobeAltIcon className='w-5 h-5'/>}/>
|
||||||
|
<TabLink to='/settings' name='Settings' icon={<Cog8ToothIcon className='w-5 h-5'/>}/>
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
+72
-12
@@ -142,7 +142,10 @@ export async function patchConfig(partial: Record<string, unknown>) {
|
|||||||
|
|
||||||
type Context = {
|
type Context = {
|
||||||
hasDockerSock: boolean;
|
hasDockerSock: boolean;
|
||||||
|
hasConfig: boolean;
|
||||||
hasConfigWrite: boolean;
|
hasConfigWrite: boolean;
|
||||||
|
hasAcl: boolean;
|
||||||
|
hasAclWrite: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export let context: Context
|
export let context: Context
|
||||||
@@ -151,24 +154,16 @@ export async function getContext() {
|
|||||||
if (!context) {
|
if (!context) {
|
||||||
context = {
|
context = {
|
||||||
hasDockerSock: await checkSock(),
|
hasDockerSock: await checkSock(),
|
||||||
hasConfigWrite: await checkConfigWrite()
|
hasConfig: await hasConfig(),
|
||||||
|
hasConfigWrite: await hasConfigW(),
|
||||||
|
hasAcl: await hasAcl(),
|
||||||
|
hasAclWrite: await hasAclW()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkConfigWrite() {
|
|
||||||
try {
|
|
||||||
await getConfig()
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to read config file', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkSock() {
|
async function checkSock() {
|
||||||
try {
|
try {
|
||||||
await access('/var/run/docker.sock', constants.R_OK)
|
await access('/var/run/docker.sock', constants.R_OK)
|
||||||
@@ -181,3 +176,68 @@ async function checkSock() {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hasConfig() {
|
||||||
|
try {
|
||||||
|
await getConfig()
|
||||||
|
return true
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasConfigW() {
|
||||||
|
const path = resolve(process.env.CONFIG_FILE ?? '/etc/headscale/config.yaml')
|
||||||
|
try {
|
||||||
|
await access(path, constants.W_OK)
|
||||||
|
return true
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasAcl() {
|
||||||
|
let path = process.env.ACL_FILE
|
||||||
|
if (!path) {
|
||||||
|
try {
|
||||||
|
const config = await getConfig()
|
||||||
|
path = config.acl_policy_path
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await access(path, constants.R_OK)
|
||||||
|
return true
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasAclW() {
|
||||||
|
let path = process.env.ACL_FILE
|
||||||
|
if (!path) {
|
||||||
|
try {
|
||||||
|
const config = await getConfig()
|
||||||
|
path = config.acl_policy_path
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await access(path, constants.W_OK)
|
||||||
|
return true
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user