From 575845acadafcb1ea6b85bfecadc384fa746ff02 Mon Sep 17 00:00:00 2001 From: Karl Ludwig Weise Date: Tue, 8 Sep 2026 08:58:59 +0200 Subject: [PATCH] Auto-decrypt appointments after added keys + removing arbitrary key expiry + slow down login to give browser time to write cookies --- src/lib/client/appointment-crypto.ts | 14 --------- src/lib/stores/staff-crypto.ts | 31 ++++++++++++++++++- .../(components)/AppointmentPreview.svelte | 16 +++++++--- .../(components)/MoveAppointment.svelte | 1 - src/routes/(pages)/login/login-form.svelte | 4 +++ 5 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/lib/client/appointment-crypto.ts b/src/lib/client/appointment-crypto.ts index 045fddb..da2857f 100644 --- a/src/lib/client/appointment-crypto.ts +++ b/src/lib/client/appointment-crypto.ts @@ -153,9 +153,7 @@ export class UnifiedAppointmentCrypto { // Staff-specific properties private staffKeyPair: StaffKeyPair | null = null; private staffId: string | null = null; - private tenantId: string | null = null; private staffAuthenticated: boolean = false; - private keyExpiry: number | null = null; // Shared crypto utilities private kyberCrypto: KyberCrypto = new KyberCrypto(); @@ -786,9 +784,7 @@ export class UnifiedAppointmentCrypto { }; this.staffId = staffId; - this.tenantId = tenantId; this.staffAuthenticated = true; - this.keyExpiry = Date.now() + 10 * 60 * 1000; // 10 minutes console.log("✅ Staff authentication successful with PRF"); } catch (error) { @@ -820,10 +816,6 @@ export class UnifiedAppointmentCrypto { throw new Error("Staff not authenticated"); } - if (this.keyExpiry && Date.now() > this.keyExpiry) { - throw new Error("Staff session expired - please authenticate again"); - } - try { // Parse the staffKeyShare which now contains: encapsulatedSecret || iv || encryptedTunnelKey const staffKeyShareBytes = this.hexToUint8Array(encryptedData.staffKeyShare); @@ -958,9 +950,7 @@ export class UnifiedAppointmentCrypto { logoutStaff(): void { this.staffKeyPair = null; this.staffId = null; - this.tenantId = null; this.staffAuthenticated = false; - this.keyExpiry = null; } /** @@ -1727,10 +1717,6 @@ export class UnifiedAppointmentCrypto { throw new Error("Staff not authenticated"); } - if (this.keyExpiry && Date.now() > this.keyExpiry) { - throw new Error("Staff session expired - please authenticate again"); - } - try { // Parse the staffKeyShare which now contains: encapsulatedSecret || iv || encryptedTunnelKey const staffKeyShareBytes = this.hexToUint8Array(staffKeyShare); diff --git a/src/lib/stores/staff-crypto.ts b/src/lib/stores/staff-crypto.ts index e05de96..bd5ebb6 100644 --- a/src/lib/stores/staff-crypto.ts +++ b/src/lib/stores/staff-crypto.ts @@ -1,9 +1,10 @@ -import { writable } from "svelte/store"; +import { get, writable } from "svelte/store"; import { UnifiedAppointmentCrypto } from "$lib/client/appointment-crypto"; import { auth } from "./auth"; interface StaffCryptoState { crypto: UnifiedAppointmentCrypto | null; + isAuthenticating: boolean; isAuthenticated: boolean; error: string | null; } @@ -11,6 +12,7 @@ interface StaffCryptoState { const createStaffCryptoStore = () => { const store = writable({ crypto: null, + isAuthenticating: false, isAuthenticated: false, error: null, }); @@ -37,11 +39,18 @@ const createStaffCryptoStore = () => { */ async authenticate(staffId: string, tenantId: string): Promise { try { + store.set({ + ...get(store), + isAuthenticating: true, + error: null, + }); + const crypto = new UnifiedAppointmentCrypto(); await crypto.authenticateStaff(staffId, tenantId); store.set({ crypto, + isAuthenticating: false, isAuthenticated: true, error: null, }); @@ -51,6 +60,7 @@ const createStaffCryptoStore = () => { const errorMessage = error instanceof Error ? error.message : "Authentication failed"; store.set({ crypto: null, + isAuthenticating: false, isAuthenticated: false, error: errorMessage, }); @@ -59,6 +69,24 @@ const createStaffCryptoStore = () => { } }, + async authenticateAndWait(staffId: string, tenantId: string): Promise { + const isAuthenticating = get(store).isAuthenticating; + if (isAuthenticating) { + // Wait for the ongoing authentication to complete + return new Promise((resolve) => { + const unsubscribe = store.subscribe((state) => { + if (!state.isAuthenticating) { + unsubscribe(); + resolve(state.isAuthenticated); + } + }); + }); + } else { + // Start a new authentication process + return await this.authenticate(staffId, tenantId); + } + }, + /** * Clear the staff crypto state and auth data */ @@ -66,6 +94,7 @@ const createStaffCryptoStore = () => { auth.clearPasskeyAuthData(); store.set({ crypto: null, + isAuthenticating: false, isAuthenticated: false, error: null, }); diff --git a/src/routes/(pages)/dashboard/calendar/(components)/AppointmentPreview.svelte b/src/routes/(pages)/dashboard/calendar/(components)/AppointmentPreview.svelte index 7bbb8f7..4c58a0d 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/AppointmentPreview.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/AppointmentPreview.svelte @@ -4,7 +4,6 @@ import { staffCrypto } from "$lib/stores/staff-crypto"; import { type TCalendarItem } from "$lib/types/calendar"; import Loader2Icon from "@lucide/svelte/icons/loader-2"; - import { onMount } from "svelte"; import { calendarStore } from "$lib/stores/calendar"; import { Button } from "$lib/components/ui/button"; import { m } from "$i18n/messages"; @@ -22,8 +21,14 @@ let decrypted = $state(); let error = $state(); - onMount(() => { - decrypt(); + $effect(() => { + if (!decrypted) { + if ($staffCrypto.isAuthenticated) { + decrypt(); + } else { + error = "Keys missing"; + } + } }); const errors = { @@ -31,6 +36,8 @@ }; const decrypt = async (retry = true) => { + error = undefined; + if (!item.appointment) { console.error("Unable to decrypt appointment data - no appointment data in item.", item.id); error = "Missing data"; @@ -64,8 +71,7 @@ const user = $auth.user; if (retry && user && user.tenantId) { console.warn("Staff crypto not initialized, retrying decryption..."); - // TODO: This should only be done once, not in every instance - await staffCrypto.authenticate(user.id, user.tenantId); + await staffCrypto.authenticateAndWait(user.id, user.tenantId); decrypt(false); return; } diff --git a/src/routes/(pages)/dashboard/calendar/(components)/MoveAppointment.svelte b/src/routes/(pages)/dashboard/calendar/(components)/MoveAppointment.svelte index 5133afe..9aaa403 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/MoveAppointment.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/MoveAppointment.svelte @@ -74,7 +74,6 @@ }; onMount(() => { - console.log("item.start", item.start); if (mode.agentId && item.availableAgents) { const availableAgents = item.availableAgents.map((it) => it.id); if (availableAgents.includes(mode.agentId)) { diff --git a/src/routes/(pages)/login/login-form.svelte b/src/routes/(pages)/login/login-form.svelte index 036eacc..c9877e6 100644 --- a/src/routes/(pages)/login/login-form.svelte +++ b/src/routes/(pages)/login/login-form.svelte @@ -43,6 +43,10 @@ onResult: async (event) => { if (event.result.type === "success") { auth.setUser(event.result.data?.user); + + // Wait for cookies to be set before navigating to dashboard + await new Promise((resolve) => setTimeout(resolve, 100)); + await goto(resolve(ROUTES.DASHBOARD.MAIN)); } else { if ($formData.type === "passkey" && $formData.id === "") {