feat: oidc based authentication
This commit is contained in:
parent
025cc4f6f1
commit
58992efa2e
10 changed files with 494 additions and 62 deletions
32
app/routes/_data.machines.tsx
Normal file
32
app/routes/_data.machines.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export default function Index() {
|
||||
return (
|
||||
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.8' }}>
|
||||
<h1>Welcome to Remix</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
target='_blank'
|
||||
href='https://remix.run/tutorials/blog'
|
||||
rel='noreferrer'
|
||||
>
|
||||
15m Quickstart Blog Tutorial
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target='_blank'
|
||||
href='https://remix.run/tutorials/jokes'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Deep Dive Jokes App Tutorial
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target='_blank' href='https://remix.run/docs' rel='noreferrer'>
|
||||
Remix Docs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
40
app/routes/_data.tsx
Normal file
40
app/routes/_data.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { CpuChipIcon, ServerStackIcon } from '@heroicons/react/24/outline'
|
||||
import { type LoaderFunctionArgs, redirect } from '@remix-run/node'
|
||||
import { Outlet } from '@remix-run/react'
|
||||
|
||||
import TabLink from '~/components/TabLink'
|
||||
import { getSession } from '~/utils/sessions'
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const session = await getSession(request.headers.get('Cookie'))
|
||||
if (!session.has('hsApiKey')) {
|
||||
return redirect('/login')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/no-null
|
||||
return null
|
||||
}
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<>
|
||||
<header className='bg-gray-800 text-white mb-16'>
|
||||
<nav className='container mx-auto'>
|
||||
<div className='flex items-center gap-x-2 mb-8 pt-4'>
|
||||
<CpuChipIcon className='w-8 h-8'/>
|
||||
<h1 className='text-2xl'>Headplane</h1>
|
||||
</div>
|
||||
<div className='flex items-center gap-x-4'>
|
||||
<TabLink to='/machines' name='Machines' icon={<ServerStackIcon className='w-4 h-4'/>}/>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main className='container mx-auto overscroll-contain'>
|
||||
<Outlet/>
|
||||
</main>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1,41 +1,5 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { redirect } from '@remix-run/node'
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "New Remix App" },
|
||||
{ name: "description", content: "Welcome to Remix!" },
|
||||
];
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
|
||||
<h1>Welcome to Remix</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://remix.run/tutorials/blog"
|
||||
rel="noreferrer"
|
||||
>
|
||||
15m Quickstart Blog Tutorial
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://remix.run/tutorials/jokes"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Deep Dive Jokes App Tutorial
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" href="https://remix.run/docs" rel="noreferrer">
|
||||
Remix Docs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
export function loader() {
|
||||
return redirect('/machines')
|
||||
}
|
||||
|
|
|
|||
82
app/routes/login.tsx
Normal file
82
app/routes/login.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { type LoaderFunctionArgs } from '@remix-run/node'
|
||||
import { Link, useLoaderData } from '@remix-run/react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { startOidc } from '~/utils/oidc'
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const issuer = process.env.OIDC_ISSUER
|
||||
const id = process.env.OIDC_CLIENT_ID
|
||||
const secret = process.env.OIDC_CLIENT_SECRET
|
||||
const normal = process.env.DISABLE_API_KEY_LOGIN
|
||||
|
||||
if (issuer && (!id || !secret)) {
|
||||
throw new Error('An invalid OIDC configuration was provided')
|
||||
}
|
||||
|
||||
const data = {
|
||||
oidc: issuer,
|
||||
apiKey: normal === undefined
|
||||
}
|
||||
|
||||
console.log(data)
|
||||
|
||||
if (!data.oidc && !data.apiKey) {
|
||||
throw new Error('No authentication method is enabled')
|
||||
}
|
||||
|
||||
if (data.oidc && !data.apiKey) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return startOidc(data.oidc, id!, request)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const data = useLoaderData<typeof loader>()
|
||||
const showOr = useMemo(() => data.oidc && data.apiKey, [data])
|
||||
|
||||
return (
|
||||
<div className='flex min-h-screen items-center justify-center'>
|
||||
<div className='w-1/3 border p-4 rounded-lg'>
|
||||
<h1 className='text-2xl mb-8'>Login</h1>
|
||||
{data.apiKey ? (
|
||||
<>
|
||||
<p className='text-sm text-gray-500 mb-4'>
|
||||
Enter an API key to authenticate with Headplane. You can generate
|
||||
one by running
|
||||
{' '}
|
||||
<code className='bg-gray-100 p-1 rounded-md'>
|
||||
headscale apikeys create
|
||||
</code>
|
||||
{' '}
|
||||
in your terminal.
|
||||
</p>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
id='api-key'
|
||||
className='border rounded-md p-2 w-full'
|
||||
placeholder='API Key'
|
||||
/>
|
||||
</>
|
||||
) : undefined}
|
||||
{showOr ? (
|
||||
<div className='flex items-center gap-x-2 py-2'>
|
||||
<hr className='flex-1'/>
|
||||
<span className='text-gray-500'>or</span>
|
||||
<hr className='flex-1'/>
|
||||
</div>
|
||||
) : undefined}
|
||||
{data.oidc ? (
|
||||
<Link to='/oidc/start'>
|
||||
<button className='bg-gray-800 text-white rounded-md p-2 w-full' type='button'>
|
||||
Login with SSO
|
||||
</button>
|
||||
</Link>
|
||||
) : undefined}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
15
app/routes/oidc.callback.tsx
Normal file
15
app/routes/oidc.callback.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { type LoaderFunctionArgs } from '@remix-run/node'
|
||||
|
||||
import { finishOidc } from '~/utils/oidc'
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const issuer = process.env.OIDC_ISSUER
|
||||
const id = process.env.OIDC_CLIENT_ID
|
||||
const secret = process.env.OIDC_CLIENT_SECRET
|
||||
|
||||
if (!issuer || !id || !secret) {
|
||||
throw new Error('An invalid OIDC configuration was provided')
|
||||
}
|
||||
|
||||
return finishOidc(issuer, id, secret, request)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue