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

View File

@@ -1,2 +1,5 @@
@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 { 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<Database>
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 {}
}
}
}
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 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'
},
})
}
}
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'
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
const { session, user } = await safeGetSession()
const { session } = await safeGetSession()
return {
session,
user,
cookies: cookies.getAll(),
}
}

View File

@@ -1,8 +1,19 @@
<script lang="ts">
import '../app.css';
<script>
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>
{@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 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 }
}

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

View File

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

View File

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