This commit is contained in:
Roman Krček
2025-06-19 20:25:36 +02:00
parent 9c94f9c717
commit 58872bada6
18 changed files with 237 additions and 48 deletions

54
package-lock.json generated
View File

@@ -8,7 +8,8 @@
"name": "esn-code-scanner", "name": "esn-code-scanner",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.49.4", "@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.50.0",
"@sveltejs/adapter-node": "^5.2.12" "@sveltejs/adapter-node": "^5.2.12"
}, },
"devDependencies": { "devDependencies": {
@@ -869,9 +870,9 @@
] ]
}, },
"node_modules/@supabase/auth-js": { "node_modules/@supabase/auth-js": {
"version": "2.69.1", "version": "2.70.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.70.0.tgz",
"integrity": "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==", "integrity": "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@supabase/node-fetch": "^2.6.14" "@supabase/node-fetch": "^2.6.14"
@@ -908,15 +909,36 @@
} }
}, },
"node_modules/@supabase/realtime-js": { "node_modules/@supabase/realtime-js": {
"version": "2.11.2", "version": "2.11.10",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.10.tgz",
"integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", "integrity": "sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@supabase/node-fetch": "^2.6.14", "@supabase/node-fetch": "^2.6.13",
"@types/phoenix": "^1.5.4", "@types/phoenix": "^1.6.6",
"@types/ws": "^8.5.10", "@types/ws": "^8.18.1",
"ws": "^8.18.0" "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": { "node_modules/@supabase/storage-js": {
@@ -929,16 +951,16 @@
} }
}, },
"node_modules/@supabase/supabase-js": { "node_modules/@supabase/supabase-js": {
"version": "2.49.4", "version": "2.50.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.4.tgz", "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.50.0.tgz",
"integrity": "sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==", "integrity": "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@supabase/auth-js": "2.69.1", "@supabase/auth-js": "2.70.0",
"@supabase/functions-js": "2.4.4", "@supabase/functions-js": "2.4.4",
"@supabase/node-fetch": "2.6.15", "@supabase/node-fetch": "2.6.15",
"@supabase/postgrest-js": "1.19.4", "@supabase/postgrest-js": "1.19.4",
"@supabase/realtime-js": "2.11.2", "@supabase/realtime-js": "2.11.10",
"@supabase/storage-js": "2.7.1" "@supabase/storage-js": "2.7.1"
} }
}, },

View File

@@ -29,7 +29,8 @@
"vite": "^6.2.6" "vite": "^6.2.6"
}, },
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.49.4", "@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.50.0",
"@sveltejs/adapter-node": "^5.2.12" "@sveltejs/adapter-node": "^5.2.12"
} }
} }

View File

@@ -1,2 +1,5 @@
@import 'tailwindcss'; @import 'tailwindcss';
@plugin '@tailwindcss/typography';
body {
font-family: "Roboto", sans-serif;
}

21
src/app.d.ts vendored
View File

@@ -1,16 +1,21 @@
// src/app.d.ts import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
import { SupabaseClient, Session } from '@supabase/supabase-js' import type { Database } from './database.types.ts' // import generated types
declare global { declare global {
namespace App { namespace App {
// interface Error {}
interface Locals { interface Locals {
supabase: SupabaseClient supabase: SupabaseClient<Database>
safeGetSession(): Promise<{ session: Session | null; user: User | null }> safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
}
interface PageData {
session: Session | null session: Session | null
user: User | null user: User | null
} }
// interface Error {} interface PageData {
session: Session | null
}
// interface PageState {}
// interface Platform {} // interface Platform {}
} }
} }
export {}

View File

@@ -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 { 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, { event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
cookies: { cookies: {
getAll: () => event.cookies.getAll(), getAll: () => event.cookies.getAll(),
@@ -47,7 +53,29 @@ export const handle: Handle = async ({ event, resolve }) => {
return resolve(event, { return resolve(event, {
filterSerializedResponseHeaders(name) { 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' return name === 'content-range' || name === 'x-supabase-api-version'
}, },
}) })
} }
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)

View File

@@ -1,10 +1,9 @@
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types' import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => { export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
const { session, user } = await safeGetSession() const { session } = await safeGetSession()
return { return {
session, session,
user,
cookies: cookies.getAll(), cookies: cookies.getAll(),
} }
} }

View File

@@ -1,8 +1,19 @@
<script lang="ts"> <script>
import '../app.css'; import { invalidate } from '$app/navigation'
import { onMount } from 'svelte'
let { children } = $props(); let { data, children } = $props()
let { session, supabase } = $derived(data)
onMount(() => {
const { data } = supabase.auth.onAuthStateChange((_, newSession) => {
if (newSession?.expires_at !== session?.expires_at) {
invalidate('supabase:auth')
}
})
return () => data.subscription.unsubscribe()
})
</script> </script>
{@render children()} {@render children()}

View File

@@ -1,14 +1,19 @@
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
import type { LayoutLoad } from './$types' 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') depends('supabase:auth')
const supabase = isBrowser() const supabase = isBrowser()
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { ? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: { global: {
fetch, fetch,
} },
}) })
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { : createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: { global: {
@@ -16,10 +21,11 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
}, },
cookies: { cookies: {
getAll() { getAll() {
return data.cookies ?? [] return data.cookies
}, },
}, },
}) })
/** /**
* It's fine to use `getSession` here, because on the client, `getSession` is * 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 * 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 { const {
data: { session }, data: { session },
} = await supabase.auth.getSession() } = await supabase.auth.getSession()
return { supabase, session }
const {
data: { user },
} = await supabase.auth.getUser()
return { session, supabase, user }
} }

View File

@@ -0,0 +1,11 @@
<script>
let { children } = $props()
</script>
<header>
<nav>
<a href="/">Home</a>
</nav>
</header>
{@render children()}

View File

@@ -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')
}
},
}

View File

@@ -0,0 +1,12 @@
<form method="POST" action="?/login">
<label>
Email
<input name="email" type="email" />
</label>
<label>
Password
<input name="password" type="password" />
</label>
<button>Login</button>
<button formaction="?/signup">Sign up</button>
</form>

View File

@@ -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)
}

View File

@@ -0,0 +1 @@
<p>Login error</p>

View File

@@ -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`.
**/

View File

@@ -0,0 +1,6 @@
<script lang="ts">
</script>
heyy

View File

@@ -27,6 +27,12 @@
}); });
</script> </script>
<style>
.robo {
font-family: "Roboto", sans-serif;
}
</style>
<QRScanner bind:message={scanned_id} /> <QRScanner bind:message={scanned_id} />
{#if scan_state === ScanState.scan_successful} {#if scan_state === ScanState.scan_successful}
@@ -34,9 +40,9 @@
{/if} {/if}
{#if scan_state === ScanState.scan_failed} {#if scan_state === ScanState.scan_failed}
<p>Scan failed. Please try again.</p> <p class="robo">Scan failed. Please try again.</p>
{/if} {/if}
{#if scan_state === ScanState.scanning} {#if scan_state === ScanState.scanning}
<p>Fetching data...</p> <p class="robo">Fetching data...</p>
{/if} {/if}

View File

@@ -4,8 +4,7 @@
Html5QrcodeScanner, Html5QrcodeScanner,
type Html5QrcodeResult, type Html5QrcodeResult,
Html5QrcodeScanType, Html5QrcodeScanType,
Html5QrcodeSupportedFormats, Html5QrcodeSupportedFormats
Html5QrcodeScannerState,
} from 'html5-qrcode'; } from 'html5-qrcode';
let width: number = 300; let width: number = 300;

View File

@@ -4,5 +4,11 @@
let { ticket_data }: { ticket_data: TicketData } = $props(); let { ticket_data }: { ticket_data: TicketData } = $props();
</script> </script>
<p>{ticket_data.name}</p> <p class="robo">{ticket_data.name}</p>
<p>{ticket_data.surname}</p> <p>{ticket_data.surname}</p>
<style>
.robo {
font-family: var(--font-display);
}
</style>