diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json
index 11d17aa..798ae78 100644
--- a/project.inlang/messages/de.json
+++ b/project.inlang/messages/de.json
@@ -42,13 +42,28 @@
"passphraseRequirements": "Mindestens 30 Zeichen.",
"errors": {
"email": "Ungültiges Format",
- "passphrase": "Muss mindestens 30 Zeichen lang sein"
- }
+ "passphrase": "Muss mindestens 30 Zeichen lang sein",
+ "noPassAtAll": "Either passphrase or passkey is required",
+ "bothPassSet": "Nur Passphrase oder Passkey darf gesetzt sein"
+ },
+ "passkey": "Passkey"
},
"login": {
"or": "Oder",
"usePasskey": "Passkey verwenden",
- "usePassphrase": "Passphrase verwenden"
+ "usePassphrase": "Passphrase verwenden",
+ "title": "Anmelden",
+ "description_passphrase": "Gib Deine E-Mail-Adresse und Deinen Passphrase ein, um Dich anzumelden.",
+ "action": "Anmelden",
+ "error": "Anmeldung fehlgeschlagen. Bitte erneut versuchen",
+ "passkey": {
+ "initial": "Warte auf Absenden des Formulars",
+ "success": "Passkey bereit",
+ "error": "Passkey konnte nicht abgerufen werden",
+ "user": "Warte während Passkey erstellt wird",
+ "loading": "Teste Passkey",
+ "retry": "Erneut versuchen"
+ }
},
"confirm": {
"success": {
@@ -68,5 +83,11 @@
"action": "E-Mail erneut senden",
"success": "E-Mail gesendet"
}
+ },
+ "logout": {
+ "title": "Erfolgreich abgemeldet",
+ "description": "Du wurdest abgemeldet.",
+ "success": "Erfolgreich abgemeldet",
+ "action": "Zur Anmeldung"
}
}
diff --git a/project.inlang/messages/en.json b/project.inlang/messages/en.json
index fc907d7..5f3adcf 100644
--- a/project.inlang/messages/en.json
+++ b/project.inlang/messages/en.json
@@ -43,14 +43,35 @@
"passphraseRequirements": "At least 30 characters.",
"errors": {
"email": "Invalid format",
- "passphrase": "Must be at least 30 characters long"
- }
+ "passphrase": "Must be at least 30 characters long",
+ "noPassAtAll": "Either passphrase or passkey is required",
+ "bothPassSet": "Only one of passphrase or passkey should be provided"
+ },
+ "noPassAtAll": "Either passphrase or passkey is required",
+ "passkey": "Passkey"
},
"login": {
"or": "Or",
"usePasskey": "use passkey",
"usePassphrase": "use passphrase",
- "orUse": "Or {button}"
+ "orUse": "Or {button}",
+ "title": "Login",
+ "description_passphrase": "Enter your email and passphrase below to access your account.",
+ "action": "Login",
+ "error": "Login failed. Please retry",
+ "passphrase": {
+ "initial": "Requesting passkey",
+ "success": "Passkey saved",
+ "error": "Passkey could not be retrieved"
+ },
+ "passkey": {
+ "initial": "Waiting for form to be sent",
+ "success": "Added Passkey successfully",
+ "error": "Passkey could not be added",
+ "user": "Waiting for passkey to be created",
+ "loading": "Completing challenge",
+ "retry": "Retry"
+ }
},
"confirm": {
"success": {
@@ -70,5 +91,11 @@
"action": "Resend E-Mail",
"success": "E-Mail sent"
}
+ },
+ "logout": {
+ "title": "Logout complete",
+ "description": "You are logged-out.",
+ "success": "Logout successful",
+ "action": "Go to Login"
}
}
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index e98e909..8d234ca 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -5,7 +5,8 @@ import { corsHandle } from "./server-hooks/corsHandle";
import { loggingHandle } from "./server-hooks/loggingHandle";
import { rateLimitHandle } from "./server-hooks/rateLimitHandle";
import { secHeaderHandle } from "./server-hooks/secHeaderHandle";
-import { authHandle } from "./server-hooks/authHandle";
+import { apiAuthHandle } from "./server-hooks/apiAuthHandle";
+import { authGuard } from "./server-hooks/authGuard";
import { i18nHandle } from "./server-hooks/i18nHandle";
import { building } from "$app/environment";
@@ -40,5 +41,6 @@ export const handle = sequence(
rateLimitHandle,
corsHandle,
secHeaderHandle,
- authHandle
+ apiAuthHandle,
+ authGuard
);
diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte
index 19c6dae..29c91d8 100644
--- a/src/lib/components/ui/input/input.svelte
+++ b/src/lib/components/ui/input/input.svelte
@@ -24,7 +24,7 @@
bind:this={ref}
data-slot="input"
class={cn(
- "selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+ "selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
@@ -39,7 +39,7 @@
bind:this={ref}
data-slot="input"
class={cn(
- "border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+ "border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
diff --git a/src/lib/components/ui/passkey/index.ts b/src/lib/components/ui/passkey/index.ts
new file mode 100644
index 0000000..d53af12
--- /dev/null
+++ b/src/lib/components/ui/passkey/index.ts
@@ -0,0 +1,5 @@
+import State from "./state.svelte";
+
+const Passkey = { State };
+
+export { Passkey };
diff --git a/src/lib/components/ui/passkey/state.svelte b/src/lib/components/ui/passkey/state.svelte
new file mode 100644
index 0000000..3191b33
--- /dev/null
+++ b/src/lib/components/ui/passkey/state.svelte
@@ -0,0 +1,45 @@
+
+
+
+ {#if state === "initial"}
+
+ {m["login.passkey.initial"]()}
+ {:else if state === "user"}
+
+ {m["login.passkey.user"]()}
+ {:else if state === "loading"}
+
+ {m["login.passkey.loading"]()}
+ {:else if state === "success"}
+
+ {m["login.passkey.success"]()}
+ {:else if state === "error"}
+
+ {m["login.passkey.error"]()}
+ {/if}
+
diff --git a/src/lib/const/routes.ts b/src/lib/const/routes.ts
index 1c6551a..2bbbffe 100644
--- a/src/lib/const/routes.ts
+++ b/src/lib/const/routes.ts
@@ -5,5 +5,9 @@ export const ROUTES = {
CHECK_EMAIL: "/setup/check-email"
},
RESEND_CONFIRMATION: "/confirm/resend",
- LOGIN: "/login"
+ LOGIN: "/login",
+ LOGOUT: "/logout",
+ DASHBOARD: {
+ MAIN: "/dashboard"
+ }
};
diff --git a/src/lib/server/auth/webauthn-service.ts b/src/lib/server/auth/webauthn-service.ts
index b0ed1e6..27cf439 100644
--- a/src/lib/server/auth/webauthn-service.ts
+++ b/src/lib/server/auth/webauthn-service.ts
@@ -189,7 +189,7 @@ export class WebAuthnService {
* This should be stored in the session and used for verification
*/
static generateChallenge(): string {
- return randomBytes(32).toString("base64url");
+ return randomBytes(32).toString("base64");
}
/**
diff --git a/src/lib/utils/passkey.ts b/src/lib/utils/passkey.ts
new file mode 100644
index 0000000..4a45267
--- /dev/null
+++ b/src/lib/utils/passkey.ts
@@ -0,0 +1,69 @@
+export const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
+ const bytes = new Uint8Array(buffer);
+ let binary = "";
+
+ // Simple loop, no fancy operations
+ for (let i = 0; i < bytes.length; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+
+ return window.btoa(binary);
+};
+
+function base64ToArrayBuffer(base64: string) {
+ const binaryString = atob(base64);
+ const bytes = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ return bytes.buffer;
+}
+
+export const fetchChallenge = async (email: string) => {
+ const resp = await fetch("/api/auth/challenge", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({ email })
+ });
+
+ try {
+ const data = await resp.json();
+ return {
+ id: data.rpId,
+ challenge: data.challenge
+ };
+ } catch {
+ return null;
+ }
+};
+
+export const generatePasskey = async ({
+ id,
+ challenge,
+ email
+}: {
+ id: string;
+ challenge: string;
+ email: string;
+}): Promise<{ response: AuthenticatorAttestationResponse } | null> => {
+ const publicKey: PublicKeyCredentialCreationOptions = {
+ challenge: base64ToArrayBuffer(challenge),
+ rp: {
+ id,
+ name: "Open Reception"
+ },
+ user: {
+ id: new Uint8Array(16),
+ name: email,
+ displayName: email
+ },
+ pubKeyCredParams: [
+ { alg: -7, type: "public-key" } // ES256
+ ]
+ };
+ return (await navigator.credentials.create({ publicKey })) as {
+ response: AuthenticatorAttestationResponse;
+ } | null;
+};
diff --git a/src/routes/(pages)/confirm/[token]/+page.server.ts b/src/routes/(pages)/confirm/[token]/+page.server.ts
index f14eb9e..d1718f9 100644
--- a/src/routes/(pages)/confirm/[token]/+page.server.ts
+++ b/src/routes/(pages)/confirm/[token]/+page.server.ts
@@ -16,7 +16,6 @@ export const load: PageServerLoad = async (event) => {
const success = resp.status < 400;
try {
const body = await resp.json();
- // TODO: Check if this is the first account
const isSetup = body.isSetup ?? false;
return { success, isSetup };
} catch (error) {
diff --git a/src/routes/(pages)/dashboard/+page.svelte b/src/routes/(pages)/dashboard/+page.svelte
new file mode 100644
index 0000000..f29a407
--- /dev/null
+++ b/src/routes/(pages)/dashboard/+page.svelte
@@ -0,0 +1,2 @@
+Hello from Dashboard
+Logout
diff --git a/src/routes/(pages)/login/+page.server.ts b/src/routes/(pages)/login/+page.server.ts
new file mode 100644
index 0000000..788ca12
--- /dev/null
+++ b/src/routes/(pages)/login/+page.server.ts
@@ -0,0 +1,39 @@
+import { fail } from "@sveltejs/kit";
+import { superValidate } from "sveltekit-superforms";
+import { zod } from "sveltekit-superforms/adapters";
+import type { Actions, PageServerLoad } from "./$types";
+import { formSchema } from "./schema";
+
+export const load: PageServerLoad = async () => {
+ return {
+ form: await superValidate(zod(formSchema))
+ };
+};
+
+export const actions: Actions = {
+ default: async (event) => {
+ const form = await superValidate(event, zod(formSchema));
+
+ if (!form.valid) {
+ return fail(400, {
+ form
+ });
+ }
+
+ const resp = await event.fetch("/api/auth/login", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({ email: form.data.email, passphrase: form.data.passphrase })
+ });
+
+ if (resp.status < 400) {
+ return { form };
+ } else {
+ return fail(400, {
+ form
+ });
+ }
+ }
+};
diff --git a/src/routes/(pages)/login/+page.svelte b/src/routes/(pages)/login/+page.svelte
index 76f6852..24de273 100644
--- a/src/routes/(pages)/login/+page.svelte
+++ b/src/routes/(pages)/login/+page.svelte
@@ -1 +1,53 @@
-Hello from login
+
+
+
+ {m["login.title"]()} - OpenReception
+
+
+
+
+
+
+ {m["login.title"]()}
+
+
+ {m["login.description_passphrase"]()}
+
+
+
+
+
+
+
+ {m["login.action"]()}
+
+
+
+
diff --git a/src/routes/(pages)/login/login-form.svelte b/src/routes/(pages)/login/login-form.svelte
new file mode 100644
index 0000000..64a9214
--- /dev/null
+++ b/src/routes/(pages)/login/login-form.svelte
@@ -0,0 +1,86 @@
+
+
+
+
+
+ {#snippet children({ props })}
+ {m["form.email"]()}
+
+ {/snippet}
+
+
+
+
+
+ {#snippet children({ props })}
+ {m["form.passphrase"]()}
+
+ {/snippet}
+
+
+
+ {m["login.or"]()}
+ {#if $isUsingPasskey}
+
+ {:else}
+
+ {/if}.
+
+
+
diff --git a/src/routes/(pages)/login/schema.ts b/src/routes/(pages)/login/schema.ts
new file mode 100644
index 0000000..53f6013
--- /dev/null
+++ b/src/routes/(pages)/login/schema.ts
@@ -0,0 +1,9 @@
+import { m } from "$i18n/messages";
+import { z } from "zod";
+
+export const formSchema = z.object({
+ email: z.string().email(m["form.errors.email"]()),
+ passphrase: z.string().min(30, m["form.errors.passphrase"]())
+});
+
+export type FormSchema = typeof formSchema;
diff --git a/src/routes/(pages)/logout/+page.server.ts b/src/routes/(pages)/logout/+page.server.ts
new file mode 100644
index 0000000..86ef326
--- /dev/null
+++ b/src/routes/(pages)/logout/+page.server.ts
@@ -0,0 +1,18 @@
+import type { PageServerLoad } from "./$types";
+
+export const load: PageServerLoad = async (event) => {
+ const success: Promise = event
+ .fetch("/api/auth/logout", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ }
+ })
+ .then(async (resp) => {
+ // TODO: Why is this failing?
+ console.log("resp", resp, await resp.json());
+ return resp.status < 400;
+ });
+
+ return { streaming: { success } };
+};
diff --git a/src/routes/(pages)/logout/+page.svelte b/src/routes/(pages)/logout/+page.svelte
new file mode 100644
index 0000000..27a0cdc
--- /dev/null
+++ b/src/routes/(pages)/logout/+page.svelte
@@ -0,0 +1,41 @@
+
+
+
+ {m["logout.title"]()} - OpenReception
+
+
+
+
+
+ {#await data.streaming.success}
+
+ {:then}
+
+ {/await}
+
+
+ {#await data.streaming.success}
+
+ {:then}
+
+ {/await}
+
+
+
diff --git a/src/routes/(pages)/setup/create-admin-account/+page.server.ts b/src/routes/(pages)/setup/create-admin-account/+page.server.ts
index 3cce61a..0cf17cb 100644
--- a/src/routes/(pages)/setup/create-admin-account/+page.server.ts
+++ b/src/routes/(pages)/setup/create-admin-account/+page.server.ts
@@ -25,19 +25,30 @@ export const actions: Actions = {
}
// Create admin account
- const admin = await UserService.createUser({
- name: "Admin",
- email: form.data.email,
- passphrase: form.data.passphrase,
- language: form.data.language
- }, event.url);
- const hasPasskey = false;
+ const admin = await UserService.createUser(
+ {
+ name: "Admin",
+ email: form.data.email,
+ passphrase: form.data.passphrase, // Will be undefined if passkey is used
+ language: form.data.language
+ },
+ event.url
+ );
+
+ if (form.data.passkey) {
+ await UserService.addPasskey(admin.id, {
+ id: form.data.passkey.id,
+ publicKey: form.data.passkey.publicKey,
+ counter: form.data.passkey.counter || 0,
+ deviceName: form.data.passkey.deviceName || "Unknown Device"
+ });
+ }
log.debug("Admin account created successfully", {
adminId: admin.id,
email: admin.email,
- authMethod: hasPasskey ? "passkey" : "passphrase"
- // passkeyId: hasPasskey ? body.passkey.id : undefined
+ authMethod: form.data.passkey ? "passkey" : "passphrase",
+ passkeyId: form.data.passkey ? form.data.passkey.id : undefined
});
return { form };
diff --git a/src/routes/(pages)/setup/create-admin-account/create-account-form.svelte b/src/routes/(pages)/setup/create-admin-account/create-account-form.svelte
index ef68386..39d2fdb 100644
--- a/src/routes/(pages)/setup/create-admin-account/create-account-form.svelte
+++ b/src/routes/(pages)/setup/create-admin-account/create-account-form.svelte
@@ -1,18 +1,23 @@
@@ -63,7 +132,7 @@
-
+
{#snippet children({ props })}
{m["form.passphrase"]()}
@@ -80,25 +149,29 @@
{m["form.passphraseRequirements"]()}
{m["login.or"]()}
- {#if $isUsingPasskey}
-
- {:else}
-
- {/if}.
+ .
+
+
+
+
+ {m["login.or"]()}
+ .
+
+
diff --git a/src/routes/(pages)/setup/create-admin-account/schema.ts b/src/routes/(pages)/setup/create-admin-account/schema.ts
index 0f7271d..187786f 100644
--- a/src/routes/(pages)/setup/create-admin-account/schema.ts
+++ b/src/routes/(pages)/setup/create-admin-account/schema.ts
@@ -1,10 +1,27 @@
import { m } from "$i18n/messages";
import { z } from "zod";
-export const formSchema = z.object({
- email: z.string().email(m["form.errors.email"]()),
- passphrase: z.string().min(30, m["form.errors.passphrase"]()),
- language: z.enum(["de", "en"])
-});
+export const formSchema = z
+ .object({
+ email: z.string().email(m["form.errors.email"]()),
+ passphrase: z.string().min(30, m["form.errors.passphrase"]()).optional(),
+ passkey: z
+ .object({
+ id: z.string().min(10),
+ publicKey: z.string().min(10),
+ counter: z.number().min(0),
+ deviceName: z.string().min(1).optional()
+ })
+ .optional(),
+ language: z.enum(["de", "en"])
+ })
+ .refine((data) => !!data.passphrase || !!data.passkey, {
+ message: m["form.errors.noPassAtAll"](),
+ path: ["passphrase"]
+ })
+ .refine((data) => !(data.passphrase && data.passkey), {
+ message: m["form.errors.bothPassSet"](),
+ path: ["passphrase"]
+ });
export type FormSchema = typeof formSchema;
diff --git a/src/server-hooks/authHandle.ts b/src/server-hooks/apiAuthHandle.ts
similarity index 84%
rename from src/server-hooks/authHandle.ts
rename to src/server-hooks/apiAuthHandle.ts
index b1f398c..23e4a09 100644
--- a/src/server-hooks/authHandle.ts
+++ b/src/server-hooks/apiAuthHandle.ts
@@ -2,12 +2,13 @@ import type { Handle } from "@sveltejs/kit";
import { SessionService } from "$lib/server/auth/session-service";
import { UniversalLogger } from "$lib/logger";
import { AuthorizationService } from "$lib/server/auth/authorization-service";
+import { getAccessToken } from "./utils/accessToken";
const logger = new UniversalLogger().setContext("AuthHandle");
-const ACCESS_TOKEN_COOKIE_NAME = "access_token";
const PROTECTED_PATHS = ["/api/admin", "/api/tenant-admin", "/api/tenants", "/api/auth/register"];
const PUBLIC_PATHS = [
+ "/",
"/api/auth/challenge",
"/api/auth/login",
"/api/auth/register",
@@ -33,8 +34,8 @@ const PROTECTED_AUTH_PATHS = [
"/api/auth/invite"
];
-export const authHandle: Handle = async ({ event, resolve }) => {
- const { url, request } = event;
+export const apiAuthHandle: Handle = async ({ event, resolve }) => {
+ const { url } = event;
const path = url.pathname;
if (!path.startsWith("/api")) {
@@ -63,22 +64,7 @@ export const authHandle: Handle = async ({ event, resolve }) => {
);
}
- let accessToken: string | null = null;
-
- // Get access token from cookie
- const accessTokenCookie = event.cookies.get(ACCESS_TOKEN_COOKIE_NAME);
- if (accessTokenCookie) {
- accessToken = accessTokenCookie;
- }
-
- // Fallback: check Authorization header
- if (!accessToken) {
- const authHeader = request.headers.get("authorization");
- if (authHeader?.startsWith("Bearer ")) {
- accessToken = authHeader.substring(7);
- }
- }
-
+ const accessToken: string | null = getAccessToken(event);
if (!accessToken) {
logger.warn(`Authentication required for ${path}`);
return new Response(JSON.stringify({ error: "Authentication required" }), {
@@ -104,8 +90,6 @@ export const authHandle: Handle = async ({ event, resolve }) => {
sessionId: sessionData.sessionId
};
- logger.debug(`Added user information for ${sessionData.user.id}`);
-
if (isGlobalAdminPath && !AuthorizationService.hasRole(sessionData.user, "GLOBAL_ADMIN")) {
return new Response(JSON.stringify({ error: "Authentication failed" }), {
status: 403,
diff --git a/src/server-hooks/authGuard.ts b/src/server-hooks/authGuard.ts
new file mode 100644
index 0000000..37bf404
--- /dev/null
+++ b/src/server-hooks/authGuard.ts
@@ -0,0 +1,42 @@
+import { redirect, type Handle } from "@sveltejs/kit";
+import { SessionService } from "$lib/server/auth/session-service";
+import { AuthorizationService } from "$lib/server/auth/authorization-service";
+import { getAccessToken } from "./utils/accessToken";
+import { ROUTES } from "$lib/const/routes";
+
+export const authGuard: Handle = async ({ event, resolve }) => {
+ const { url } = event;
+ const path = url.pathname;
+
+ // Do not handle api paths
+ if (path.startsWith("/api")) {
+ return resolve(event);
+ }
+
+ // Only guard protected routes
+ const isDashboardRoute = path.startsWith(ROUTES.DASHBOARD.MAIN);
+ if (isDashboardRoute) {
+ // Check for accessToken
+ const accessToken: string | null = getAccessToken(event);
+ if (!accessToken) redirect(302, ROUTES.LOGIN);
+
+ // Verify access token with database session check
+ const sessionData = await SessionService.validateTokenWithDB(accessToken);
+ if (!sessionData) redirect(302, ROUTES.LOGIN);
+
+ switch (true) {
+ case isDashboardRoute &&
+ AuthorizationService.hasAnyRole(sessionData.user, [
+ "GLOBAL_ADMIN",
+ "TENANT_ADMIN",
+ "STAFF"
+ ]):
+ return resolve(event);
+ default:
+ // Access not granted
+ redirect(302, ROUTES.LOGIN);
+ }
+ }
+
+ return resolve(event);
+};
diff --git a/src/server-hooks/utils/accessToken.ts b/src/server-hooks/utils/accessToken.ts
new file mode 100644
index 0000000..0fcb0d8
--- /dev/null
+++ b/src/server-hooks/utils/accessToken.ts
@@ -0,0 +1,24 @@
+import type { RequestEvent } from "@sveltejs/kit";
+
+const ACCESS_TOKEN_COOKIE_NAME = "access_token";
+export const getAccessToken = (
+ event: RequestEvent>, string | null>
+): string | null => {
+ let accessToken: string | null = null;
+
+ // Get access token from cookie
+ const accessTokenCookie = event.cookies.get(ACCESS_TOKEN_COOKIE_NAME);
+ if (accessTokenCookie) {
+ accessToken = accessTokenCookie;
+ }
+
+ // Fallback: check Authorization header
+ if (!accessToken) {
+ const authHeader = event.request.headers.get("authorization");
+ if (authHeader?.startsWith("Bearer ")) {
+ accessToken = authHeader.substring(7);
+ }
+ }
+
+ return accessToken;
+};