feat(TALE-35): add initial machine key authorization

This commit is contained in:
Aarnav Tale 2024-10-03 11:58:05 -04:00
parent d867769025
commit e8c1cadf54
No known key found for this signature in database
4 changed files with 153 additions and 2 deletions

View File

@ -1,9 +1,10 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { BeakerIcon, EyeIcon, IssueDraftIcon, PencilIcon } from '@primer/octicons-react'
import { type ActionFunctionArgs, json, LoaderFunctionArgs } from '@remix-run/node'
import { ActionFunctionArgs, json, LoaderFunctionArgs } from '@remix-run/node'
import { useFetcher, useLoaderData } from '@remix-run/react'
import { useEffect, useState } from 'react'
import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'
import { setTimeout } from 'node:timers/promises'
import Button from '~/components/Button'
import Code from '~/components/Code'
@ -75,6 +76,7 @@ export async function action({ request }: ActionFunctionArgs) {
policy: acl,
})
await setTimeout(250)
return json({ success: true })
} catch (error) {
return json({ success: false }, {

View File

@ -97,6 +97,35 @@ export async function menuAction(request: ActionFunctionArgs['request']) {
}
}
case 'register': {
const key = data.get('mkey')?.toString()
const user = data.get('user')?.toString()
if (!key) {
return json({ message: 'No machine key provided' }, {
status: 400,
})
}
if (!user) {
return json({ message: 'No user provided' }, {
status: 400,
})
}
try {
await post('v1/node/register', session.get('hsApiKey')!, {
user, key,
})
return json({ message: 'Machine registered' })
} catch {
return json({ message: 'Failed to register machine' }, {
status: 500,
})
}
}
default: {
return json({ message: 'Invalid method' }, {
status: 400,

View File

@ -0,0 +1,115 @@
import { Form, useSubmit } from '@remix-run/react'
import { Dispatch, SetStateAction, useState } from 'react'
import { PlusIcon, ServerIcon, KeyIcon } from '@primer/octicons-react'
import { cn } from '~/utils/cn'
import Code from '~/components/Code'
import Dialog from '~/components/Dialog'
import TextField from '~/components/TextField'
import Select from '~/components/Select'
import Menu from '~/components/Menu'
import { Machine, User } from '~/types'
export interface NewProps {
server: string
users: User[]
}
export default function New(data: NewProps) {
const submit = useSubmit()
const mkeyState = useState(false)
const pkeyState = useState(false)
const [mkey, setMkey] = useState('')
const [user, setUser] = useState(data.users[0].id)
return (
<>
<Dialog>
<Dialog.Panel control={mkeyState}>
{close => (
<>
<Dialog.Title>
Register Machine Key
</Dialog.Title>
<Dialog.Text className='mb-4'>
The machine key is given when you run
{' '}
<Code>
tailscale up --login-server=
</Code>
<Code>
{data.server}
</Code>
{' '}
on your device.
</Dialog.Text>
<Form
method="POST"
onSubmit={(e) => {
submit(e.currentTarget)
}}
>
<input type="hidden" name="_method" value="register" />
<input type="hidden" name="id" value="_" />
<TextField
label='Machine Key'
placeholder='nodekey:ff.....'
name="mkey"
state={[mkey, setMkey]}
className='my-2 font-mono'
/>
<Select
label="Owner"
name="user"
placeholder="Select a user"
state={[user, setUser]}
>
{data.users.map(user => (
<Select.Item key={user.id} id={user.name}>
{user.name}
</Select.Item>
))}
</Select>
<div className='mt-6 flex justify-end gap-2 mt-6'>
<Dialog.Action
variant="cancel"
onPress={close}
>
Cancel
</Dialog.Action>
<Dialog.Action
variant="confirm"
onPress={close}
>
Register
</Dialog.Action>
</div>
</Form>
</>
)}
</Dialog.Panel>
</Dialog>
<Menu>
<Menu.Button
className={cn(
'w-fit text-sm rounded-lg px-4 py-2',
'bg-main-700 dark:bg-main-800 text-white',
'hover:bg-main-800 dark:hover:bg-main-700',
)}
>
Add Device
</Menu.Button>
<Menu.Items>
<Menu.ItemButton control={mkeyState}>
<ServerIcon className='w-4 h-4 mr-2'/>
Register Machine Key
</Menu.ItemButton>
<Menu.ItemButton control={pkeyState} isDisabled>
<KeyIcon className='w-4 h-4 mr-2'/>
Generate Pre-auth Key
</Menu.ItemButton>
</Menu.Items>
</Menu>
</>
)
}

View File

@ -15,6 +15,7 @@ import { useLiveData } from '~/utils/useLiveData'
import { menuAction } from './action'
import MachineRow from './machine'
import NewMachine from './dialogs/new'
export async function loader({ request }: LoaderFunctionArgs) {
const session = await getSession(request.headers.get('Cookie'))
@ -43,6 +44,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
routes: routes.routes,
users: users.users,
magic,
server: context.headscaleUrl,
}
}
@ -56,7 +58,10 @@ export default function Page() {
return (
<>
<h1 className="text-2xl font-medium mb-4">Machines</h1>
<div className="flex justify-between items-center">
<h1 className="text-2xl font-medium mb-4">Machines</h1>
<NewMachine server={data.server} users={data.users} />
</div>
<table className="table-auto w-full rounded-lg">
<thead className="text-gray-500 dark:text-gray-400">
<tr className="text-left uppercase text-xs font-bold px-0.5">