adds oauth support
This commit is contained in:
55
web/src/routes/api/v1/auth/oauth/+server.ts
Normal file
55
web/src/routes/api/v1/auth/oauth/+server.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { env } from "$env/dynamic/public";
|
||||
import { env as private_env } from "$env/dynamic/private";
|
||||
|
||||
import { pb } from "$lib/pocketbase";
|
||||
import { error, json, type RequestEvent } from "@sveltejs/kit";
|
||||
|
||||
const redirectURL = private_env.ORIGIN + "/login/redirect"
|
||||
|
||||
export async function GET(event: RequestEvent) {
|
||||
try {
|
||||
const r = await pb.collection('users').listAuthMethods();
|
||||
|
||||
for (const provider of r.authProviders) {
|
||||
const imageURL = `${env.PUBLIC_POCKETBASE_URL}/_/images/oauth2/${provider.name}.svg`
|
||||
provider['img' as keyof typeof provider] = await imageUrlToBase64(imageURL, event.fetch);
|
||||
provider['url' as keyof typeof provider] = `${provider.authUrl}${redirectURL}`
|
||||
}
|
||||
return json(r)
|
||||
} catch (e: any) {
|
||||
throw error(e.status, e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function POST(event: RequestEvent) {
|
||||
const data = await event.request.json();
|
||||
try {
|
||||
const r = await pb.collection('users').authWithOAuth2Code(
|
||||
data.name,
|
||||
data.code,
|
||||
data.codeVerifier,
|
||||
redirectURL,
|
||||
)
|
||||
return json(r)
|
||||
} catch (e: any) {
|
||||
throw error(e.status, e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function imageUrlToBase64(url: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response>) {
|
||||
try {
|
||||
const response = await f(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
const base64DataUrl = `data:${response.headers.get('content-type') || 'image/png'};base64,${buffer.toString('base64')}`;
|
||||
return base64DataUrl;
|
||||
} catch (error) {
|
||||
console.error('Error fetching image:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,15 @@
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
import { login, type User } from "$lib/stores/user_store";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
import { ClientResponseError, type AuthProviderInfo } from "pocketbase";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { object, string } from "yup";
|
||||
|
||||
let loading: boolean = false;
|
||||
|
||||
const redirectURL = "/redirect";
|
||||
|
||||
const authProviders = $page.data.authMethods.authProviders;
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<User>({
|
||||
initialValues: {
|
||||
id: "",
|
||||
@@ -52,6 +56,13 @@
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function setProvider(provider: AuthProviderInfo) {
|
||||
localStorage.setItem(
|
||||
"provider",
|
||||
JSON.stringify(provider),
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -59,7 +70,7 @@
|
||||
</svelte:head>
|
||||
<main class="flex justify-center">
|
||||
<form
|
||||
class="login-panel max-w-md border border-input-border rounded-xl p-8 flex flex-col justify-center items-center gap-8 w-[28rem] mt-8"
|
||||
class="login-panel max-w-md border border-input-border rounded-xl p-8 flex flex-col justify-center items-center gap-4 w-[28rem] mt-8"
|
||||
on:submit={handleSubmit}
|
||||
>
|
||||
{#if $theme == "light"}
|
||||
@@ -99,5 +110,28 @@
|
||||
></span
|
||||
>
|
||||
{/if}
|
||||
{#if authProviders.length}
|
||||
<div class="flex gap-4 items-center w-full">
|
||||
<hr class="basis-full border-input-border" />
|
||||
<span class="text-gray-500 uppercase">{$_("or")}</span>
|
||||
<hr class="basis-full border-input-border" />
|
||||
</div>
|
||||
<div class="w-80 space-y-4">
|
||||
{#each authProviders as provider}
|
||||
<a
|
||||
href={provider.url}
|
||||
class="btn-secondary inline-flex min-w-full justify-center"
|
||||
on:click={() => setProvider(provider)}
|
||||
>
|
||||
<img
|
||||
class="w-5 aspect-square mr-4"
|
||||
src={provider.img}
|
||||
alt="Provider logo"
|
||||
/>
|
||||
Login with {provider.displayName}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</main>
|
||||
|
||||
9
web/src/routes/login/+page.ts
Normal file
9
web/src/routes/login/+page.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { users_auth_methods } from "$lib/stores/user_store";
|
||||
import { type Load } from "@sveltejs/kit";
|
||||
|
||||
export const load: Load = async ({ fetch }) => {
|
||||
|
||||
const authMethods = await users_auth_methods(fetch)
|
||||
|
||||
return { authMethods: authMethods }
|
||||
};
|
||||
72
web/src/routes/login/redirect/+page.svelte
Normal file
72
web/src/routes/login/redirect/+page.svelte
Normal file
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
import { oauth_login } from "$lib/stores/user_store";
|
||||
import type { AuthProviderInfo } from "pocketbase";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let error = $page.url.searchParams.get("error");
|
||||
let errorDescription = $page.url.searchParams.get("error_description");
|
||||
const errorURI = $page.url.searchParams.get("error_uri");
|
||||
|
||||
const state = $page.url.searchParams.get("state");
|
||||
const code = $page.url.searchParams.get("code");
|
||||
onMount(async () => {
|
||||
if (error || !state || !code) {
|
||||
return;
|
||||
}
|
||||
|
||||
const providerData = localStorage.getItem("provider");
|
||||
|
||||
if (!providerData) {
|
||||
error = "missing_provider";
|
||||
errorDescription =
|
||||
"No OAuth provider was specified in local storage.";
|
||||
return;
|
||||
}
|
||||
|
||||
const provider: AuthProviderInfo = JSON.parse(providerData);
|
||||
|
||||
if (provider.state !== state) {
|
||||
error = "mismacthed_provider";
|
||||
errorDescription =
|
||||
"OAuth provider does not match the one defined in local storage.";
|
||||
return;
|
||||
}
|
||||
|
||||
oauth_login({
|
||||
name: provider.name,
|
||||
code: code,
|
||||
codeVerifier: provider.codeVerifier,
|
||||
})
|
||||
.then(() => {
|
||||
goto("/");
|
||||
})
|
||||
.catch((e) => {
|
||||
error = "oauth_error";
|
||||
errorDescription = e.toString();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<main
|
||||
class="flex items-center justify-center"
|
||||
style="min-height: calc(100vh - 388px)"
|
||||
>
|
||||
{#if error}
|
||||
<div
|
||||
class="rounded-xl bg-input-background-error border border-red-400 p-6 max-w-xl space-y-4"
|
||||
>
|
||||
<h5 class="text-xl font-semibold">{error}</h5>
|
||||
<p>{errorDescription}</p>
|
||||
{#if errorURI}
|
||||
<p><a class="underline" href={errorURI}>More Info</a></p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="max-w-fit space-y-4 text-center">
|
||||
<div class="spinner spinner-dark"></div>
|
||||
<h5 class="text-xl font-semibold">Authenticating...</h5>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
Reference in New Issue
Block a user