chore: switch to react-router v7

This commit is contained in:
Aarnav Tale
2024-12-31 10:30:14 +05:30
parent 39504e2487
commit aa9872a45b
101 changed files with 3825 additions and 6796 deletions
+102 -96
View File
@@ -1,168 +1,174 @@
import { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'
import { useLiveData } from '~/utils/useLiveData'
import { getSession } from '~/utils/sessions'
import { Link as RemixLink } from '@remix-run/react'
import { PreAuthKey, User } from '~/types'
import { pull, post } from '~/utils/headscale'
import { loadContext } from '~/utils/config/headplane'
import { useState } from 'react'
import { send } from '~/utils/res'
import { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
import { useLoaderData } from 'react-router';
import { useLiveData } from '~/utils/useLiveData';
import { getSession } from '~/utils/sessions';
import { Link as RemixLink } from 'react-router';
import { PreAuthKey, User } from '~/types';
import { pull, post } from '~/utils/headscale';
import { loadContext } from '~/utils/config/headplane';
import { useState } from 'react';
import { send } from '~/utils/res';
import Link from '~/components/Link'
import TableList from '~/components/TableList'
import Select from '~/components/Select'
import Switch from '~/components/Switch'
import Link from '~/components/Link';
import TableList from '~/components/TableList';
import Select from '~/components/Select';
import Switch from '~/components/Switch';
import AddPreAuthKey from './dialogs/new'
import AuthKeyRow from './components/key'
import AddPreAuthKey from './dialogs/new';
import AuthKeyRow from './components/key';
export async function action({ request }: ActionFunctionArgs) {
const session = await getSession(request.headers.get('Cookie'))
const session = await getSession(request.headers.get('Cookie'));
if (!session.has('hsApiKey')) {
return send({ message: 'Unauthorized' }, {
status: 401,
})
return send(
{ message: 'Unauthorized' },
{
status: 401,
},
);
}
const data = await request.formData()
const data = await request.formData();
// Expiring a pre-auth key
if (request.method === 'DELETE') {
const key = data.get('key')
const user = data.get('user')
const key = data.get('key');
const user = data.get('user');
if (!key || !user) {
return send({ message: 'Missing parameters' }, {
status: 400,
})
return send(
{ message: 'Missing parameters' },
{
status: 400,
},
);
}
await post<{ preAuthKey: PreAuthKey }>(
'v1/preauthkey/expire',
session.get('hsApiKey')!,
session.get('hsApiKey')!,
{
user: user,
key: key,
}
)
},
);
return { message: 'Pre-auth key expired' }
return { message: 'Pre-auth key expired' };
}
// Creating a new pre-auth key
if (request.method === 'POST') {
const user = data.get('user')
const expiry = data.get('expiry')
const reusable = data.get('reusable')
const ephemeral = data.get('ephemeral')
const user = data.get('user');
const expiry = data.get('expiry');
const reusable = data.get('reusable');
const ephemeral = data.get('ephemeral');
if (!user || !expiry || !reusable || !ephemeral) {
return send({ message: 'Missing parameters' }, {
status: 400,
})
return send(
{ message: 'Missing parameters' },
{
status: 400,
},
);
}
// Extract the first "word" from expiry which is the day number
// Calculate the date X days from now using the day number
const day = Number(expiry.toString().split(' ')[0])
const date = new Date()
date.setDate(date.getDate() + day)
const day = Number(expiry.toString().split(' ')[0]);
const date = new Date();
date.setDate(date.getDate() + day);
const key = await post<{ preAuthKey: PreAuthKey }>(
'v1/preauthkey',
session.get('hsApiKey')!,
session.get('hsApiKey')!,
{
user: user,
ephemeral: ephemeral === 'on',
reusable: reusable === 'on',
expiration: date.toISOString(),
aclTags: [], // TODO
}
)
},
);
return { message: 'Pre-auth key created', key }
return { message: 'Pre-auth key created', key };
}
}
export async function loader({ request }: LoaderFunctionArgs) {
const context = await loadContext()
const session = await getSession(request.headers.get('Cookie'))
const users = await pull<{ users: User[] }>('v1/user', session.get('hsApiKey')!)
const context = await loadContext();
const session = await getSession(request.headers.get('Cookie'));
const users = await pull<{ users: User[] }>(
'v1/user',
session.get('hsApiKey')!,
);
const preAuthKeys = await Promise.all(users.users.map(user => {
const qp = new URLSearchParams()
qp.set('user', user.name)
const preAuthKeys = await Promise.all(
users.users.map((user) => {
const qp = new URLSearchParams();
qp.set('user', user.name);
return pull<{ preAuthKeys: PreAuthKey[] }>(
`v1/preauthkey?${qp.toString()}`,
session.get('hsApiKey')!
)
}))
return pull<{ preAuthKeys: PreAuthKey[] }>(
`v1/preauthkey?${qp.toString()}`,
session.get('hsApiKey')!,
);
}),
);
return {
keys: preAuthKeys.flatMap(keys => keys.preAuthKeys),
keys: preAuthKeys.flatMap((keys) => keys.preAuthKeys),
users: users.users,
server: context.headscalePublicUrl ?? context.headscaleUrl,
}
};
}
export default function Page() {
const { keys, users, server } = useLoaderData<typeof loader>()
const [user, setUser] = useState('All')
const [status, setStatus] = useState('Active')
useLiveData({ interval: 3000 })
const { keys, users, server } = useLoaderData<typeof loader>();
const [user, setUser] = useState('All');
const [status, setStatus] = useState('Active');
useLiveData({ interval: 3000 });
const filteredKeys = keys.filter(key => {
const filteredKeys = keys.filter((key) => {
if (user !== 'All' && key.user !== user) {
return false
return false;
}
if (status !== 'All') {
const now = new Date()
const expiry = new Date(key.expiration)
const now = new Date();
const expiry = new Date(key.expiration);
if (status === 'Active') {
return !(expiry < now) && !key.used
return !(expiry < now) && !key.used;
}
if (status === 'Used/Expired') {
return key.used || expiry < now
return key.used || expiry < now;
}
if (status === 'Reusable') {
return key.reusable
return key.reusable;
}
if (status === 'Ephemeral') {
return key.ephemeral
return key.ephemeral;
}
}
return true
})
return true;
});
return (
<div className='flex flex-col w-2/3'>
<div className="flex flex-col w-2/3">
<p className="mb-8 text-md">
<RemixLink
to="/settings"
className="font-medium"
>
<RemixLink to="/settings" className="font-medium">
Settings
</RemixLink>
<span className="mx-2">
/
</span>
{' '}
Pre-Auth Keys
<span className="mx-2">/</span> Pre-Auth Keys
</p>
<h1 className='text-2xl font-medium mb-4'>Pre-Auth Keys</h1>
<h1 className="text-2xl font-medium mb-4">Pre-Auth Keys</h1>
<p className="text-gray-700 dark:text-gray-300 mb-4">
Headscale fully supports pre-authentication keys in order to
easily add devices to your Tailnet.
To learn more about using pre-authentication keys, visit the
{' '}
Headscale fully supports pre-authentication keys in order to easily add
devices to your Tailnet. To learn more about using pre-authentication
keys, visit the{' '}
<Link
to="https://tailscale.com/kb/1085/auth-keys/"
name="Tailscale Auth Keys documentation"
@@ -182,7 +188,7 @@ export default function Page() {
state={[user, setUser]}
>
<Select.Item id="All">All</Select.Item>
{users.map(user => (
{users.map((user) => (
<Select.Item key={user.id} id={user.name}>
{user.name}
</Select.Item>
@@ -209,16 +215,16 @@ export default function Page() {
<TableList className="mt-4">
{filteredKeys.length === 0 ? (
<TableList.Item>
<p className="opacity-50 text-sm mx-auto">
No pre-auth keys
</p>
<p className="opacity-50 text-sm mx-auto">No pre-auth keys</p>
</TableList.Item>
) : filteredKeys.map(key => (
<TableList.Item key={key.id}>
<AuthKeyRow authKey={key} server={server} />
</TableList.Item>
))}
) : (
filteredKeys.map((key) => (
<TableList.Item key={key.id}>
<AuthKeyRow authKey={key} server={server} />
</TableList.Item>
))
)}
</TableList>
</div>
)
);
}
+19 -20
View File
@@ -1,19 +1,19 @@
import type { PreAuthKey } from '~/types'
import { toast } from '~/components/Toaster'
import type { PreAuthKey } from '~/types';
import { toast } from '~/components/Toaster';
import Code from '~/components/Code'
import Button from '~/components/Button'
import Attribute from '~/components/Attribute'
import ExpireKey from '../dialogs/expire'
import Code from '~/components/Code';
import Button from '~/components/Button';
import Attribute from '~/components/Attribute';
import ExpireKey from '../dialogs/expire';
interface Props {
authKey: PreAuthKey
server: string
authKey: PreAuthKey;
server: string;
}
export default function AuthKeyRow({ authKey, server }: Props) {
const createdAt = new Date(authKey.createdAt).toLocaleString()
const expiration = new Date(authKey.expiration).toLocaleString()
const createdAt = new Date(authKey.createdAt).toLocaleString();
const expiration = new Date(authKey.expiration).toLocaleString();
return (
<div className="w-full">
@@ -31,25 +31,24 @@ export default function AuthKeyRow({ authKey, server }: Props) {
tailscale up --login-server {server} --authkey {authKey.key}
</Code>
<div className="flex gap-4 items-center">
{authKey.used || new Date(authKey.expiration) < new Date()
? undefined
: (
<ExpireKey authKey={authKey} />
)}
{authKey.used ||
new Date(authKey.expiration) < new Date() ? undefined : (
<ExpireKey authKey={authKey} />
)}
<Button
variant="light"
variant="light"
className="my-4"
onPress={async () => {
await navigator.clipboard.writeText(
`tailscale up --login-server ${server} --authkey ${authKey.key}`
)
`tailscale up --login-server ${server} --authkey ${authKey.key}`,
);
toast('Copied command to clipboard')
toast('Copied command to clipboard');
}}
>
Copy Tailscale Command
</Button>
</div>
</div>
)
);
}
+25 -32
View File
@@ -1,45 +1,40 @@
import { useFetcher } from '@remix-run/react'
import type { PreAuthKey } from '~/types'
import { cn } from '~/utils/cn'
import { useFetcher } from 'react-router';
import type { PreAuthKey } from '~/types';
import { cn } from '~/utils/cn';
import Dialog from '~/components/Dialog'
import Spinner from '~/components/Spinner'
import Dialog from '~/components/Dialog';
import Spinner from '~/components/Spinner';
interface Props {
authKey: PreAuthKey
authKey: PreAuthKey;
}
export default function ExpireKey({ authKey }: Props) {
const fetcher = useFetcher()
const fetcher = useFetcher();
return (
<Dialog>
<Dialog.Button className="my-4">
Expire Key
</Dialog.Button>
<Dialog.Button className="my-4">Expire Key</Dialog.Button>
<Dialog.Panel>
{close => (
{(close) => (
<>
<Dialog.Title>
Expire auth key?
</Dialog.Title>
<fetcher.Form method="DELETE" onSubmit={e => {
fetcher.submit(e.currentTarget)
close()
}}>
<Dialog.Title>Expire auth key?</Dialog.Title>
<fetcher.Form
method="DELETE"
onSubmit={(e) => {
fetcher.submit(e.currentTarget);
close();
}}
>
<input type="hidden" name="user" value={authKey.user} />
<input type="hidden" name="key" value={authKey.key} />
<Dialog.Text>
Expiring this authentication key will immediately
prevent it from being used to authenticate new devices.
{' '}
This action cannot be undone.
Expiring this authentication key will immediately prevent it
from being used to authenticate new devices. This action cannot
be undone.
</Dialog.Text>
<div className="mt-6 flex justify-end gap-2 mt-6">
<Dialog.Action
variant="cancel"
onPress={close}
>
<Dialog.Action variant="cancel" onPress={close}>
Cancel
</Dialog.Action>
<Dialog.Action
@@ -52,11 +47,9 @@ export default function ExpireKey({ authKey }: Props) {
)}
onPress={close}
>
{fetcher.state === 'idle'
? undefined
: (
<Spinner className="w-3 h-3" />
)}
{fetcher.state === 'idle' ? undefined : (
<Spinner className="w-3 h-3" />
)}
Expire
</Dialog.Action>
</div>
@@ -65,5 +58,5 @@ export default function ExpireKey({ authKey }: Props) {
)}
</Dialog.Panel>
</Dialog>
)
);
}
+55 -58
View File
@@ -1,50 +1,47 @@
import { RepoForkedIcon } from '@primer/octicons-react'
import { useFetcher } from '@remix-run/react'
import { useState } from 'react'
import { RepoForkedIcon } from '@primer/octicons-react';
import { useFetcher } from 'react-router';
import { useState } from 'react';
import Dialog from '~/components/Dialog'
import TextField from '~/components/TextField'
import NumberField from '~/components/NumberField'
import Tooltip from '~/components/Tooltip'
import Select from '~/components/Select'
import Switch from '~/components/Switch'
import Link from '~/components/Link'
import Spinner from '~/components/Spinner'
import Dialog from '~/components/Dialog';
import TextField from '~/components/TextField';
import NumberField from '~/components/NumberField';
import Tooltip from '~/components/Tooltip';
import Select from '~/components/Select';
import Switch from '~/components/Switch';
import Link from '~/components/Link';
import Spinner from '~/components/Spinner';
import { cn } from '~/utils/cn'
import { User } from '~/types'
import { cn } from '~/utils/cn';
import { User } from '~/types';
interface Props {
users: User[]
users: User[];
}
// TODO: Tags
export default function AddPreAuthKey(data: Props) {
const fetcher = useFetcher()
const [user, setUser] = useState('')
const [reusable, setReusable] = useState(false)
const [ephemeral, setEphemeral] = useState(false)
const [aclTags, setAclTags] = useState([])
const [expiry, setExpiry] = useState(90)
const fetcher = useFetcher();
const [user, setUser] = useState('');
const [reusable, setReusable] = useState(false);
const [ephemeral, setEphemeral] = useState(false);
const [aclTags, setAclTags] = useState([]);
const [expiry, setExpiry] = useState(90);
return (
<Dialog>
<Dialog.Button className="my-4">
Create pre-auth key
</Dialog.Button>
<Dialog.Button className="my-4">Create pre-auth key</Dialog.Button>
<Dialog.Panel>
{close => (
{(close) => (
<>
<Dialog.Title>
Generate auth key
</Dialog.Title>
<fetcher.Form method="POST" onSubmit={e => {
fetcher.submit(e.currentTarget)
close()
}}>
<Dialog.Text className="font-semibold">
User
</Dialog.Text>
<Dialog.Title>Generate auth key</Dialog.Title>
<fetcher.Form
method="POST"
onSubmit={(e) => {
fetcher.submit(e.currentTarget);
close();
}}
>
<Dialog.Text className="font-semibold">User</Dialog.Text>
<Dialog.Text className="text-sm">
Attach this key to a user
</Dialog.Text>
@@ -54,7 +51,7 @@ export default function AddPreAuthKey(data: Props) {
placeholder="Select a user"
state={[user, setUser]}
>
{data.users.map(user => (
{data.users.map((user) => (
<Select.Item key={user.id} id={user.name}>
{user.name}
</Select.Item>
@@ -80,9 +77,7 @@ export default function AddPreAuthKey(data: Props) {
/>
<div className="flex justify-between items-center mt-6">
<div>
<Dialog.Text className="font-semibold">
Reusable
</Dialog.Text>
<Dialog.Text className="font-semibold">Reusable</Dialog.Text>
<Dialog.Text className="text-sm">
Use this key to authenticate more than one device.
</Dialog.Text>
@@ -91,19 +86,22 @@ export default function AddPreAuthKey(data: Props) {
label="Reusable"
name="reusable"
defaultSelected={reusable}
onChange={() => { setReusable(!reusable) }}
onChange={() => {
setReusable(!reusable);
}}
/>
</div>
<input type="hidden" name="reusable" value={reusable.toString()} />
<input
type="hidden"
name="reusable"
value={reusable.toString()}
/>
<div className="flex justify-between items-center mt-6">
<div>
<Dialog.Text className="font-semibold">
Ephemeral
</Dialog.Text>
<Dialog.Text className="font-semibold">Ephemeral</Dialog.Text>
<Dialog.Text className="text-sm">
Devices authenticated with this key will
be automatically removed once they go offline.
{' '}
Devices authenticated with this key will be automatically
removed once they go offline.{' '}
<Link
to="https://tailscale.com/kb/1111/ephemeral-nodes"
name="Tailscale Ephemeral Nodes Documentation"
@@ -117,16 +115,17 @@ export default function AddPreAuthKey(data: Props) {
name="ephemeral"
defaultSelected={ephemeral}
onChange={() => {
setEphemeral(!ephemeral)
setEphemeral(!ephemeral);
}}
/>
</div>
<input type="hidden" name="ephemeral" value={ephemeral.toString()} />
<input
type="hidden"
name="ephemeral"
value={ephemeral.toString()}
/>
<div className="mt-6 flex justify-end gap-2 mt-6">
<Dialog.Action
variant="cancel"
onPress={close}
>
<Dialog.Action variant="cancel" onPress={close}>
Cancel
</Dialog.Action>
<Dialog.Action
@@ -134,11 +133,9 @@ export default function AddPreAuthKey(data: Props) {
onPress={close}
isDisabled={!user || !expiry}
>
{fetcher.state === 'idle'
? undefined
: (
<Spinner className="w-3 h-3" />
)}
{fetcher.state === 'idle' ? undefined : (
<Spinner className="w-3 h-3" />
)}
Generate
</Dialog.Action>
</div>
@@ -147,5 +144,5 @@ export default function AddPreAuthKey(data: Props) {
)}
</Dialog.Panel>
</Dialog>
)
);
}
+22 -22
View File
@@ -1,28 +1,26 @@
import Link from '~/components/Link'
import Button from '~/components/Button'
import { Link as RemixLink } from '@remix-run/react'
import { ArrowRightIcon } from '@primer/octicons-react'
import { cn } from '~/utils/cn'
import Link from '~/components/Link';
import Button from '~/components/Button';
import { Link as RemixLink } from 'react-router';
import { ArrowRightIcon } from '@primer/octicons-react';
import { cn } from '~/utils/cn';
export default function Page() {
return (
<div className="flex flex-col gap-8 max-w-screen-lg">
<div className='flex flex-col w-2/3'>
<h1 className='text-2xl font-medium mb-4'>Settings</h1>
<div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Settings</h1>
<p className="text-gray-700 dark:text-gray-300">
The settings page is still under construction.
As I'm able to add more features, I'll be adding them here.
If you require any features, feel free to open an issue on
the GitHub repository.
The settings page is still under construction. As I'm able to add more
features, I'll be adding them here. If you require any features, feel
free to open an issue on the GitHub repository.
</p>
</div>
<div className='flex flex-col w-2/3'>
<h1 className='text-2xl font-medium mb-4'>Pre-Auth Keys</h1>
<div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Pre-Auth Keys</h1>
<p className="text-gray-700 dark:text-gray-300">
Headscale fully supports pre-authentication keys in order to
easily add devices to your Tailnet.
To learn more about using pre-authentication keys, visit the
{' '}
Headscale fully supports pre-authentication keys in order to easily
add devices to your Tailnet. To learn more about using
pre-authentication keys, visit the{' '}
<Link
to="https://tailscale.com/kb/1085/auth-keys/"
name="Tailscale Auth Keys documentation"
@@ -32,14 +30,16 @@ export default function Page() {
</p>
</div>
<RemixLink to="/settings/auth-keys">
<span className={cn(
'text-lg font-medium',
'text-gray-700 dark:text-gray-300',
)}>
<span
className={cn(
'text-lg font-medium',
'text-gray-700 dark:text-gray-300',
)}
>
Manage Auth Keys
<ArrowRightIcon className="w-5 h-5 ml-2" />
</span>
</RemixLink>
</div>
)
);
}