From 58872bada670a729fdaa7e49f506a0a0734fae5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Kr=C4=8Dek?= Date: Thu, 19 Jun 2025 20:25:36 +0200 Subject: [PATCH] Working --- package-lock.json | 54 +++++++++++++------ package.json | 3 +- src/app.css | 5 +- src/app.d.ts | 21 +++++--- src/hooks.server.ts | 38 +++++++++++-- src/routes/+layout.server.ts | 5 +- src/routes/+layout.svelte | 19 +++++-- src/routes/+layout.ts | 21 ++++++-- src/routes/auth/+layout.svelte | 11 ++++ src/routes/auth/+page.server.ts | 32 +++++++++++ src/routes/auth/+page.svelte | 12 +++++ src/routes/auth/confirm/+server.ts | 31 +++++++++++ src/routes/auth/error/+page.svelte | 1 + src/routes/private/+layout.server.ts | 5 ++ src/routes/private/home/+page.svelte | 6 +++ src/routes/private/scanner/+page.svelte | 10 +++- src/routes/private/scanner/QRScanner.svelte | 3 +- .../private/scanner/TicketDisplay.svelte | 8 ++- 18 files changed, 237 insertions(+), 48 deletions(-) create mode 100644 src/routes/auth/+layout.svelte create mode 100644 src/routes/auth/+page.server.ts create mode 100644 src/routes/auth/+page.svelte create mode 100644 src/routes/auth/confirm/+server.ts create mode 100644 src/routes/auth/error/+page.svelte create mode 100644 src/routes/private/+layout.server.ts create mode 100644 src/routes/private/home/+page.svelte diff --git a/package-lock.json b/package-lock.json index 1c27001..7989488 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,8 @@ "name": "esn-code-scanner", "version": "0.0.1", "dependencies": { - "@supabase/supabase-js": "^2.49.4", + "@supabase/ssr": "^0.6.1", + "@supabase/supabase-js": "^2.50.0", "@sveltejs/adapter-node": "^5.2.12" }, "devDependencies": { @@ -869,9 +870,9 @@ ] }, "node_modules/@supabase/auth-js": { - "version": "2.69.1", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz", - "integrity": "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==", + "version": "2.70.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.70.0.tgz", + "integrity": "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==", "license": "MIT", "dependencies": { "@supabase/node-fetch": "^2.6.14" @@ -908,15 +909,36 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", - "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "version": "2.11.10", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.10.tgz", + "integrity": "sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.14", - "@types/phoenix": "^1.5.4", - "@types/ws": "^8.5.10", - "ws": "^8.18.0" + "@supabase/node-fetch": "^2.6.13", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.6.1.tgz", + "integrity": "sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.43.4" + } + }, + "node_modules/@supabase/ssr/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/@supabase/storage-js": { @@ -929,16 +951,16 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.49.4", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.4.tgz", - "integrity": "sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==", + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.50.0.tgz", + "integrity": "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.69.1", + "@supabase/auth-js": "2.70.0", "@supabase/functions-js": "2.4.4", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.19.4", - "@supabase/realtime-js": "2.11.2", + "@supabase/realtime-js": "2.11.10", "@supabase/storage-js": "2.7.1" } }, diff --git a/package.json b/package.json index 45eb416..4282d0b 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "vite": "^6.2.6" }, "dependencies": { - "@supabase/supabase-js": "^2.49.4", + "@supabase/ssr": "^0.6.1", + "@supabase/supabase-js": "^2.50.0", "@sveltejs/adapter-node": "^5.2.12" } } diff --git a/src/app.css b/src/app.css index 1c4d2a8..97f54b0 100644 --- a/src/app.css +++ b/src/app.css @@ -1,2 +1,5 @@ @import 'tailwindcss'; -@plugin '@tailwindcss/typography'; + +body { + font-family: "Roboto", sans-serif; +} \ No newline at end of file diff --git a/src/app.d.ts b/src/app.d.ts index bce918f..9a0cf33 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,16 +1,21 @@ -// src/app.d.ts -import { SupabaseClient, Session } from '@supabase/supabase-js' +import type { Session, SupabaseClient, User } from '@supabase/supabase-js' +import type { Database } from './database.types.ts' // import generated types + declare global { namespace App { + // interface Error {} interface Locals { - supabase: SupabaseClient - safeGetSession(): Promise<{ session: Session | null; user: User | null }> - } - interface PageData { + supabase: SupabaseClient + safeGetSession: () => Promise<{ session: Session | null; user: User | null }> session: Session | null user: User | null } - // interface Error {} + interface PageData { + session: Session | null + } + // interface PageState {} // interface Platform {} } -} \ No newline at end of file +} + +export {} \ No newline at end of file diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 9a89766..079d393 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,9 +1,15 @@ -// src/hooks.server.ts -import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' import { createServerClient } from '@supabase/ssr' -import type { Handle } from '@sveltejs/kit' +import { type Handle, redirect } from '@sveltejs/kit' +import { sequence } from '@sveltejs/kit/hooks' -export const handle: Handle = async ({ event, resolve }) => { +import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' + +const supabase: Handle = async ({ event, resolve }) => { + /** + * Creates a Supabase client specific to this server request. + * + * The Supabase client gets the Auth token from the request cookies. + */ event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { cookies: { getAll: () => event.cookies.getAll(), @@ -47,7 +53,29 @@ export const handle: Handle = async ({ event, resolve }) => { return resolve(event, { filterSerializedResponseHeaders(name) { + /** + * Supabase libraries use the `content-range` and `x-supabase-api-version` + * headers, so we need to tell SvelteKit to pass it through. + */ return name === 'content-range' || name === 'x-supabase-api-version' }, }) -} \ No newline at end of file +} + +const authGuard: Handle = async ({ event, resolve }) => { + const { session, user } = await event.locals.safeGetSession() + event.locals.session = session + event.locals.user = user + + if (!event.locals.session && event.url.pathname.startsWith('/private')) { + redirect(303, '/auth') + } + + if (event.locals.session && event.url.pathname === '/auth') { + redirect(303, '/private/home') + } + + return resolve(event) +} + +export const handle: Handle = sequence(supabase, authGuard) \ No newline at end of file diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 55e62fe..7543dfa 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,10 +1,9 @@ -// src/routes/+layout.server.ts import type { LayoutServerLoad } from './$types' + export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => { - const { session, user } = await safeGetSession() + const { session } = await safeGetSession() return { session, - user, cookies: cookies.getAll(), } } \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b8a1c4d..f2ee8ac 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,8 +1,19 @@ - -{@render children()} +{@render children()} \ No newline at end of file diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 768b716..7a40abc 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1,14 +1,19 @@ +import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr' import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' import type { LayoutLoad } from './$types' -import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr' -export const load: LayoutLoad = async ({ fetch, data, depends }) => { +export const load: LayoutLoad = async ({ data, depends, fetch }) => { + /** + * Declare a dependency so the layout can be invalidated, for example, on + * session refresh. + */ depends('supabase:auth') + const supabase = isBrowser() ? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { global: { fetch, - } + }, }) : createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { global: { @@ -16,10 +21,11 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => { }, cookies: { getAll() { - return data.cookies ?? [] + return data.cookies }, }, }) + /** * It's fine to use `getSession` here, because on the client, `getSession` is * safe, and on the server, it reads `session` from the `LayoutData`, which @@ -28,5 +34,10 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => { const { data: { session }, } = await supabase.auth.getSession() - return { supabase, session } + + const { + data: { user }, + } = await supabase.auth.getUser() + + return { session, supabase, user } } \ No newline at end of file diff --git a/src/routes/auth/+layout.svelte b/src/routes/auth/+layout.svelte new file mode 100644 index 0000000..042ea05 --- /dev/null +++ b/src/routes/auth/+layout.svelte @@ -0,0 +1,11 @@ + + +
+ +
+ +{@render children()} \ No newline at end of file diff --git a/src/routes/auth/+page.server.ts b/src/routes/auth/+page.server.ts new file mode 100644 index 0000000..57908d2 --- /dev/null +++ b/src/routes/auth/+page.server.ts @@ -0,0 +1,32 @@ +import { redirect } from '@sveltejs/kit' + +import type { Actions } from './$types' + +export const actions: Actions = { + signup: async ({ request, locals: { supabase } }) => { + const formData = await request.formData() + const email = formData.get('email') as string + const password = formData.get('password') as string + + const { error } = await supabase.auth.signUp({ email, password }) + if (error) { + console.error(error) + redirect(303, '/auth/error') + } else { + redirect(303, '/') + } + }, + login: async ({ request, locals: { supabase } }) => { + const formData = await request.formData() + const email = formData.get('email') as string + const password = formData.get('password') as string + + const { error } = await supabase.auth.signInWithPassword({ email, password }) + if (error) { + console.error(error) + redirect(303, '/auth/error') + } else { + redirect(303, '/private/home') + } + }, +} \ No newline at end of file diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte new file mode 100644 index 0000000..cded2fe --- /dev/null +++ b/src/routes/auth/+page.svelte @@ -0,0 +1,12 @@ +
+ + + + +
\ No newline at end of file diff --git a/src/routes/auth/confirm/+server.ts b/src/routes/auth/confirm/+server.ts new file mode 100644 index 0000000..4fd0574 --- /dev/null +++ b/src/routes/auth/confirm/+server.ts @@ -0,0 +1,31 @@ +import type { EmailOtpType } from '@supabase/supabase-js' +import { redirect } from '@sveltejs/kit' + +import type { RequestHandler } from './$types' + +export const GET: RequestHandler = async ({ url, locals: { supabase } }) => { + const token_hash = url.searchParams.get('token_hash') + const type = url.searchParams.get('type') as EmailOtpType | null + const next = url.searchParams.get('next') ?? '/' + + /** + * Clean up the redirect URL by deleting the Auth flow parameters. + * + * `next` is preserved for now, because it's needed in the error case. + */ + const redirectTo = new URL(url) + redirectTo.pathname = next + redirectTo.searchParams.delete('token_hash') + redirectTo.searchParams.delete('type') + + if (token_hash && type) { + const { error } = await supabase.auth.verifyOtp({ type, token_hash }) + if (!error) { + redirectTo.searchParams.delete('next') + redirect(303, redirectTo) + } + } + + redirectTo.pathname = '/auth/error' + redirect(303, redirectTo) +} \ No newline at end of file diff --git a/src/routes/auth/error/+page.svelte b/src/routes/auth/error/+page.svelte new file mode 100644 index 0000000..0176f4c --- /dev/null +++ b/src/routes/auth/error/+page.svelte @@ -0,0 +1 @@ +

Login error

\ No newline at end of file diff --git a/src/routes/private/+layout.server.ts b/src/routes/private/+layout.server.ts new file mode 100644 index 0000000..803cddb --- /dev/null +++ b/src/routes/private/+layout.server.ts @@ -0,0 +1,5 @@ +/** + * This file is necessary to ensure protection of all routes in the `private` + * directory. It makes the routes in this directory _dynamic_ routes, which + * send a server request, and thus trigger `hooks.server.ts`. + **/ \ No newline at end of file diff --git a/src/routes/private/home/+page.svelte b/src/routes/private/home/+page.svelte new file mode 100644 index 0000000..36a3c0b --- /dev/null +++ b/src/routes/private/home/+page.svelte @@ -0,0 +1,6 @@ + + + +heyy \ No newline at end of file diff --git a/src/routes/private/scanner/+page.svelte b/src/routes/private/scanner/+page.svelte index a3843f9..b03a903 100644 --- a/src/routes/private/scanner/+page.svelte +++ b/src/routes/private/scanner/+page.svelte @@ -27,6 +27,12 @@ }); + + {#if scan_state === ScanState.scan_successful} @@ -34,9 +40,9 @@ {/if} {#if scan_state === ScanState.scan_failed} -

Scan failed. Please try again.

+

Scan failed. Please try again.

{/if} {#if scan_state === ScanState.scanning} -

Fetching data...

+

Fetching data...

{/if} diff --git a/src/routes/private/scanner/QRScanner.svelte b/src/routes/private/scanner/QRScanner.svelte index e85b5fb..9bb65d4 100644 --- a/src/routes/private/scanner/QRScanner.svelte +++ b/src/routes/private/scanner/QRScanner.svelte @@ -4,8 +4,7 @@ Html5QrcodeScanner, type Html5QrcodeResult, Html5QrcodeScanType, - Html5QrcodeSupportedFormats, - Html5QrcodeScannerState, + Html5QrcodeSupportedFormats } from 'html5-qrcode'; let width: number = 300; diff --git a/src/routes/private/scanner/TicketDisplay.svelte b/src/routes/private/scanner/TicketDisplay.svelte index 6e76511..5d360b1 100644 --- a/src/routes/private/scanner/TicketDisplay.svelte +++ b/src/routes/private/scanner/TicketDisplay.svelte @@ -4,5 +4,11 @@ let { ticket_data }: { ticket_data: TicketData } = $props(); -

{ticket_data.name}

+

{ticket_data.name}

{ticket_data.surname}

+ + \ No newline at end of file