mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-30 19:26:37 +00:00
initial commit
This commit is contained in:
16
frontend/src/routes/authorize/+page.server.ts
Normal file
16
frontend/src/routes/authorize/+page.server.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import OidcService from '$lib/services/oidc-service';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ url, cookies }) => {
|
||||
const clientId = url.searchParams.get('client_id');
|
||||
const oidcService = new OidcService(cookies.get('access_token'));
|
||||
|
||||
const client = await oidcService.getClient(clientId!);
|
||||
|
||||
return {
|
||||
scope: url.searchParams.get('scope')!,
|
||||
nonce: url.searchParams.get('nonce') || undefined,
|
||||
state: url.searchParams.get('state')!,
|
||||
client
|
||||
};
|
||||
};
|
||||
131
frontend/src/routes/authorize/+page.svelte
Normal file
131
frontend/src/routes/authorize/+page.svelte
Normal file
@@ -0,0 +1,131 @@
|
||||
<script lang="ts">
|
||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import OidcService from '$lib/services/oidc-service';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import applicationConfigurationStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { getWebauthnErrorMessage } from '$lib/utils/error-util';
|
||||
import { startAuthentication } from '@simplewebauthn/browser';
|
||||
import { AxiosError } from 'axios';
|
||||
import { LucideMail, LucideUser } from 'lucide-svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import type { PageData } from './$types';
|
||||
import ClientProviderImages from './components/client-provider-images.svelte';
|
||||
import ScopeItem from './components/scope-item.svelte';
|
||||
|
||||
const webauthnService = new WebAuthnService();
|
||||
const oidService = new OidcService();
|
||||
|
||||
let isLoading = false;
|
||||
let success = false;
|
||||
let errorMessage: string | null = null;
|
||||
let authorizationRequired = false;
|
||||
|
||||
export let data: PageData;
|
||||
let { scope, nonce, client, state } = data;
|
||||
|
||||
async function authorize() {
|
||||
isLoading = true;
|
||||
try {
|
||||
// Get access token if not signed in
|
||||
if (!$userStore?.id) {
|
||||
const loginOptions = await webauthnService.getLoginOptions();
|
||||
const authResponse = await startAuthentication(loginOptions);
|
||||
await webauthnService.finishLogin(authResponse);
|
||||
}
|
||||
|
||||
await oidService.authorize(client!.id, scope, nonce).then(async (code) => {
|
||||
onSuccess(code);
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError && e.response?.status === 403) {
|
||||
authorizationRequired = true;
|
||||
} else {
|
||||
errorMessage = getWebauthnErrorMessage(e);
|
||||
}
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function authorizeNewClient() {
|
||||
isLoading = true;
|
||||
try {
|
||||
await oidService.authorizeNewClient(client!.id, scope, nonce).then(async (code) => {
|
||||
onSuccess(code);
|
||||
});
|
||||
} catch (e) {
|
||||
errorMessage = getWebauthnErrorMessage(e);
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSuccess(code: string) {
|
||||
success = true;
|
||||
setTimeout(() => {
|
||||
window.location.href = `${client!.callbackURL}?code=${code}&state=${state}`;
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign in to {client.name}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if client == null}
|
||||
<p>Client not found</p>
|
||||
{:else}
|
||||
<SignInWrapper>
|
||||
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Sign in to {client.name}</h1>
|
||||
{#if !authorizationRequired}
|
||||
<p class="text-muted-foreground mb-10 mt-2">
|
||||
{#if errorMessage}
|
||||
{errorMessage}. Please try again.
|
||||
{:else}
|
||||
Do you want to sign in to <b>{client.name}</b> with your
|
||||
<b>{$applicationConfigurationStore.appName}</b> account?
|
||||
{/if}
|
||||
</p>
|
||||
{:else}
|
||||
<div transition:slide={{ duration: 300 }}>
|
||||
<Card.Root class="mb-10 mt-6">
|
||||
<Card.Header class="pb-5">
|
||||
<p class="text-muted-foreground text-start">
|
||||
<b>{client.name}</b> wants to access the following information:
|
||||
</p>
|
||||
</Card.Header>
|
||||
<Card.Content data-testid="scopes">
|
||||
<div class="flex flex-col gap-3">
|
||||
{#if scope!.includes('email')}
|
||||
<ScopeItem icon={LucideMail} name="Email" description="View your email address" />
|
||||
{/if}
|
||||
{#if scope!.includes('profile')}
|
||||
<ScopeItem
|
||||
icon={LucideUser}
|
||||
name="Profile"
|
||||
description="View your profile information"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex justify-center gap-2">
|
||||
<Button onclick={() => history.back()} class="w-full" variant="secondary">Cancel</Button>
|
||||
{#if !errorMessage}
|
||||
<Button
|
||||
class="w-full"
|
||||
{isLoading}
|
||||
on:click={authorizationRequired ? authorizeNewClient : authorize}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="w-full" on:click={() => (errorMessage = null)}>Try again</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</SignInWrapper>
|
||||
{/if}
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import Logo from '$lib/components/logo.svelte';
|
||||
import CheckmarkAnimated from '$lib/icons/checkmark-animated.svelte';
|
||||
import ConnectArrow from '$lib/icons/connect-arrow.svelte';
|
||||
import CrossAnimated from '$lib/icons/cross-animated.svelte';
|
||||
import type { OidcClient } from '$lib/types/oidc.type';
|
||||
|
||||
const {
|
||||
success,
|
||||
error,
|
||||
client
|
||||
}: {
|
||||
success: boolean;
|
||||
error: boolean;
|
||||
client: OidcClient;
|
||||
} = $props();
|
||||
|
||||
let animationDone = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (success || error) {
|
||||
setTimeout(() => {
|
||||
animationDone = true;
|
||||
}, 500);
|
||||
} else {
|
||||
animationDone = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center gap-3">
|
||||
<div
|
||||
class=" bg-muted rounded-2xl p-3 transition-transform duration-500 ease-in {success || error
|
||||
? 'translate-x-[108px]'
|
||||
: ''}"
|
||||
>
|
||||
<Logo class="h-10 w-10" />
|
||||
</div>
|
||||
|
||||
<ConnectArrow
|
||||
class="arrow-fade-out h-w-32 w-32 {success || error ? 'opacity-0' : 'opacity-100'}"
|
||||
/>
|
||||
<div
|
||||
class="rounded-2xl p-3 [transition:transform_500ms_ease-in,background-color_200ms] {success ||
|
||||
error
|
||||
? '-translate-x-[108px]'
|
||||
: ''} {animationDone ? (success ? 'bg-green-200' : 'bg-red-200') : 'bg-muted'}"
|
||||
>
|
||||
{#if animationDone && success}
|
||||
<div class="flex h-10 w-10 items-center justify-center">
|
||||
<CheckmarkAnimated class="h-7 w-7" />
|
||||
</div>
|
||||
{:else if animationDone && error}
|
||||
<div class="flex h-10 w-10 items-center justify-center">
|
||||
<CrossAnimated class="h-5 w-5" />
|
||||
</div>
|
||||
{:else if client.hasLogo}
|
||||
<img
|
||||
class="h-10 w-10"
|
||||
src="/api/oidc/clients/{client.id}/logo"
|
||||
draggable={false}
|
||||
alt="Client Logo"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-10 w-10 items-center justify-center text-3xl font-bold">
|
||||
{client.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
13
frontend/src/routes/authorize/components/scope-item.svelte
Normal file
13
frontend/src/routes/authorize/components/scope-item.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
export let icon: ConstructorOfATypedSvelteComponent;
|
||||
export let name: string;
|
||||
export let description: string;
|
||||
</script>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="mr-5 rounded-lg bg-muted p-2"><svelte:component this={icon} /></div>
|
||||
<div class="text-start">
|
||||
<h3 class="font-semibold">{name}</h3>
|
||||
<p class="text-sm text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user