Compare commits

..

5 Commits

Author SHA1 Message Date
ef51513fe1
remove readme todos
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
2025-05-02 15:56:51 -07:00
94514ec965
auth: refactor common oauth provider logic, add options to disable providers and require invites 2025-05-02 15:53:21 -07:00
f9a27cbbb7
WIP: auth: refactor to page routes instead of api routes 2025-05-02 13:57:17 -07:00
69150caab3
WIP: temp 2025-05-02 13:57:16 -07:00
2ed0b70780
WIP: auth: improve handling of invite tokens 2025-05-02 13:57:14 -07:00
5 changed files with 52 additions and 53 deletions

View File

@ -1,28 +0,0 @@
<script lang="ts">
import { LucideLoaderCircle } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
interface Props {
providerName: string;
displayName: string;
iconSrc: string;
inviteToken?: string;
}
let { providerName, displayName, inviteToken, iconSrc }: Props = $props();
let submitted = $state(false);
</script>
<form method="get" onsubmit={() => (submitted = true)} action="/auth/{providerName}">
{#if inviteToken}
<input type="hidden" value={inviteToken} name="invite" />
{/if}
<Button type="submit" disabled={submitted}>
{#if submitted}
<LucideLoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<img class="mr-2 h-4 w-4" alt="{displayName} Logo" src={iconSrc} />
{/if}
Sign {inviteToken ? 'up' : 'in'} with {displayName}
</Button>
</form>

View File

@ -1,26 +1,54 @@
<script lang="ts"> <script lang="ts">
import { LucideLoaderCircle } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import googleIcon from '$lib/assets/google.svg'; import googleIcon from '$lib/assets/google.svg';
import { enabledAuthProviders } from '$lib/auth'; import { enabledAuthProviders } from '$lib/auth';
import AuthButton from './auth-button.svelte';
interface Props { let { inviteToken, class: className, ...rest }: {
inviteToken?: string; inviteToken?: string;
class?: string; class?: string;
} rest?: { [p: string]: unknown }
let { inviteToken, class: className }: Props = $props(); } = $props();
let submitted = $state(false);
</script> </script>
<div class={cn('flex gap-6', className)}> <div class={cn('flex gap-6', className)} {...rest}>
{#if enabledAuthProviders.authentik} {#if enabledAuthProviders.authentik }
<AuthButton <form method="get" onsubmit={() => submitted = true}
providerName="authentik" action="/auth/authentik{inviteToken ? `?invite=${inviteToken}` : ''}">
displayName="Authentik" <input type="hidden" value={inviteToken} name="invite" />
iconSrc="https://auth.cazzzer.com/static/dist/assets/icons/icon.svg" <Button type="submit" disabled={submitted}>
{inviteToken} {#if submitted}
/> <LucideLoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<img
class="mr-2 h-4 w-4"
alt="Authentik Logo"
src="https://auth.cazzzer.com/static/dist/assets/icons/icon.svg"
/>
{/if}
Sign in with Authentik
</Button>
</form>
{/if} {/if}
{#if enabledAuthProviders.google} {#if enabledAuthProviders.google }
<AuthButton providerName="google" displayName="Google" iconSrc={googleIcon} {inviteToken} /> <form method="get" onsubmit={() => submitted = true}
action="/auth/google{inviteToken ? `?invite=${inviteToken}` : ''}">
<input type="hidden" value={inviteToken} name="invite" />
<Button type="submit" disabled={submitted}>
{#if submitted}
<LucideLoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<img
class="mr-2 h-4 w-4"
alt="Google Logo"
src={googleIcon}
/>
{/if}
Sign in with Google
</Button>
</form>
{/if} {/if}
</div> </div>

View File

@ -3,24 +3,24 @@ import { oauthProviders } from '$lib/server/oauth';
import { is } from 'typia'; import { is } from 'typia';
import { type AuthProvider, enabledAuthProviders } from '$lib/auth'; import { type AuthProvider, enabledAuthProviders } from '$lib/auth';
export async function GET({ params: { provider }, url, cookies }) { export async function GET(event) {
const { provider } = event.params;
if (!is<AuthProvider>(provider) || !enabledAuthProviders[provider]) { if (!is<AuthProvider>(provider) || !enabledAuthProviders[provider]) {
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
} }
const oauthProvider = oauthProviders[provider]; const oauthProvider = oauthProviders[provider];
const inviteToken = url.searchParams.get('invite') ?? '';
const state = generateState(); const state = generateState();
const codeVerifier = generateCodeVerifier(); const codeVerifier = generateCodeVerifier();
const authUrl = oauthProvider.createAuthorizationURL(state + inviteToken, codeVerifier); const url = oauthProvider.createAuthorizationURL(state, codeVerifier);
cookies.set(`${provider}_oauth_state`, state, { event.cookies.set(`${provider}_oauth_state`, state, {
path: '/', path: '/',
httpOnly: true, httpOnly: true,
maxAge: 60 * 10, // 10 minutes maxAge: 60 * 10, // 10 minutes
sameSite: 'lax', sameSite: 'lax',
}); });
cookies.set(`${provider}_code_verifier`, codeVerifier, { event.cookies.set(`${provider}_code_verifier`, codeVerifier, {
path: '/', path: '/',
httpOnly: true, httpOnly: true,
maxAge: 60 * 10, // 10 minutes maxAge: 60 * 10, // 10 minutes
@ -30,7 +30,7 @@ export async function GET({ params: { provider }, url, cookies }) {
return new Response(null, { return new Response(null, {
status: 302, status: 302,
headers: { headers: {
Location: authUrl.toString(), Location: url.toString(),
}, },
}); });
} }

View File

@ -2,7 +2,6 @@ import type { LayoutServerLoad } from './$types';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { isValidInviteToken } from '$lib/server/auth'; import { isValidInviteToken } from '$lib/server/auth';
export const load: LayoutServerLoad = ({ params, locals }) => { export const load: LayoutServerLoad = ({ params }) => {
if (!isValidInviteToken(params.id)) redirect(302, '/'); if (!isValidInviteToken(params.id)) redirect(307, '/')
if (locals.user !== null) redirect(302, '/');
}; };

View File

@ -10,7 +10,7 @@
</svelte:head> </svelte:head>
<h1 class="mb-2 scroll-m-20 text-center text-3xl font-extrabold tracking-tight lg:text-4xl"> <h1 class="mb-2 scroll-m-20 text-center text-3xl font-extrabold tracking-tight lg:text-4xl">
You are invited to VPGen Welcome to VPGen
</h1> </h1>
<AuthForm {inviteToken} /> <AuthForm inviteToken={inviteToken} />