)
}
diff --git a/app/routes/_data.users._index.tsx b/app/routes/_data.users._index.tsx
deleted file mode 100644
index 11c09a6..0000000
--- a/app/routes/_data.users._index.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-/* eslint-disable unicorn/filename-case */
-import { PersonIcon } from '@primer/octicons-react'
-import { type LoaderFunctionArgs } from '@remix-run/node'
-import { useLoaderData } from '@remix-run/react'
-
-import Attribute from '~/components/Attribute'
-import Card from '~/components/Card'
-import StatusCircle from '~/components/StatusCircle'
-import { type Machine } from '~/types'
-import { pull } from '~/utils/headscale'
-import { getSession } from '~/utils/sessions'
-import { useLiveData } from '~/utils/useLiveData'
-
-export async function loader({ request }: LoaderFunctionArgs) {
- const session = await getSession(request.headers.get('Cookie'))
-
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const data = await pull<{ nodes: Machine[] }>('v1/node', session.get('hsApiKey')!)
-
- const users = new Map()
- for (const machine of data.nodes) {
- const { user } = machine
- if (!users.has(user.id)) {
- users.set(user.id, [])
- }
-
- users.get(user.id)?.push(machine)
- }
-
- return [...users.values()].map(machines => {
- const { user } = machines[0]
-
- return {
- ...user,
- machines
- }
- })
-}
-
-export default function Page() {
- const data = useLoaderData()
- useLiveData({ interval: 3000 })
-
- return (
-
- {data.map(user => (
-
-
-
-
- {user.name}
-
-
-
- {user.machines.map(machine => (
-
-
-
-
- ))}
-
-
- ))}
-
- )
-}
diff --git a/app/routes/_data.users._index/add.tsx b/app/routes/_data.users._index/add.tsx
new file mode 100644
index 0000000..05b9781
--- /dev/null
+++ b/app/routes/_data.users._index/add.tsx
@@ -0,0 +1,82 @@
+import { Form, useSubmit } from '@remix-run/react'
+import { useState } from 'react'
+
+import Code from '~/components/Code'
+import Dialog from '~/components/Dialog'
+import TextField from '~/components/TextField'
+
+interface Props {
+ magic?: string
+}
+
+export default function Add({ magic }: Props) {
+ const [username, setUsername] = useState('')
+ const submit = useSubmit()
+
+ return (
+
+ )
+}
diff --git a/app/routes/_data.users._index/auth.tsx b/app/routes/_data.users._index/auth.tsx
new file mode 100644
index 0000000..689dce0
--- /dev/null
+++ b/app/routes/_data.users._index/auth.tsx
@@ -0,0 +1,49 @@
+import { HomeIcon, PasskeyFillIcon } from '@primer/octicons-react'
+
+import Card from '~/components/Card'
+import Link from '~/components/Link'
+
+import Add from './add'
+
+interface Props {
+ readonly magic: string | undefined
+}
+
+export default function Auth({ magic }: Props) {
+ return (
+
+
+
+
+
+ Basic Authentication
+
+
+ Users are not managed externally.
+ Using OpenID Connect can create a better
+ experience when using Headscale.
+ {' '}
+
+ Learn more
+
+
+
+
+
+
+ User Management
+
+
+ You can add, remove, and rename users here.
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/routes/_data.users._index/oidc.tsx b/app/routes/_data.users._index/oidc.tsx
new file mode 100644
index 0000000..8017d16
--- /dev/null
+++ b/app/routes/_data.users._index/oidc.tsx
@@ -0,0 +1,56 @@
+import { OrganizationIcon, PasskeyFillIcon } from '@primer/octicons-react'
+
+import Card from '~/components/Card'
+import Link from '~/components/Link'
+import { type Context } from '~/utils/config'
+
+import Add from './add'
+
+interface Props {
+ readonly oidc: NonNullable
+ readonly magic: string | undefined
+}
+
+export default function Oidc({ oidc, magic }: Props) {
+ return (
+
+
+
+
+
+ OpenID Connect
+
+
+ Users are managed through your
+ {' '}
+
+ OpenID Connect provider
+
+ {'. '}
+ Groups and user information do not automatically sync.
+ {' '}
+
+ Learn more
+
+
+
+
+
+
+ User Management
+
+
+ You can still add users manually, however it is recommended
+ that you manage users through your OIDC provider.
+