diff --git a/src/hooks.client.ts b/src/hooks.client.ts new file mode 100644 index 0000000..3af4709 --- /dev/null +++ b/src/hooks.client.ts @@ -0,0 +1,7 @@ +import { refresh_auth } from "$lib/pocketbase"; +import type { ClientInit } from "@sveltejs/kit"; + +export const init: ClientInit = async () => { + // NOTE: If the auth token is invalidated, this will block the entire page + // await refresh_auth(); +}; diff --git a/src/lib/components/cards/DriverCard.svelte b/src/lib/components/cards/DriverCard.svelte index c935601..756284c 100644 --- a/src/lib/components/cards/DriverCard.svelte +++ b/src/lib/components/cards/DriverCard.svelte @@ -43,7 +43,7 @@ // Reactive state let required: boolean = $derived(!driver); - let disabled: boolean = $derived(!pbUser?.admin); + let disabled: boolean = $derived(!$pbUser?.admin); let firstname_input_value: string = $state(driver?.firstname ?? ""); let lastname_input_value: string = $state(driver?.lastname ?? ""); let code_input_value: string = $state(driver?.code ?? ""); diff --git a/src/lib/components/cards/RaceCard.svelte b/src/lib/components/cards/RaceCard.svelte index b571bde..edd8810 100644 --- a/src/lib/components/cards/RaceCard.svelte +++ b/src/lib/components/cards/RaceCard.svelte @@ -50,7 +50,7 @@ // Reactive state let required: boolean = $derived(!race); - let disabled: boolean = $derived(!pbUser?.admin); + let disabled: boolean = $derived(!$pbUser?.admin); let name_value: string = $state(race?.name ?? ""); let step_value: string = $state(race?.step.toString() ?? ""); let pxx_value: string = $state(race?.pxx.toString() ?? ""); diff --git a/src/lib/components/cards/RacePickCard.svelte b/src/lib/components/cards/RacePickCard.svelte index 73aa55f..e202c55 100644 --- a/src/lib/components/cards/RacePickCard.svelte +++ b/src/lib/components/cards/RacePickCard.svelte @@ -96,7 +96,7 @@ // Database actions const update_racepick = (create?: boolean): (() => Promise) => { const handler = async (): Promise => { - if (!pbUser?.id || pbUser.id === "") { + if (!$pbUser?.id || $pbUser.id === "") { toastStore.trigger(get_error_toast("Invalid user id!")); return; } @@ -114,7 +114,7 @@ } const racepick_data = { - user: pbUser.id, + user: $pbUser.id, race: data.currentrace.id, pxx: pxx_select_value, dnf: dnf_select_value, diff --git a/src/lib/components/cards/RaceResultCard.svelte b/src/lib/components/cards/RaceResultCard.svelte index f051cc4..9bd4d47 100644 --- a/src/lib/components/cards/RaceResultCard.svelte +++ b/src/lib/components/cards/RaceResultCard.svelte @@ -51,7 +51,7 @@ // Reactive state let required: boolean = $derived(!result); - let disabled: boolean = $derived(!pbUser?.admin); // TODO: Datelock (prevent entering future result) + let disabled: boolean = $derived(!$pbUser?.admin); // TODO: Datelock (prevent entering future result) let race_select_value: string = $state(result?.race ?? ""); let currentrace: Race | undefined = $derived( diff --git a/src/lib/components/cards/SeasonPickCard.svelte b/src/lib/components/cards/SeasonPickCard.svelte index afbe94a..7d7f586 100644 --- a/src/lib/components/cards/SeasonPickCard.svelte +++ b/src/lib/components/cards/SeasonPickCard.svelte @@ -184,7 +184,7 @@ // Database actions const update_seasonpick = (create?: boolean): (() => Promise) => { const handler = async (): Promise => { - if (!pbUser?.id || pbUser.id === "") { + if (!$pbUser?.id || $pbUser.id === "") { toastStore.trigger(get_error_toast("Invalid user id!")); return; } @@ -229,7 +229,7 @@ } const seasonpick_data = { - user: pbUser.id, + user: $pbUser.id, hottake: hottake_value, wdcwinner: wdc_value, wccwinner: wcc_value, diff --git a/src/lib/components/cards/SubstitutionCard.svelte b/src/lib/components/cards/SubstitutionCard.svelte index 419f282..2d01372 100644 --- a/src/lib/components/cards/SubstitutionCard.svelte +++ b/src/lib/components/cards/SubstitutionCard.svelte @@ -43,7 +43,7 @@ // Reactive state let required: boolean = $derived(!substitution); - let disabled: boolean = $derived(!pbUser?.admin); + let disabled: boolean = $derived(!$pbUser?.admin); let active_drivers: Driver[] = $derived((drivers ?? []).filter((d: Driver) => d.active)); let inactive_drivers: Driver[] = $derived((drivers ?? []).filter((d: Driver) => !d.active)); let substitute_value: string = $state(substitution?.substitute ?? ""); diff --git a/src/lib/components/cards/TeamCard.svelte b/src/lib/components/cards/TeamCard.svelte index 69469f6..6abbe3a 100644 --- a/src/lib/components/cards/TeamCard.svelte +++ b/src/lib/components/cards/TeamCard.svelte @@ -46,7 +46,7 @@ // Reactive state let required: boolean = $derived(!team); - let disabled: boolean = $derived(!pbUser?.admin); + let disabled: boolean = $derived(!$pbUser?.admin); let name_value: string = $state(team?.name ?? ""); let color_value: string = $state(team?.color ?? ""); let banner_value: FileList | undefined = $state(); diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index c82d96e..0fb951d 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -159,11 +159,12 @@ export const fetch_visibleracepicks = async ( export const fetch_currentracepick = async ( fetch: (_: any) => Promise, ): Promise => { - if (!pbUser) return undefined; + const user: User | undefined = get(pbUser); + if (!user) return undefined; const currentpickeduser: CurrentPickedUser = await pb .collection("currentpickedusers") - .getOne(pbUser.id, { fetch: fetch }); + .getOne(user.id, { fetch: fetch }); if (!currentpickeduser.picked) return undefined; @@ -204,11 +205,12 @@ export const fetch_hottakes = async (fetch: (_: any) => Promise): Prom export const fetch_currentseasonpick = async ( fetch: (_: any) => Promise, ): Promise => { - if (!pbUser) return undefined; + const user: User | undefined = get(pbUser); + if (!user) return undefined; const seasonpickeduser: CurrentPickedUser = await pb .collection("seasonpickedusers") - .getOne(pbUser.id, { fetch: fetch }); + .getOne(user.id, { fetch: fetch }); if (!seasonpickeduser.picked) return undefined; diff --git a/src/lib/pocketbase.ts b/src/lib/pocketbase.ts index dc64492..a5af668 100644 --- a/src/lib/pocketbase.ts +++ b/src/lib/pocketbase.ts @@ -2,9 +2,13 @@ import Pocketbase, { type RecordModel, type RecordSubscription } from "pocketbas import type { Graphic, User } from "$lib/schema"; import { env } from "$env/dynamic/public"; import { invalidate } from "$app/navigation"; +import { get, writable, type Writable } from "svelte/store"; export let pb = new Pocketbase(env.PUBLIC_PBURL || "http://192.168.86.50:8090"); -export let pbUser: User | undefined = undefined; + +// Keep this in a writable store, because this is basically a $state. +// We can't use $state in non-component files though. +export let pbUser: Writable = writable(undefined); const update_user = async (record: RecordModel): Promise => { let avatar_url: string; @@ -17,7 +21,7 @@ const update_user = async (record: RecordModel): Promise => { avatar_url = pb.files.getURL(driver_headshot_template, driver_headshot_template.file); } - pbUser = { + pbUser.set({ id: record.id, username: record.username, firstname: record.firstname, @@ -25,19 +29,19 @@ const update_user = async (record: RecordModel): Promise => { avatar: record.avatar, avatar_url: avatar_url, admin: record.admin, - } as User; + } as User); }; // Update the pbUser object when authStore changes (e.g. after logging in) pb.authStore.onChange(async () => { if (!pb.authStore.isValid) { console.log("pb.authStore is invalid: Setting pbUser to undefined"); - pbUser = undefined; + pbUser.set(undefined); return; } if (!pb.authStore.record) { console.log("pb.authStore.record is null: Setting pbUser to undefined"); - pbUser = undefined; + pbUser.set(undefined); return; } @@ -46,9 +50,24 @@ pb.authStore.onChange(async () => { // TODO: If the user has not chosen an avatar, // the page keeps displaying the "Login" button (wtf) console.log("Updating pbUser..."); - console.dir(pbUser, { depth: null }); + console.dir(get(pbUser), { depth: null }); }, true); +export const clear_auth = (): void => { + console.log("Cleared pb.authStore"); + pb.authStore.clear(); +}; + +export const refresh_auth = async (): Promise => { + if (pb.authStore.isValid) { + console.log("Refreshed pb.authStore"); + await pb.collection("users").authRefresh(); + } else { + console.log("pb.autStore is invalid: Did not refresh pb.authStore"); + pb.authStore.clear(); + } +}; + /** * Subscribe to PocketBase realtime collections */ diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1954154..ebfa116 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -42,11 +42,10 @@ } from "@skeletonlabs/skeleton"; import { computePosition, autoUpdate, offset, shift, flip, arrow } from "@floating-ui/dom"; import { invalidate } from "$app/navigation"; - import { get_error_toast, get_info_toast } from "$lib/toast"; - import { pb, pbUser, subscribe, unsubscribe } from "$lib/pocketbase"; + import { get_error_toast, get_info_toast, get_warning_toast } from "$lib/toast"; + import { clear_auth, pb, pbUser, refresh_auth, subscribe, unsubscribe } from "$lib/pocketbase"; import { AVATAR_HEIGHT, AVATAR_WIDTH } from "$lib/config"; import { error } from "@sveltejs/kit"; - import { get } from "svelte/store"; let { data, children }: { data: LayoutData; children: Snippet } = $props(); @@ -139,9 +138,9 @@ storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow }); // Reactive state - let username_value: string = $state(pbUser?.username ?? ""); - let firstname_value: string = $state(pbUser?.firstname ?? ""); - let email_value: string = $state(pbUser?.email ?? ""); + let username_value: string = $state($pbUser?.username ?? ""); + let firstname_value: string = $state($pbUser?.firstname ?? ""); + let email_value: string = $state($pbUser?.email ?? ""); let password_value: string = $state(""); let avatar_value: FileList | undefined = $state(); @@ -176,14 +175,15 @@ await invalidate("data:user"); drawerStore.close(); - username_value = pbUser?.username ?? ""; - firstname_value = pbUser?.firstname ?? ""; - email_value = pbUser?.email ?? ""; + username_value = $pbUser?.username ?? ""; + firstname_value = $pbUser?.firstname ?? ""; + email_value = $pbUser?.email ?? ""; password_value = ""; }; const logout = async (): Promise => { - pb.authStore.clear(); + clear_auth(); + await invalidate("data:user"); drawerStore.close(); username_value = ""; @@ -252,29 +252,34 @@ await pb.collection("users").requestVerification(email_value.trim()); toastStore.trigger(get_info_toast("Check your inbox!")); - pb.authStore.clear(); + // Just in case + clear_auth(); await login(); } else { - if (!pbUser?.id || pbUser.id === "") { + if (!$pbUser?.id || $pbUser.id === "") { toastStore.trigger(get_error_toast("Invalid user id!")); return; } - if (email_value && email_value.trim() !== pbUser.email) { - await pb.collection("users").requestEmailChange(email_value.trim()); - toastStore.trigger(get_info_toast("Check your inbox!")); - } - - await pb.collection("users").update(pbUser.id, { - username: username_value.trim().length > 0 ? username_value.trim() : pbUser.username, + await pb.collection("users").update($pbUser.id, { + username: username_value.trim().length > 0 ? username_value.trim() : $pbUser.username, firstname: - firstname_value.trim().length > 0 ? firstname_value.trim() : pbUser.firstname, + firstname_value.trim().length > 0 ? firstname_value.trim() : $pbUser.firstname, avatar: avatar_avif, }); - pb.authStore.clear(); - toastStore.trigger(get_info_toast("Please login again (sry UwU)!")); + if (email_value && email_value.trim() !== $pbUser.email) { + await pb.collection("users").requestEmailChange(email_value.trim()); + + // When changing the email address, the auth token is invalidated + await logout(); + toastStore.trigger(get_info_toast("Check your inbox!")); + toastStore.trigger( + get_warning_toast("Please login again AFTER confirming the email address!"), + ); + } + drawerStore.close(); } } catch (error) { @@ -539,14 +544,14 @@ activate={$page.url.pathname.startsWith("/data")}>Data - {#if !pbUser} + {#if !$pbUser} {:else}
diff --git a/src/routes/seasonpicks/+page.svelte b/src/routes/seasonpicks/+page.svelte index 9af2d6a..2673dfe 100644 --- a/src/routes/seasonpicks/+page.svelte +++ b/src/routes/seasonpicks/+page.svelte @@ -7,7 +7,7 @@ type ModalStore, } from "@skeletonlabs/skeleton"; import type { PageData } from "./$types"; - import type { Driver, Hottake, SeasonPick, SeasonPickedUser } from "$lib/schema"; + import type { Driver, Hottake, SeasonPick, SeasonPickedUser, User } from "$lib/schema"; import { ChequeredFlagIcon, LazyImage } from "$lib/components"; import { get_by_value, @@ -75,7 +75,7 @@ : 'lg:grid-cols-2 2xl:grid-cols-2'}" > - {#if pbUser} + {#if $pbUser} {@const teamwinners = data.seasonpick ? data.seasonpick.teamwinners .map((id: string) => get_by_value(drivers, "id", id) as Driver) @@ -346,10 +346,8 @@ : [undefined]}