From 09ffc21523a90ad96fc03a8a83f976fdec1cbb11 Mon Sep 17 00:00:00 2001 From: Karl Ludwig Weise Date: Fri, 6 Mar 2026 10:22:28 +0100 Subject: [PATCH 1/9] Added flow for staff members to create appointments (#204) * Added flow for staff members to create appointments * Add calendar page title * Remove security check npm audit for pull-requests * Fix docker build to be multi-platform * Attempt to fix tests * Remove obsolete test * Refresh session every 3 minutes --------- Co-authored-by: Karl Ludwig Weise --- .github/workflows/pr-checks.yml | 3 - .github/workflows/release.yml | 2 +- Dockerfile | 3 + package.json | 2 +- project.inlang/messages/de.json | 8 +- project.inlang/messages/en.json | 6 +- src/lib/client/appointment-crypto.ts | 205 ++++++++++++++---- .../server/services/appointment-service.ts | 28 ++- src/lib/utils/localizations.ts | 14 ++ src/routes/(pages)/dashboard/+layout.svelte | 2 +- .../(components)/AppointmentDetail.svelte | 46 ++-- .../add-appointment/AddAppointment.svelte | 60 ++++- .../add-appointment/Summary.svelte | 31 ++- .../search-client-form.svelte | 56 +++-- .../(components)/add-appointment/types.ts | 4 + .../(pages)/dashboard/calendar/+page.svelte | 6 +- .../[appointmentId]/delete/+server.ts | 4 +- .../delete/__tests__/delete-api.test.ts | 13 +- .../[id]/appointments/staff-create/+server.ts | 7 +- .../__tests__/staff-create.test.ts | 56 ----- 20 files changed, 364 insertions(+), 192 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 643097f..2513c4e 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -22,9 +22,6 @@ jobs: - name: Install dependencies run: npm ci - - name: Security audit - run: npm audit --audit-level=high - - name: Run linting run: npm run lint check: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c37478..b9e7ad7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} - name: Build Docker image - run: docker build -t openreception/open-reception:${{ github.ref_name }} . + run: docker buildx build --platform linux/amd64,linux/arm64 -t openreception/open-reception:${{ github.ref_name }} . - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master diff --git a/Dockerfile b/Dockerfile index aec12d0..bdb274c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,9 @@ COPY package*.json ./ RUN npm ci --ignore-scripts && \ npm cache clean --force +# Set node env +ENV NODE_ENV=production + # Copy source and build COPY . . RUN npm run build diff --git a/package.json b/package.json index 49e9cf2..c17fac7 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "docker:dev:down": "docker compose -f docker-compose.dev.yml down", "docker:dev:logs": "docker compose -f docker-compose.dev.yml logs -f", "docker:dev:clean": "docker compose -f docker-compose.dev.yml down -v --remove-orphans", - "docker:build": "docker build -t openreception/open-reception:latest .", + "docker:build": "docker buildx build --platform linux/amd64,linux/arm64 -t openreception/open-reception:latest .", "docker:build:tag": "docker tag openreception/open-reception:latest openreception/open-reception:$npm_package_version", "docker:push": "docker push openreception/open-reception:$npm_package_version && docker push openreception/open-reception:latest", "docker:build-and-push": "npm run docker:build && npm run docker:build:tag && npm run docker:push", diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json index 7e0fc0a..eaecd38 100644 --- a/project.inlang/messages/de.json +++ b/project.inlang/messages/de.json @@ -924,8 +924,9 @@ } }, "calendar": { + "title": "Kalender", "today": "Heute", - "loading": "Loading calendar", + "loading": "Kalender wird geladen", "decrypting": "wird entschlüsselt...", "decryptingError": "Entschlüsselung fehlgeschlagen. Bitte neu anmelden.", "decryptingErrorNoKeyShare": "Entschlüsselung fehlgeschlagen. Bitten Sie ein Teammitglied, Ihnen Zugriff zu gewähren.", @@ -957,11 +958,14 @@ "error": "Fehler beim Bestätigen des Termins" }, "addAppointment": { + "title": "Neuen Termin erstellen", "preview": "Neuer Termin {time} Uhr {channel}", "steps": { "selectClient": { "hasNoEmail": "Klient hat keine E-Mail Adresse", - "action": "Klient suchen" + "action": "Klient suchen", + "confirmNewClient": "Dieser Klient hat noch kein Konto. Möchten Sie für ihn ein Konto erstellen?", + "proceedExistingClient": "Klient ausgewählt." }, "agents": { "title": "Akteur auswählen" diff --git a/project.inlang/messages/en.json b/project.inlang/messages/en.json index bd746e7..f2c0ee5 100644 --- a/project.inlang/messages/en.json +++ b/project.inlang/messages/en.json @@ -933,6 +933,7 @@ } }, "calendar": { + "title": "Calendar", "today": "Today", "loading": "Loading calendar", "decrypting": "decrypting...", @@ -966,11 +967,14 @@ "error": "Failed to confirm appointment" }, "addAppointment": { + "title": "Add Appointment", "preview": "Add {time} {channel}", "steps": { "selectClient": { "hasNoEmail": "Client has no e-mail", - "action": "Search Client" + "action": "Search Client", + "confirmNewClient": "This client does not have an account yet. Do you want to create an account for them?", + "proceedExistingClient": "Client selected." }, "agents": { "title": "Select an Agent" diff --git a/src/lib/client/appointment-crypto.ts b/src/lib/client/appointment-crypto.ts index 42b2fd6..af79ce0 100644 --- a/src/lib/client/appointment-crypto.ts +++ b/src/lib/client/appointment-crypto.ts @@ -48,8 +48,9 @@ */ import { OptimizedArgon2 } from "$lib/crypto/hashing"; +import { AESCrypto, BufferUtils, KyberCrypto, ShamirSecretSharing } from "$lib/crypto/utils"; +import type { ClientTunnelResponse } from "$lib/server/services/appointment-service"; import { pinThrottleStore } from "$lib/stores/pin-throttle"; -import { KyberCrypto, AESCrypto, ShamirSecretSharing, BufferUtils } from "$lib/crypto/utils"; // Type definitions for unified cryptography interface ClientKeyPair { @@ -104,6 +105,17 @@ interface MyAppointmentsResponse { encryptedData: EncryptedData; }>; } + +type StaffKeyShares = Array<{ userId: string; encryptedTunnelKey: string }>; + +type EncryptableTunnelConfig = { + emailHash: string; + decryptedTunnelKey: CryptoKey; + staffKeyShares: StaffKeyShares; + clientPublicKey: string; + tunnelId: string; +}; + /** * Generate deterministic SHA-256 hash of email for privacy-preserving lookups */ @@ -483,6 +495,143 @@ export class UnifiedAppointmentCrypto { } } + private createTunnelForNewClient = async (params: { + tenantId: string; + email: string; + }): Promise => { + const usedPin = crypto.randomUUID().slice(0, 6); + + // Create tunnel + await this.initNewClient(params.email, usedPin, params.tenantId); + + if (!this.tunnelId) { + throw new Error("Failed to use initialized client tunnel"); + } + + if (!this.tunnelKey) { + throw new Error("Failed to use initialized client tunnel key"); + } + + if (!this.clientKeyPair?.publicKey) { + throw new Error("Failed to use initialized client public key"); + } + + return { + tunnelId: this.tunnelId, + emailHash: await hashEmail(params.email), + clientPublicKey: this.clientKeyPair?.publicKey, + decryptedTunnelKey: this.tunnelKey, + staffKeyShares: await this.getStaffKeyShares(params.tenantId), + }; + }; + + private useTunnelForExistingClient = async (params: { + tunnel: ClientTunnelResponse; + tenantId: string; + email: string; + }): Promise => { + const decryptedTunnelKey = await this.decryptTunnelKeyByStaff( + params.tunnel.currentStaffEncryptedTunnelKey!, + ); + return { + tunnelId: params.tunnel.id, + emailHash: await hashEmail(params.email), + clientPublicKey: params.tunnel.clientPublicKey, + decryptedTunnelKey, + staffKeyShares: await this.getStaffKeyShares(params.tenantId, decryptedTunnelKey), + }; + }; + + /** + * Creates a new encrypted appointment that is created by a staff member + */ + async createAppointmentByStaff(params: { + appointmentData: AppointmentDataByStaff; + appointmentDate: Date; + agentId: string; + channelId: string; + duration: number; + tenantId: string; + email?: string; + hasNoEmail: boolean; + tunnel: ClientTunnelResponse | undefined; + }): Promise { + if (!this.staffAuthenticated || !this.staffKeyPair) { + throw new Error("Staff Member not authenticated"); + } + + try { + // If new client, create new tunnel and then get it + const usedEmail = params.email ?? `${crypto.randomUUID()}@client.noemail`; + const tunnelConfig = !params.tunnel + ? await this.createTunnelForNewClient({ tenantId: params.tenantId, email: usedEmail }) + : await this.useTunnelForExistingClient({ + tunnel: params.tunnel, + tenantId: params.tenantId, + email: usedEmail, + }); + + // Encrypt appointment data + const encryptedAppointment = await this.encryptAppointmentData( + params.appointmentData, + tunnelConfig.decryptedTunnelKey, + ); + + // Set tunnelKey & clientKeyPair for encryptTunnelKeyForClient + this.tunnelKey = tunnelConfig.decryptedTunnelKey; + this.clientKeyPair = { + publicKey: tunnelConfig.clientPublicKey, + privateKey: "", // Not needed here + }; + + // Call endpoint + const sendEmail = params.appointmentData.shareEmail && Boolean(params.email); + const requestData = { + clientEmail: params.appointmentData.shareEmail ? usedEmail : undefined, + hasNoEmail: params.hasNoEmail, + emailHash: await hashEmail(usedEmail), + appointmentDate: params.appointmentDate.toISOString(), + duration: params.duration, + agentId: params.agentId, + channelId: params.channelId, + encryptedAppointment, + sendEmail, + clientLanguage: params.appointmentData.locale, + tunnelId: tunnelConfig.tunnelId, + clientPublicKey: tunnelConfig.clientPublicKey, + staffKeyShares: tunnelConfig.staffKeyShares, + privateKeyShare: await this.getClientKeyShare(), + clientEncryptedTunnelKey: await this.encryptTunnelKeyForClient(), + }; + const response = await fetch(`/api/tenants/${params.tenantId}/appointments/staff-create`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestData), + }); + + if (!response.ok) { + throw new Error("Appointment could not be created"); + } + + const result = await response.json(); + + console.log("✅ Encrypted appointment created:", result.id); + return result.id; + } catch (error) { + console.error("❌ Error creating appointment:", error); + throw error; + } finally { + // Clear sensitive data from class properties to prevent leaks on reuse + this.tunnelKey = null; + this.clientKeyPair = null; + this.emailHash = null; + this.tunnelId = null; + this.serverPrivateKeyShare = null; + this.clientAuthenticated = false; + this.pin = null; + } + } + /** * Allows to check, if a client is authenticated */ @@ -947,8 +1096,10 @@ export class UnifiedAppointmentCrypto { */ private async encryptAppointmentData( data: AppointmentData | AppointmentDataByStaff, + tunnelKey?: CryptoKey, ): Promise { - if (!this.tunnelKey) throw new Error("No tunnel key available"); + const usedTunnelKey = tunnelKey ?? this.tunnelKey; + if (!usedTunnelKey) throw new Error("No tunnel key available"); const encoder = new TextEncoder(); const plaintext = encoder.encode(JSON.stringify(data)); @@ -956,7 +1107,7 @@ export class UnifiedAppointmentCrypto { const encrypted = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, - this.tunnelKey, + usedTunnelKey, plaintext, ); @@ -1089,35 +1240,12 @@ export class UnifiedAppointmentCrypto { // Public key is stored as Base64, not Hex const staffPublicKeyBytes = this.base64ToUint8Array(staff.publicKey); - console.log("🔐 Encrypting tunnel key for staff:", { - userId: staff.userId, - publicKeyLength: staffPublicKeyBytes.length, - publicKeyHex: - Array.from(staffPublicKeyBytes) - .map((b) => b.toString(16).padStart(2, "0")) - .join("") - .substring(0, 64) + "...", - tunnelKeyLength: tunnelKeyArray.length, - }); - // Kyber encapsulation creates a shared secret const { sharedSecret, encapsulatedSecret } = KyberCrypto.encapsulate(staffPublicKeyBytes); - console.log("🔑 Kyber encapsulation done:", { - sharedSecretLength: sharedSecret.length, - encapsulatedSecretLength: encapsulatedSecret.length, - }); - // Use the first 32 bytes of shared secret as AES key (same as decryption) const aesKeyBytes = sharedSecret.slice(0, 32); - console.log( - "🔑 AES key for encryption (hex):", - Array.from(aesKeyBytes) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""), - ); - // Import as CryptoKey for Web Crypto API const aesKey = await crypto.subtle.importKey("raw", aesKeyBytes, { name: "AES-GCM" }, false, [ "encrypt", @@ -1126,19 +1254,6 @@ export class UnifiedAppointmentCrypto { // Generate IV for AES-GCM const iv = BufferUtils.randomBytes(12); - console.log( - "📍 IV for encryption (hex):", - Array.from(iv) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""), - ); - console.log( - "🔒 Tunnel key to encrypt (hex):", - Array.from(tunnelKeyArray) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""), - ); - // Encrypt tunnel key with AES-GCM const encrypted = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, @@ -1149,13 +1264,6 @@ export class UnifiedAppointmentCrypto { // encrypted contains ciphertext + 16-byte auth tag const encryptedArray = new Uint8Array(encrypted); - console.log( - "🔒 Encrypted tunnel key (hex):", - Array.from(encryptedArray) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""), - ); - // Store: encapsulatedSecret || iv || encrypted (ciphertext+authTag) const combined = new Uint8Array( encapsulatedSecret.length + iv.length + encryptedArray.length, @@ -1386,10 +1494,11 @@ export class UnifiedAppointmentCrypto { private async getStaffKeyShares( tenantId: string, - ): Promise> { + decryptedTunnelKey?: CryptoKey, + ): Promise { // Fetch and encrypt tunnel key for all staff members const staffPublicKeys = await this.fetchStaffPublicKeys(tenantId); - return await this.encryptTunnelKeyForStaff(staffPublicKeys); + return await this.encryptTunnelKeyForStaff(staffPublicKeys, decryptedTunnelKey); } private async getClientKeyShare(): Promise { diff --git a/src/lib/server/services/appointment-service.ts b/src/lib/server/services/appointment-service.ts index 6be65e3..327c424 100644 --- a/src/lib/server/services/appointment-service.ts +++ b/src/lib/server/services/appointment-service.ts @@ -348,7 +348,7 @@ export class AppointmentService { */ public async deleteAppointmentByStaff( appointmentId: string, - clientEmail: string, + clientEmail: string | undefined, clientLanguage: string = "de", ): Promise { const log = logger.setContext("AppointmentService"); @@ -396,18 +396,22 @@ export class AppointmentService { const channelTitle = await getChannelTitle(this.tenantId, channelId, clientLanguage); // Send cancellation email to client (async, don't wait) - const clientData = { - email: clientEmail, - language: clientLanguage, - }; + if (clientEmail) { + const clientData = { + email: clientEmail, + language: clientLanguage, + }; - sendAppointmentCancelledEmail(clientData, tenant, appointment, channelTitle).catch((error) => { - log.error("Failed to send appointment cancellation email", { - appointmentId, - clientEmail, - error: String(error), - }); - }); + sendAppointmentCancelledEmail(clientData, tenant, appointment, channelTitle).catch( + (error) => { + log.error("Failed to send appointment cancellation email", { + appointmentId, + clientEmail, + error: String(error), + }); + }, + ); + } // Create notifications for all staff members in the channel const notificationService = await NotificationService.forTenant(this.tenantId); diff --git a/src/lib/utils/localizations.ts b/src/lib/utils/localizations.ts index 0094bbb..868abca 100644 --- a/src/lib/utils/localizations.ts +++ b/src/lib/utils/localizations.ts @@ -1,6 +1,7 @@ import { getLocale } from "$i18n/runtime"; import type { supportedLocales } from "$lib/const/locales"; import type { TPublicTenant } from "$lib/types/public"; +import type { TTenant } from "$lib/types/tenant"; export const removeEmptyTranslations = (object: { [key: string]: string } | undefined) => { if (!object) return object; @@ -25,3 +26,16 @@ export const getPublicLocale = (tenant: TPublicTenant): string => { } return tenant.defaultLanguage as unknown as string; }; + +export const getDefaultAppointmentLocale = (tenant: TTenant | null): string => { + const locale = getLocale(); + if (!tenant) return locale; + + const availableLanguages = tenant.languages; + if (availableLanguages.includes(locale)) { + return locale; + } + + // TODO: Not ideal + return availableLanguages[0]; +}; diff --git a/src/routes/(pages)/dashboard/+layout.svelte b/src/routes/(pages)/dashboard/+layout.svelte index 2c65d89..63da6d2 100644 --- a/src/routes/(pages)/dashboard/+layout.svelte +++ b/src/routes/(pages)/dashboard/+layout.svelte @@ -28,7 +28,7 @@ } if (!intervalSession) { refreshSession(); - intervalSession = setInterval(refreshSession, 10 * 60 * 1000); // 10 minutes + intervalSession = setInterval(refreshSession, 3 * 60 * 1000); // 3 minutes } const unsubscribe = () => { diff --git a/src/routes/(pages)/dashboard/calendar/(components)/AppointmentDetail.svelte b/src/routes/(pages)/dashboard/calendar/(components)/AppointmentDetail.svelte index a4b77a2..c0636cc 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/AppointmentDetail.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/AppointmentDetail.svelte @@ -87,33 +87,37 @@ {#if item.appointment.appointment} -
+
{item.decrypted.name}
-
- - {#if item.decrypted.phone} - - {/if} -
+ {#if item.decrypted.email || item.decrypted.phone} +
+ {#if item.decrypted.email} + + {/if} + {#if item.decrypted.phone} + + {/if} +
+ {/if}
diff --git a/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/AddAppointment.svelte b/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/AddAppointment.svelte index 5f01406..81a1259 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/AddAppointment.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/AddAppointment.svelte @@ -1,15 +1,20 @@ - + {#if step === "email"} {:else if step === "agent" && item.availableAgents} {:else if step === "summary"} - {:else if step === "client"} diff --git a/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/Summary.svelte b/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/Summary.svelte index 013027b..8aa5260 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/Summary.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/Summary.svelte @@ -4,18 +4,24 @@ import { Text } from "$lib/components/ui/typography"; import { agents as agentsStore } from "$lib/stores/agents"; import { toDisplayDateTime } from "$lib/utils/datetime"; - import { Calendar, Mail, Phone, User, UserStar } from "@lucide/svelte"; + import { Calendar, Languages, Mail, Phone, User, UserStar } from "@lucide/svelte"; import type { TAddAppointment, TAddAppointmentStep } from "./types"; + import * as Select from "$lib/components/ui/select"; + import { tenants } from "$lib/stores/tenants"; + import { languageSwitchLocales } from "$lib/const/locales"; let { step, newAppointment, + onChangeLocale, }: { step: TAddAppointmentStep; newAppointment: TAddAppointment; + onChangeLocale: (locale: string) => void; } = $props(); let agent = $derived($agentsStore.agents.find((a) => a.id === newAppointment.agentId)); + let languages = $derived($tenants.currentTenant?.languages);
@@ -41,8 +47,29 @@
{/if} {#if newAppointment.email} +
+ + + + {@const locale = newAppointment.locale + ? languageSwitchLocales[newAppointment.locale as keyof typeof languageSwitchLocales] + : undefined} + {locale ? locale.label : "Select language"} + + + {#each languages as language (language)} + + {languageSwitchLocales[language as keyof typeof languageSwitchLocales].label} + + {/each} + + +
{#if schedule === undefined}{/if} diff --git a/src/routes/(pages)/(clients)/book-appointment/[[id]]/(components)/utils.ts b/src/routes/(pages)/(clients)/book-appointment/[[id]]/(components)/utils.ts index 4880106..b20f3e3 100644 --- a/src/routes/(pages)/(clients)/book-appointment/[[id]]/(components)/utils.ts +++ b/src/routes/(pages)/(clients)/book-appointment/[[id]]/(components)/utils.ts @@ -98,6 +98,11 @@ export const fetchSchedule = async (opts: { if (!browser) return; const curDate = today(getLocalTimeZone()); + + // Return empty list for past months + if (opts.year < curDate.year || (opts.year === curDate.year && opts.month < curDate.month)) { + return []; + } const startDay = curDate.year === opts.year && curDate.month === opts.month ? curDate.day : 1; const lastDayOfMonth = new Date(opts.year, opts.month, 0).getDate(); diff --git a/src/routes/(pages)/dashboard/calendar/+page.svelte b/src/routes/(pages)/dashboard/calendar/+page.svelte index 1d3c985..89b98ac 100644 --- a/src/routes/(pages)/dashboard/calendar/+page.svelte +++ b/src/routes/(pages)/dashboard/calendar/+page.svelte @@ -136,21 +136,23 @@ if (["all", "available"].includes(shownAppointments)) { if (shownChannels.length === 0 || shownChannels.includes(channelId)) { channelData.availableSlots.forEach((slot) => { - if ( - shownAgents.length === 0 || - shownAgents.some((id) => slot.availableAgents.map((a) => a.id).includes(id)) - ) { - channelItems.push({ - id: `${channelId}-${slot.from}`, - date: dayEntry.date, - start: slot.from, - duration: slot.duration, - channelId, - color: channelData.channel.color, - column: 0, - status: "available", - availableAgents: slot.availableAgents, - }); + if (new Date(slot.to) > new Date()) { + if ( + shownAgents.length === 0 || + shownAgents.some((id) => slot.availableAgents.map((a) => a.id).includes(id)) + ) { + channelItems.push({ + id: `${channelId}-${slot.from}`, + date: dayEntry.date, + start: slot.from, + duration: slot.duration, + channelId, + color: channelData.channel.color, + column: 0, + status: "available", + availableAgents: slot.availableAgents, + }); + } } }); } From bd6c10e16029ac9e2df42e4ec4311f570fb80bc4 Mon Sep 17 00:00:00 2001 From: Hendrik Date: Tue, 10 Mar 2026 19:08:03 +0100 Subject: [PATCH 6/9] 224 remove sensitive information from logs (#225) * Logging adapted * Fix log mocks * Lint fixes --- src/lib/client/appointment-crypto.ts | 6 +---- src/lib/server/auth/authorization-service.ts | 12 +++++----- src/lib/server/auth/session-service.ts | 22 +++++++++---------- src/lib/server/auth/webauthn-service.ts | 8 ++----- src/lib/server/email/mailer.ts | 11 ++++++---- .../server/services/appointment-service.ts | 17 ++++++-------- .../central-database-migration-service.ts | 2 -- .../services/client-pin-reset-service.ts | 4 ---- src/lib/server/services/invite-service.ts | 6 +---- .../server/services/staff-crypto.service.ts | 2 -- src/lib/server/services/staff-service.ts | 2 +- src/lib/server/services/user-service.ts | 1 - src/lib/stores/pin-throttle.ts | 4 ++-- .../appointment-booked/+server.ts | 6 +---- .../appointment-reminder/+server.ts | 6 +---- .../appointment-request/+server.ts | 6 +---- .../e-mail-templates/confirmation/+server.ts | 6 +---- .../e-mail-templates/pin-reset/+server.ts | 5 +---- .../e-mail-templates/user-invite/+server.ts | 6 +---- .../setup-passkey/setup-passkey-form.svelte | 2 -- .../(components)/AppointmentPreview.svelte | 2 +- src/routes/(pages)/login/login-form.svelte | 2 -- src/routes/api/auth/invite/+server.ts | 3 +-- src/routes/api/auth/login/+server.ts | 1 - .../api/auth/passkeys/[passkeyId]/+server.ts | 2 -- .../passkeys/[passkeyId]/crypto/+server.ts | 4 +--- src/routes/api/env/+server.ts | 2 -- src/routes/api/public/utils.ts | 5 ++++- src/routes/api/tenants/[id]/+server.ts | 2 +- .../delete-by-client/+server.ts | 5 ++--- .../[appointmentId]/deny/+server.ts | 5 ++--- .../__tests__/deny-appointment-api.test.ts | 1 + .../appointments/add-to-tunnel/+server.ts | 9 +++----- .../[id]/appointments/challenge/+server.ts | 6 ++--- .../appointments/create-new-client/+server.ts | 5 +---- .../__tests__/create-new-client-api.test.ts | 16 ++++---------- .../[id]/appointments/staff-create/+server.ts | 1 - .../tunnels/add-staff-key-shares/+server.ts | 5 +---- .../appointments/verify-challenge/+server.ts | 8 +++---- .../clients/pin-reset/complete/+server.ts | 3 +-- .../[id]/clients/pin-reset/init/+server.ts | 3 +-- .../[id]/clients/pin-reset/request/+server.ts | 4 +--- .../api/tenants/[id]/notifications/+server.ts | 2 +- .../notifications/[notificationId]/+server.ts | 4 ++-- .../[id]/staff/[staffId]/crypto/+server.ts | 2 -- src/server-hooks/apiAuthHandle.ts | 2 +- 46 files changed, 77 insertions(+), 161 deletions(-) diff --git a/src/lib/client/appointment-crypto.ts b/src/lib/client/appointment-crypto.ts index af79ce0..7f51419 100644 --- a/src/lib/client/appointment-crypto.ts +++ b/src/lib/client/appointment-crypto.ts @@ -208,11 +208,7 @@ export class UnifiedAppointmentCrypto { // Note: clientKeyShare will be used during actual appointment creation await this.encryptTunnelKeyForClient(); - console.log("✅ New client initialized", { - tunnelId: this.tunnelId, - emailHashPrefix: this.emailHash.slice(0, 8), - staffCount: staffPublicKeys.length, - }); + console.log("✅ New client initialized", {}); this.clientAuthenticated = true; } catch (error) { diff --git a/src/lib/server/auth/authorization-service.ts b/src/lib/server/auth/authorization-service.ts index 53682ac..e401ecb 100644 --- a/src/lib/server/auth/authorization-service.ts +++ b/src/lib/server/auth/authorization-service.ts @@ -13,9 +13,7 @@ export class AuthorizationService { } if (user.role !== requiredRole) { - logger.warn( - `Access denied: User ${user.email} has role ${user.role}, required ${requiredRole}`, - ); + logger.warn(`Access denied: User ${user.id} has role ${user.role}, required ${requiredRole}`); throw new AuthorizationError(); } @@ -29,7 +27,7 @@ export class AuthorizationService { if (!allowedRoles.includes(user.role as UserRole)) { logger.warn( - `Access denied: User ${user.email} has role ${user.role}, allowed roles: ${allowedRoles.join(", ")}`, + `Access denied: User ${user.id} has role ${user.role}, allowed roles: ${allowedRoles.join(", ")}`, ); throw new AuthorizationError(); } @@ -51,13 +49,13 @@ export class AuthorizationService { if (user.role === "TENANT_ADMIN" || user.role === "STAFF") { if (!user.tenantId) { - logger.warn(`Access denied: User ${user.email} has no tenant assigned`); + logger.warn(`Access denied: User ${user.id} has no tenant assigned`); throw new AuthorizationError("No tenant access"); } if (user.tenantId !== tenantId) { logger.warn( - `Access denied: User ${user.email} trying to access tenant ${tenantId}, but belongs to ${user.tenantId}`, + `Access denied: User ${user.id} trying to access tenant ${tenantId}, but belongs to ${user.tenantId}`, ); throw new AuthorizationError("Tenant access denied"); } @@ -66,7 +64,7 @@ export class AuthorizationService { return; } - logger.warn(`Access denied: User ${user.email} has invalid role ${user.role}`); + logger.warn(`Access denied: User ${user.id} has invalid role ${user.role}`); throw new AuthorizationError("Invalid role"); } diff --git a/src/lib/server/auth/session-service.ts b/src/lib/server/auth/session-service.ts index f45542d..c246c88 100644 --- a/src/lib/server/auth/session-service.ts +++ b/src/lib/server/auth/session-service.ts @@ -43,7 +43,7 @@ export class SessionService { userAgent?: string, passkeyId?: string, ): Promise { - logger.info(`Creating session for user: ${userId}`); + logger.debug(`Creating session for user: ${userId}`); const existingUser = await db.select().from(user).where(eq(user.id, userId)).limit(1); @@ -88,7 +88,7 @@ export class SessionService { await db.update(user).set({ lastLoginAt: new Date() }).where(eq(user.id, userId)); - logger.info(`Session created successfully for user: ${userId}`, { passkeyId }); + logger.debug(`Session created successfully for user: ${userId}`, { passkeyId }); return { sessionToken: updatedSession.sessionToken, @@ -246,7 +246,7 @@ export class SessionService { }) .where(eq(userSession.id, session.user_session.id)); - logger.info("Session tokens refreshed successfully"); + logger.debug("Session tokens refreshed successfully"); return { accessToken: newTokens.accessToken, @@ -256,27 +256,27 @@ export class SessionService { } static async logout(sessionToken: string): Promise { - logger.info(`Logging out session: ${sessionToken}`); + logger.debug(`Logging out session: ${sessionToken}`); await db.delete(userSession).where(eq(userSession.sessionToken, sessionToken)); - logger.info(`Session logged out successfully: ${sessionToken}`); + logger.debug(`Session logged out successfully: ${sessionToken}`); } static async logoutAllSessions(userId: string): Promise { - logger.info(`Logging out all sessions for user: ${userId}`); + logger.debug(`Logging out all sessions for user: ${userId}`); await db.delete(userSession).where(eq(userSession.userId, userId)); - logger.info(`All sessions logged out for user: ${userId}`); + logger.debug(`All sessions logged out for user: ${userId}`); } static async cleanupExpiredSessions(): Promise { - logger.info("Cleaning up expired sessions"); + logger.debug("Cleaning up expired sessions"); await db.delete(userSession).where(lt(userSession.expiresAt, new Date())); - logger.info("Expired sessions cleaned up"); + logger.debug("Expired sessions cleaned up"); } static async getUserSession(sessionId: string): Promise { @@ -309,10 +309,10 @@ export class SessionService { } static async revokeSession(sessionId: string): Promise { - logger.info(`Revoking session: ${sessionId}`); + logger.debug(`Revoking session: ${sessionId}`); await db.delete(userSession).where(eq(userSession.id, sessionId)); - logger.info(`Session revoked: ${sessionId}`); + logger.debug(`Session revoked: ${sessionId}`); } } diff --git a/src/lib/server/auth/webauthn-service.ts b/src/lib/server/auth/webauthn-service.ts index 650803b..b298178 100644 --- a/src/lib/server/auth/webauthn-service.ts +++ b/src/lib/server/auth/webauthn-service.ts @@ -155,8 +155,7 @@ export class WebAuthnService { }) .where(eq(userPasskey.id, normalizedCredentialId)); - logger.info("WebAuthn authentication successful", { - credentialId: normalizedCredentialId.substring(0, 20) + "...", + logger.debug("WebAuthn authentication successful", { userId: passkey.userId, counterUpdated: `${passkey.counter} → ${newCounter}`, }); @@ -244,12 +243,9 @@ export class WebAuthnService { "base64", ); - logger.info("Registration verified successfully", { - credentialId: normalizedCredentialId.substring(0, 20) + "...", - credentialIdLength: normalizedCredentialId.length, + logger.debug("Registration verified successfully", { counter: registrationInfo.credential.counter, publicKeyLength: registrationInfo.credential.publicKey.length, - publicKeyBytesFirst10: Array.from(registrationInfo.credential.publicKey.slice(0, 10)), }); return { diff --git a/src/lib/server/email/mailer.ts b/src/lib/server/email/mailer.ts index 164c19a..a526a4f 100644 --- a/src/lib/server/email/mailer.ts +++ b/src/lib/server/email/mailer.ts @@ -2,6 +2,7 @@ import nodemailer from "nodemailer"; import { env } from "$env/dynamic/private"; import type Mail from "nodemailer/lib/mailer"; import type { SelectUser } from "../db/central-schema"; +import logger from "$lib/logger"; /** * Simple client data type for email sending @@ -92,10 +93,11 @@ export async function sendEmail( textContent: string, ): Promise { const transporter = createTransporter(); + logger.setContext("sendMail"); if (!recipient.email) { // Recipient has not stored an email address, so no mail will be send - // TODO: Log this event + logger.info("Recipient has no email address, skipping email sending"); return; } @@ -121,9 +123,9 @@ export async function sendEmail( try { await transporter.sendMail(mailOptions); - console.log(`Email sent successfully to ${recipient.email}`); + logger.debug(`Email sent successfully to ${recipient.email}`); } catch (error) { - console.error("Failed to send email:", error); + logger.error("Failed to send email:", { error }); throw new Error(`Failed to send email to ${recipient.email}`); } } @@ -134,11 +136,12 @@ export async function sendEmail( */ export async function testEmailConnection(): Promise { try { + logger.setContext("testEmailConnection"); const transporter = createTransporter(); await transporter.verify(); return true; } catch (error) { - console.error("SMTP connection test failed:", error); + logger.error("SMTP connection test failed:", { error }); return false; } } diff --git a/src/lib/server/services/appointment-service.ts b/src/lib/server/services/appointment-service.ts index 327c424..5a03571 100644 --- a/src/lib/server/services/appointment-service.ts +++ b/src/lib/server/services/appointment-service.ts @@ -513,11 +513,10 @@ export class AppointmentService { ): Promise { const log = logger.setContext("AppointmentService"); - log.info("Adding appointment to existing tunnel", { + log.debug("Adding appointment to existing tunnel", { tenantId: this.tenantId, tunnelId: appointmentData.tunnelId, appointmentDate: appointmentData.appointmentDate, - emailHashPrefix: appointmentData.emailHash.slice(0, 8), }); const db = await this.getDb(); @@ -593,7 +592,7 @@ export class AppointmentService { requiresConfirmation, }; - log.info("Successfully added appointment to tunnel", { + log.debug("Successfully added appointment to tunnel", { tenantId: this.tenantId, tunnelId: appointmentData.tunnelId, appointmentId: result.id, @@ -611,11 +610,10 @@ export class AppointmentService { ): Promise { const log = logger.setContext("AppointmentService"); - log.info("Creating new client appointment tunnel", { + log.debug("Creating new client appointment tunnel", { tenantId: this.tenantId, tunnelId: clientData.tunnelId, appointmentDate: clientData.appointmentDate, - emailHashPrefix: clientData.emailHash.slice(0, 8), }); // Check if there are any authorized users (ACCESS_GRANTED) in this tenant @@ -764,7 +762,7 @@ export class AppointmentService { requiresConfirmation: result.requiresConfirmation, }; - log.info("Successfully created new client appointment tunnel", { + log.debug("Successfully created new client appointment tunnel", { tenantId: this.tenantId, tunnelId: clientData.tunnelId, appointmentId: result.appointment.id, @@ -955,10 +953,9 @@ export class AppointmentService { challengeResponse: string, ): Promise { const log = logger.setContext("AppointmentService"); - log.info("Client deleting appointment with authentication", { + log.debug("Client deleting appointment with authentication", { tenantId: this.tenantId, appointmentId, - emailHashPrefix: emailHash.slice(0, 8), }); // 1. Verify challenge-response @@ -1058,7 +1055,7 @@ export class AppointmentService { // 4. Delete the appointment await db.delete(tenantSchema.appointment).where(eq(tenantSchema.appointment.id, appointmentId)); - log.info("Appointment deleted successfully by client", { + log.debug("Appointment deleted successfully by client", { tenantId: this.tenantId, appointmentId, emailHashPrefix: emailHash.slice(0, 8), @@ -1073,7 +1070,7 @@ export class AppointmentService { }, }); - log.info("Staff notification sent for client-initiated deletion", { + log.debug("Staff notification sent for client-initiated deletion", { tenantId: this.tenantId, appointmentId, channelId: appointment.channelId, diff --git a/src/lib/server/services/central-database-migration-service.ts b/src/lib/server/services/central-database-migration-service.ts index f67316f..0fe4641 100644 --- a/src/lib/server/services/central-database-migration-service.ts +++ b/src/lib/server/services/central-database-migration-service.ts @@ -194,8 +194,6 @@ export class CentralDatabaseMigrationService { logger.info("Central database created and initialized", { database: config.database, - host: config.host, - port: config.port, }); } diff --git a/src/lib/server/services/client-pin-reset-service.ts b/src/lib/server/services/client-pin-reset-service.ts index bee9558..78bc949 100644 --- a/src/lib/server/services/client-pin-reset-service.ts +++ b/src/lib/server/services/client-pin-reset-service.ts @@ -90,8 +90,6 @@ export class ClientPinResetService { .returning({ token: tenantSchema.clientPinResetToken.token }); log.info("PIN reset token created", { - emailHash: emailHash.slice(0, 8), - tokenId: resetToken.token.slice(0, 8), expiresAt, }); @@ -226,9 +224,7 @@ export class ClientPinResetService { .where(eq(tenantSchema.clientPinResetToken.token, token)); log.info("PIN reset completed successfully", { - tokenId: token.slice(0, 8), tunnelId: tunnel.id, - emailHash: emailHash.slice(0, 8), }); return tunnel.id; diff --git a/src/lib/server/services/invite-service.ts b/src/lib/server/services/invite-service.ts index 6a872d4..909d062 100644 --- a/src/lib/server/services/invite-service.ts +++ b/src/lib/server/services/invite-service.ts @@ -39,13 +39,10 @@ export class InviteService { const [createdInvite] = await db.insert(userInvite).values(inviteData).returning(); - logger.info("User invitation created", { + logger.debug("User invitation created", { inviteId: createdInvite.id, - inviteCode: createdInvite.inviteCode, - email: createdInvite.email, tenantId: createdInvite.tenantId, role: createdInvite.role, - invitedBy: createdInvite.invitedBy, }); return createdInvite; @@ -124,7 +121,6 @@ export class InviteService { logger.info("Invitation marked as used", { inviteCode, createdUserId, - email: updatedInvite.email, }); return updatedInvite; diff --git a/src/lib/server/services/staff-crypto.service.ts b/src/lib/server/services/staff-crypto.service.ts index 870b2ed..2419e23 100644 --- a/src/lib/server/services/staff-crypto.service.ts +++ b/src/lib/server/services/staff-crypto.service.ts @@ -97,7 +97,6 @@ export class StaffCryptoService { log.info("Staff keypair stored successfully", { tenantId, userId, - passkeyId, hasPublicKey: !!publicKey, hasPrivateKeyShare: !!privateKeyShare, }); @@ -321,7 +320,6 @@ export class StaffCryptoService { log.info("Staff crypto deletion completed", { tenantId, userId, - passkeyId, deleted, }); diff --git a/src/lib/server/services/staff-service.ts b/src/lib/server/services/staff-service.ts index 970a85e..07d2b27 100644 --- a/src/lib/server/services/staff-service.ts +++ b/src/lib/server/services/staff-service.ts @@ -250,7 +250,7 @@ export class StaffService { deletedKeySharesCount, }; - logger.info("Staff member deleted successfully", { + logger.debug("Staff member deleted successfully", { staffId, tenantId, deletedUser: userDeletionResult.deletedUser, diff --git a/src/lib/server/services/user-service.ts b/src/lib/server/services/user-service.ts index 9fb43be..b9ade13 100644 --- a/src/lib/server/services/user-service.ts +++ b/src/lib/server/services/user-service.ts @@ -660,7 +660,6 @@ export class UserService { log.info("User deleted successfully", { userId, - deletedUser: deletedUsers[0], deletedPasskeysCount, tenantId: user.tenantId, }); diff --git a/src/lib/stores/pin-throttle.ts b/src/lib/stores/pin-throttle.ts index de8677e..e96fb4f 100644 --- a/src/lib/stores/pin-throttle.ts +++ b/src/lib/stores/pin-throttle.ts @@ -36,8 +36,8 @@ const createPinThrottleStore = () => { // Throttle expired, clear storage localStorage.removeItem(STORAGE_KEY); } - } catch (e) { - console.error("Failed to parse throttle state:", e); + } catch (error) { + console.error("Failed to parse throttle state:", error); localStorage.removeItem(STORAGE_KEY); } } diff --git a/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-booked/+server.ts b/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-booked/+server.ts index e1f0b6a..eb7927c 100644 --- a/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-booked/+server.ts +++ b/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-booked/+server.ts @@ -1,5 +1,5 @@ import AppointmentBooked from "$lib/emails/AppointmentBooked.svelte"; -import { renderOutputToHtml, htmlToText } from "$lib/emails/utils"; +import { renderOutputToHtml } from "$lib/emails/utils"; import type { SelectTenant } from "$lib/server/db/central-schema"; import type { SelectAppointment } from "$lib/server/db/tenant-schema"; import type { RequestHandler } from "@sveltejs/kit"; @@ -30,10 +30,6 @@ export const GET: RequestHandler = async () => { }, }); const html = renderOutputToHtml(emailRender); - const text = htmlToText(html); - - // Output for testing purposes - console.log(text); return new Response(html, { headers: { "Content-Type": "text/html", diff --git a/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-reminder/+server.ts b/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-reminder/+server.ts index fcb1fbf..62eb0e9 100644 --- a/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-reminder/+server.ts +++ b/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-reminder/+server.ts @@ -1,5 +1,5 @@ import AppointmentReminder from "$lib/emails/AppointmentReminder.svelte"; -import { renderOutputToHtml, htmlToText } from "$lib/emails/utils"; +import { renderOutputToHtml } from "$lib/emails/utils"; import type { SelectTenant } from "$lib/server/db/central-schema"; import type { SelectAppointment } from "$lib/server/db/tenant-schema"; import type { RequestHandler } from "@sveltejs/kit"; @@ -30,10 +30,6 @@ export const GET: RequestHandler = async () => { }, }); const html = renderOutputToHtml(emailRender); - const text = htmlToText(html); - - // Output for testing purposes - console.log(text); return new Response(html, { headers: { "Content-Type": "text/html", diff --git a/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-request/+server.ts b/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-request/+server.ts index 86914e3..823fabe 100644 --- a/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-request/+server.ts +++ b/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-request/+server.ts @@ -1,5 +1,5 @@ import AppointmentRequested from "$lib/emails/AppointmentRequest.svelte"; -import { renderOutputToHtml, htmlToText } from "$lib/emails/utils"; +import { renderOutputToHtml } from "$lib/emails/utils"; import type { SelectTenant } from "$lib/server/db/central-schema"; import type { SelectAppointment } from "$lib/server/db/tenant-schema"; import type { RequestHandler } from "@sveltejs/kit"; @@ -29,10 +29,6 @@ export const GET: RequestHandler = async () => { }, }); const html = renderOutputToHtml(emailRender); - const text = htmlToText(html); - - // Output for testing purposes - console.log(text); return new Response(html, { headers: { "Content-Type": "text/html", diff --git a/src/routes/(local-only)/[test=dev]/e-mail-templates/confirmation/+server.ts b/src/routes/(local-only)/[test=dev]/e-mail-templates/confirmation/+server.ts index ce1aed6..2e3d817 100644 --- a/src/routes/(local-only)/[test=dev]/e-mail-templates/confirmation/+server.ts +++ b/src/routes/(local-only)/[test=dev]/e-mail-templates/confirmation/+server.ts @@ -1,5 +1,5 @@ import Confirmation from "$lib/emails/Confirmation.svelte"; -import { htmlToText, renderOutputToHtml } from "$lib/emails/utils"; +import { renderOutputToHtml } from "$lib/emails/utils"; import type { RequestHandler } from "@sveltejs/kit"; import { render } from "svelte/server"; @@ -17,10 +17,6 @@ export const GET: RequestHandler = async () => { }, }); const html = renderOutputToHtml(emailRender); - const text = htmlToText(html); - - // Output for testing purposes - console.log(text); return new Response(html, { headers: { "Content-Type": "text/html", diff --git a/src/routes/(local-only)/[test=dev]/e-mail-templates/pin-reset/+server.ts b/src/routes/(local-only)/[test=dev]/e-mail-templates/pin-reset/+server.ts index 327f7f3..9af62da 100644 --- a/src/routes/(local-only)/[test=dev]/e-mail-templates/pin-reset/+server.ts +++ b/src/routes/(local-only)/[test=dev]/e-mail-templates/pin-reset/+server.ts @@ -1,5 +1,5 @@ import PinReset from "$lib/emails/PinReset.svelte"; -import { renderOutputToHtml, htmlToText } from "$lib/emails/utils"; +import { renderOutputToHtml } from "$lib/emails/utils"; import type { SelectTenant } from "$lib/server/db/central-schema"; import type { RequestHandler } from "@sveltejs/kit"; import { render } from "svelte/server"; @@ -17,10 +17,7 @@ export const GET: RequestHandler = async () => { }, }); const html = renderOutputToHtml(emailRender); - const text = htmlToText(html); - // Output for testing purposes - console.log(text); return new Response(html, { headers: { "Content-Type": "text/html", diff --git a/src/routes/(local-only)/[test=dev]/e-mail-templates/user-invite/+server.ts b/src/routes/(local-only)/[test=dev]/e-mail-templates/user-invite/+server.ts index 15d9f58..bc111c6 100644 --- a/src/routes/(local-only)/[test=dev]/e-mail-templates/user-invite/+server.ts +++ b/src/routes/(local-only)/[test=dev]/e-mail-templates/user-invite/+server.ts @@ -1,5 +1,5 @@ import UserInvite from "$lib/emails/UserInvite.svelte"; -import { htmlToText, renderOutputToHtml } from "$lib/emails/utils"; +import { renderOutputToHtml } from "$lib/emails/utils"; import type { SelectTenant } from "$lib/server/db/central-schema"; import type { RequestHandler } from "@sveltejs/kit"; import { render } from "svelte/server"; @@ -19,10 +19,6 @@ export const GET: RequestHandler = async () => { }, }); const html = renderOutputToHtml(emailRender); - const text = htmlToText(html); - - // Output for testing purposes - console.log(text); return new Response(html, { headers: { "Content-Type": "text/html", diff --git a/src/routes/(pages)/confirm/setup-passkey/setup-passkey-form.svelte b/src/routes/(pages)/confirm/setup-passkey/setup-passkey-form.svelte index 0af77cf..58fadcb 100644 --- a/src/routes/(pages)/confirm/setup-passkey/setup-passkey-form.svelte +++ b/src/routes/(pages)/confirm/setup-passkey/setup-passkey-form.svelte @@ -148,14 +148,12 @@ prfOutput = prfOutputResp; logger.info("PRF output retrieved successfully", { - passkeyId: passkeyResp.id, prfOutputLength: prfOutputResp.byteLength, }); } catch (error) { $passkeyLoading = "error"; logger.error("Failed to get PRF output", { email: $formData.email, - passkeyId: passkeyResp.id, error, }); toast.error(m["setupPasskey.errorGettingPrfOutput"]()); diff --git a/src/routes/(pages)/dashboard/calendar/(components)/AppointmentPreview.svelte b/src/routes/(pages)/dashboard/calendar/(components)/AppointmentPreview.svelte index 0a5e145..41afaeb 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/AppointmentPreview.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/AppointmentPreview.svelte @@ -74,7 +74,7 @@ staffKeyShare: item.appointment.staffKeyShare, }); } catch (err) { - console.error("Error decrypting appointment:", err); + console.error("Error decrypting appointment:", item.id); error = err instanceof Error ? err.message : "Decryption failed"; } }; diff --git a/src/routes/(pages)/login/login-form.svelte b/src/routes/(pages)/login/login-form.svelte index a2db400..5d28a28 100644 --- a/src/routes/(pages)/login/login-form.svelte +++ b/src/routes/(pages)/login/login-form.svelte @@ -137,13 +137,11 @@ if (credentialResp.prfOutput) { prfOutputBase64 = arrayBufferToBase64(credentialResp.prfOutput); logger.info("PRF output retrieved from login", { - passkeyId, prfOutputLength: credentialResp.prfOutput.byteLength, }); } else { logger.warn("No PRF output in login response - crypto features may not work", { email: $formData.email, - passkeyId, }); } diff --git a/src/routes/api/auth/invite/+server.ts b/src/routes/api/auth/invite/+server.ts index 6f99750..835f499 100644 --- a/src/routes/api/auth/invite/+server.ts +++ b/src/routes/api/auth/invite/+server.ts @@ -206,8 +206,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { // Send invitation email await sendUserInviteEmail(email, name, tenant, role, registrationUrl, language); - logger.info("User invitation sent successfully", { - invitedBy: locals.user.id, + logger.debug("User invitation sent successfully", { invitedEmail: email, tenantId, role, diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts index 67a43c0..2a8016e 100644 --- a/src/routes/api/auth/login/+server.ts +++ b/src/routes/api/auth/login/+server.ts @@ -284,7 +284,6 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress, logger.info("Login successful", { userId: sessionData.user.id, - email: sessionData.user.email, role: sessionData.user.role, authMethod: body.passphrase ? "passphrase" : "webauthn", }); diff --git a/src/routes/api/auth/passkeys/[passkeyId]/+server.ts b/src/routes/api/auth/passkeys/[passkeyId]/+server.ts index beba00b..29fa831 100644 --- a/src/routes/api/auth/passkeys/[passkeyId]/+server.ts +++ b/src/routes/api/auth/passkeys/[passkeyId]/+server.ts @@ -149,7 +149,6 @@ export async function DELETE({ params, locals }: RequestEvent) { log.info("Passkey deleted successfully", { passkeyId, userId, - deviceName: deletedPasskey.deviceName, }); // If user has a tenant (STAFF or TENANT_ADMIN), also delete associated crypto data (CASCADE) @@ -165,7 +164,6 @@ export async function DELETE({ params, locals }: RequestEvent) { ); log.info("Staff crypto data cascaded deletion completed", { - passkeyId, userId, tenantId, deleted, diff --git a/src/routes/api/auth/passkeys/[passkeyId]/crypto/+server.ts b/src/routes/api/auth/passkeys/[passkeyId]/crypto/+server.ts index eb5829c..6bb65bb 100644 --- a/src/routes/api/auth/passkeys/[passkeyId]/crypto/+server.ts +++ b/src/routes/api/auth/passkeys/[passkeyId]/crypto/+server.ts @@ -262,11 +262,9 @@ export async function POST({ request, params, locals }: RequestEvent) { privateKeyShare, ); - log.info("Crypto keys stored successfully", { - passkeyId, + log.debug("Crypto keys stored successfully", { userId, tenantId, - prfHash: prfHash.substring(0, 16) + "...", }); return json({ diff --git a/src/routes/api/env/+server.ts b/src/routes/api/env/+server.ts index 28465df..9ba219d 100644 --- a/src/routes/api/env/+server.ts +++ b/src/routes/api/env/+server.ts @@ -11,8 +11,6 @@ export async function GET() { envOkay = false; if (!process.env.DATABASE_URL?.startsWith("postgres:")) envOkay = false; - console.warn("Environment is okay", envOkay); - return json({ envOkay, }); diff --git a/src/routes/api/public/utils.ts b/src/routes/api/public/utils.ts index 471b1a1..bf9ee9b 100644 --- a/src/routes/api/public/utils.ts +++ b/src/routes/api/public/utils.ts @@ -1,4 +1,5 @@ import { dev } from "$app/environment"; +import logger from "$lib/logger"; import { db } from "$lib/server/db"; import { tenant } from "$lib/server/db/central-schema"; import { eq } from "drizzle-orm"; @@ -30,7 +31,9 @@ export const getTenantIdByDomain = async ( .limit(1); return tenants[0]?.id || null; } catch (error) { - console.log("error", error); + logger + .setContext("API.Public.Utils") + .error("Failed to get tenant ID by domain", { domain, error }); throw error; } } diff --git a/src/routes/api/tenants/[id]/+server.ts b/src/routes/api/tenants/[id]/+server.ts index 57fe6de..6aa0942 100644 --- a/src/routes/api/tenants/[id]/+server.ts +++ b/src/routes/api/tenants/[id]/+server.ts @@ -497,7 +497,7 @@ export const DELETE: RequestHandler = async ({ params, locals }) => { throw new ValidationError(ERRORS.TENANTS.NO_TENANT_ID); } - log.info("Attempting tenant deletion", { + log.debug("Attempting tenant deletion", { tenantId, requestedBy: locals.user?.id, userRole: locals.user?.role, diff --git a/src/routes/api/tenants/[id]/appointments/[appointmentId]/delete-by-client/+server.ts b/src/routes/api/tenants/[id]/appointments/[appointmentId]/delete-by-client/+server.ts index e7e6866..0d200dd 100644 --- a/src/routes/api/tenants/[id]/appointments/[appointmentId]/delete-by-client/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/[appointmentId]/delete-by-client/+server.ts @@ -133,7 +133,7 @@ export const DELETE: RequestHandler = async ({ request, params }) => { const body = await request.json(); const { emailHash, challengeId, challengeResponse } = requestSchema.parse(body); - log.info("Client deleting appointment", { + log.debug("Client deleting appointment", { tenantId, appointmentId, emailHashPrefix: emailHash.slice(0, 8), @@ -148,10 +148,9 @@ export const DELETE: RequestHandler = async ({ request, params }) => { challengeResponse, ); - log.info("Appointment deleted successfully by client", { + log.debug("Appointment deleted successfully by client", { tenantId, appointmentId, - emailHashPrefix: emailHash.slice(0, 8), }); return json({ diff --git a/src/routes/api/tenants/[id]/appointments/[appointmentId]/deny/+server.ts b/src/routes/api/tenants/[id]/appointments/[appointmentId]/deny/+server.ts index d13b755..95cf268 100644 --- a/src/routes/api/tenants/[id]/appointments/[appointmentId]/deny/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/[appointmentId]/deny/+server.ts @@ -123,14 +123,13 @@ export const POST: RequestHandler = async ({ params, request, locals }) => { const body = await request.json(); const { clientEmail, clientLanguage } = requestSchema.parse(body); - log.info("Denying appointment", { + log.debug("Denying appointment", { tenantId, appointmentId, - clientEmailPrefix: clientEmail ? clientEmail.slice(0, 3) : undefined, }); await appointmentService.denyAppointment(appointmentId, clientEmail, clientLanguage); - log.info("Appointment denied successfully", { + log.debug("Appointment denied successfully", { tenantId, appointmentId, }); diff --git a/src/routes/api/tenants/[id]/appointments/[appointmentId]/deny/__tests__/deny-appointment-api.test.ts b/src/routes/api/tenants/[id]/appointments/[appointmentId]/deny/__tests__/deny-appointment-api.test.ts index 1c98f11..9341412 100644 --- a/src/routes/api/tenants/[id]/appointments/[appointmentId]/deny/__tests__/deny-appointment-api.test.ts +++ b/src/routes/api/tenants/[id]/appointments/[appointmentId]/deny/__tests__/deny-appointment-api.test.ts @@ -16,6 +16,7 @@ vi.mock("$lib/server/utils/permissions", () => ({ vi.mock("$lib/logger", () => ({ default: { setContext: vi.fn(() => ({ + debug: vi.fn(), info: vi.fn(), error: vi.fn(), })), diff --git a/src/routes/api/tenants/[id]/appointments/add-to-tunnel/+server.ts b/src/routes/api/tenants/[id]/appointments/add-to-tunnel/+server.ts index 9a7bbd7..5259796 100644 --- a/src/routes/api/tenants/[id]/appointments/add-to-tunnel/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/add-to-tunnel/+server.ts @@ -208,11 +208,10 @@ export const POST: RequestHandler = async ({ request, params }) => { throw new ValidationError("Bees incoming"); } - logger.info("Adding appointment to existing tunnel", { + logger.debug("Adding appointment to existing tunnel", { tenantId, tunnelId: validatedData.tunnelId, appointmentDate: validatedData.appointmentDate, - emailHashPrefix: validatedData.emailHash.slice(0, 8), }); const db = await getTenantDb(tenantId); @@ -230,7 +229,6 @@ export const POST: RequestHandler = async ({ request, params }) => { logger.warn("Client tunnel not found", { tenantId, tunnelId: validatedData.tunnelId, - emailHashPrefix: validatedData.emailHash.slice(0, 8), }); throw new NotFoundError("Tunnel not found or access denied"); } @@ -335,14 +333,14 @@ export const POST: RequestHandler = async ({ request, params }) => { if (requiresConfirmation) { await sendAppointmentRequestEmail(clientData, tenant, result, channelTitle); - logger.info("Appointment request email sent", { + logger.debug("Appointment request email sent", { tunnelId: validatedData.tunnelId, appointmentId: result.id, tenantId, }); } else { await sendAppointmentCreatedEmail(clientData, tenant, result, channelTitle); - logger.info("Appointment confirmation email sent", { + logger.debug("Appointment confirmation email sent", { tunnelId: validatedData.tunnelId, appointmentId: result.id, tenantId, @@ -352,7 +350,6 @@ export const POST: RequestHandler = async ({ request, params }) => { } } catch (emailError) { logger.error("Failed to send appointment notification email", { - tunnelId: validatedData.tunnelId, appointmentId: result.id, tenantId, error: String(emailError), diff --git a/src/routes/api/tenants/[id]/appointments/challenge/+server.ts b/src/routes/api/tenants/[id]/appointments/challenge/+server.ts index 0373dbd..8d7de69 100644 --- a/src/routes/api/tenants/[id]/appointments/challenge/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/challenge/+server.ts @@ -144,7 +144,6 @@ export const POST: RequestHandler = async ({ request, params }) => { if (!throttleResult.allowed) { logger.warn("PIN challenge throttled", { tenantId, - emailHashPrefix: emailHash.slice(0, 8), retryAfterMs: throttleResult.retryAfterMs, failedAttempts: throttleResult.failedAttempts, }); @@ -163,7 +162,7 @@ export const POST: RequestHandler = async ({ request, params }) => { ); } - logger.info("Creating challenge for existing client", { + logger.debug("Creating challenge for existing client", { tenantId, emailHashPrefix: emailHash.slice(0, 8), }); @@ -184,7 +183,6 @@ export const POST: RequestHandler = async ({ request, params }) => { if (tunnelResult.length === 0) { logger.warn("Client tunnel not found for challenge", { tenantId, - emailHashPrefix: emailHash.slice(0, 8), }); return json({ error: "Client not found" }, { status: 404 }); } @@ -223,7 +221,7 @@ export const POST: RequestHandler = async ({ request, params }) => { privateKeyShare: tunnel.privateKeyShare, }; - logger.info("Successfully created challenge", { + logger.debug("Successfully created challenge", { tenantId, tunnelId: tunnel.id, challengeLength: challenge.length, diff --git a/src/routes/api/tenants/[id]/appointments/create-new-client/+server.ts b/src/routes/api/tenants/[id]/appointments/create-new-client/+server.ts index 02fec3d..da033d4 100644 --- a/src/routes/api/tenants/[id]/appointments/create-new-client/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/create-new-client/+server.ts @@ -233,12 +233,9 @@ export const POST: RequestHandler = async ({ request, params }) => { throw new ValidationError("Bees incoming"); } - logger.info("Creating new client appointment tunnel", { + logger.debug("Creating new client appointment tunnel", { tenantId, tunnelId: validatedData.tunnelId, - appointmentDate: validatedData.appointmentDate, - duration: validatedData.duration, - emailHashPrefix: validatedData.emailHash.slice(0, 8), }); const appointmentService = await AppointmentService.forTenant(tenantId); diff --git a/src/routes/api/tenants/[id]/appointments/create-new-client/__tests__/create-new-client-api.test.ts b/src/routes/api/tenants/[id]/appointments/create-new-client/__tests__/create-new-client-api.test.ts index 1bd3168..ffe01cb 100644 --- a/src/routes/api/tenants/[id]/appointments/create-new-client/__tests__/create-new-client-api.test.ts +++ b/src/routes/api/tenants/[id]/appointments/create-new-client/__tests__/create-new-client-api.test.ts @@ -13,6 +13,7 @@ vi.mock("$lib/server/services/appointment-service", () => ({ vi.mock("$lib/logger", () => ({ logger: { + debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), @@ -96,12 +97,9 @@ describe("Create New Client API Route", () => { expect(response.status).toBe(200); expect(data).toEqual(mockAppointment); expect(mockService.createNewClientWithAppointment).toHaveBeenCalledWith(validRequestBody); - expect(logger.info).toHaveBeenCalledWith("Creating new client appointment tunnel", { + expect(logger.debug).toHaveBeenCalledWith("Creating new client appointment tunnel", { tenantId: mockTenantId, tunnelId: mockTunnelId, - appointmentDate: validRequestBody.appointmentDate, - duration: validRequestBody.duration, - emailHashPrefix: "test-ema", }); }); @@ -195,12 +193,9 @@ describe("Create New Client API Route", () => { expect(data.error).toBe( "Cannot create client appointments: No authorized users found in tenant", ); - expect(logger.info).toHaveBeenCalledWith("Creating new client appointment tunnel", { + expect(logger.debug).toHaveBeenCalledWith("Creating new client appointment tunnel", { tenantId: mockTenantId, tunnelId: mockTunnelId, - appointmentDate: validRequestBody.appointmentDate, - duration: validRequestBody.duration, - emailHashPrefix: "test-ema", }); }); @@ -256,12 +251,9 @@ describe("Create New Client API Route", () => { const event = createMockRequestEvent(); await POST(event); - expect(logger.info).toHaveBeenCalledWith("Creating new client appointment tunnel", { + expect(logger.debug).toHaveBeenCalledWith("Creating new client appointment tunnel", { tenantId: mockTenantId, tunnelId: mockTunnelId, - appointmentDate: validRequestBody.appointmentDate, - duration: 10, - emailHashPrefix: "test-ema", // First 8 chars of "test-email-hash" }); }); diff --git a/src/routes/api/tenants/[id]/appointments/staff-create/+server.ts b/src/routes/api/tenants/[id]/appointments/staff-create/+server.ts index ea4335e..e31dbe7 100644 --- a/src/routes/api/tenants/[id]/appointments/staff-create/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/staff-create/+server.ts @@ -376,7 +376,6 @@ export const POST: RequestHandler = async ({ params, request, locals }) => { logger.info("PIN reset token created for new client", { tenantId, - tokenId: pinResetToken.slice(0, 8), }); } catch (error) { logger.error("Failed to create PIN reset token for new client", { diff --git a/src/routes/api/tenants/[id]/appointments/tunnels/add-staff-key-shares/+server.ts b/src/routes/api/tenants/[id]/appointments/tunnels/add-staff-key-shares/+server.ts index 979e8d4..da17ac5 100644 --- a/src/routes/api/tenants/[id]/appointments/tunnels/add-staff-key-shares/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/tunnels/add-staff-key-shares/+server.ts @@ -246,8 +246,6 @@ export const POST: RequestHandler = async ({ params, locals, request }) => { log.info("No new key shares to add - all already exist", { tenantId, staffUserId, - totalRequested: keyShares.length, - previousConfirmationState: staffUser[0].confirmationState, }); await ensureAccessGranted(); @@ -284,12 +282,11 @@ export const POST: RequestHandler = async ({ params, locals, request }) => { await ensureAccessGranted(); - log.info("Staff key shares added successfully", { + log.debug("Staff key shares added successfully", { tenantId, staffUserId, addedCount: result.length, skippedCount: duplicateKeyShares.length, - requesterId: locals.user?.id, }); return json({ diff --git a/src/routes/api/tenants/[id]/appointments/verify-challenge/+server.ts b/src/routes/api/tenants/[id]/appointments/verify-challenge/+server.ts index ef50f47..92fd4ed 100644 --- a/src/routes/api/tenants/[id]/appointments/verify-challenge/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/verify-challenge/+server.ts @@ -133,7 +133,7 @@ export const POST: RequestHandler = async ({ request, params }) => { const body = await request.json(); const { challengeId, challengeResponse } = requestSchema.parse(body); - logger.info("Verifying challenge for existing client", { + logger.debug("Verifying challenge for existing client", { tenantId, challengeId, }); @@ -160,7 +160,6 @@ export const POST: RequestHandler = async ({ request, params }) => { logger.warn("Challenge response mismatch", { tenantId, challengeId, - emailHashPrefix: storedChallenge.emailHash.slice(0, 8), }); // Record failed attempt for throttling @@ -185,14 +184,13 @@ export const POST: RequestHandler = async ({ request, params }) => { logger.warn("Client tunnel not found for verification", { tenantId, challengeId, - emailHashPrefix: storedChallenge.emailHash.slice(0, 8), }); throw new NotFoundError("Client not found"); } const tunnel = tunnelResult[0]; - logger.info("Challenge response validated successfully", { + logger.debug("Challenge response validated successfully", { tenantId, challengeId, tunnelId: tunnel.id, @@ -207,7 +205,7 @@ export const POST: RequestHandler = async ({ request, params }) => { tunnelId: tunnel.id, }; - logger.info("Successfully verified challenge", { + logger.debug("Successfully verified challenge", { tenantId, tunnelId: tunnel.id, }); diff --git a/src/routes/api/tenants/[id]/clients/pin-reset/complete/+server.ts b/src/routes/api/tenants/[id]/clients/pin-reset/complete/+server.ts index a85c7f3..a424858 100644 --- a/src/routes/api/tenants/[id]/clients/pin-reset/complete/+server.ts +++ b/src/routes/api/tenants/[id]/clients/pin-reset/complete/+server.ts @@ -157,10 +157,9 @@ export const POST: RequestHandler = async ({ params, request }) => { validatedData.newClientEncryptedTunnelKey, ); - logger.info("PIN reset completed successfully", { + logger.debug("PIN reset completed successfully", { tenantId, tunnelId, - tokenId: validatedData.token.slice(0, 8), }); return json({ diff --git a/src/routes/api/tenants/[id]/clients/pin-reset/init/+server.ts b/src/routes/api/tenants/[id]/clients/pin-reset/init/+server.ts index be2a379..4abc037 100644 --- a/src/routes/api/tenants/[id]/clients/pin-reset/init/+server.ts +++ b/src/routes/api/tenants/[id]/clients/pin-reset/init/+server.ts @@ -152,10 +152,9 @@ export const POST: RequestHandler = async ({ params, request, locals }) => { const expiresAt = new Date(Date.now() + expirationMinutes * 60 * 1000); - logger.info("PIN reset token created for QR code", { + logger.debug("PIN reset token created for QR code", { tenantId, emailHash: validatedData.emailHash.slice(0, 8), - tokenId: token.slice(0, 8), initiatedBy: locals.user?.id, }); diff --git a/src/routes/api/tenants/[id]/clients/pin-reset/request/+server.ts b/src/routes/api/tenants/[id]/clients/pin-reset/request/+server.ts index 39c96e6..cac5372 100644 --- a/src/routes/api/tenants/[id]/clients/pin-reset/request/+server.ts +++ b/src/routes/api/tenants/[id]/clients/pin-reset/request/+server.ts @@ -166,12 +166,10 @@ export const POST: RequestHandler = async ({ params, request, locals }) => { const baseUrl = env.PUBLIC_APP_URL || "http://localhost:5173"; const resetUrl = `${baseUrl}/reset-pin/${token}`; - logger.info("PIN reset token created for email", { + logger.debug("PIN reset token created for email", { tenantId, emailHash: validatedData.emailHash.slice(0, 8), - tokenId: token.slice(0, 8), initiatedBy: locals.user?.id, - resetUrl, }); // TODO: Send actual email once we have a way to get the client's email diff --git a/src/routes/api/tenants/[id]/notifications/+server.ts b/src/routes/api/tenants/[id]/notifications/+server.ts index 44d087a..01ed7de 100644 --- a/src/routes/api/tenants/[id]/notifications/+server.ts +++ b/src/routes/api/tenants/[id]/notifications/+server.ts @@ -243,7 +243,7 @@ export const DELETE: RequestHandler = async ({ params, url, locals }) => { const notificationService = await NotificationService.forTenant(tenantId); const deletedCount = await notificationService.deleteAllNotifications(locals.user.id, readOnly); - log.info("Notifications deleted successfully", { + log.debug("Notifications deleted successfully", { tenantId, staffId: locals.user.id, deletedCount, diff --git a/src/routes/api/tenants/[id]/notifications/[notificationId]/+server.ts b/src/routes/api/tenants/[id]/notifications/[notificationId]/+server.ts index 93678e1..082f944 100644 --- a/src/routes/api/tenants/[id]/notifications/[notificationId]/+server.ts +++ b/src/routes/api/tenants/[id]/notifications/[notificationId]/+server.ts @@ -182,7 +182,7 @@ export const DELETE: RequestHandler = async ({ params, locals }) => { const notificationService = await NotificationService.forTenant(tenantId); await notificationService.deleteNotification(notificationId, locals.user.id); - log.info("Notification deleted successfully", { + log.debug("Notification deleted successfully", { tenantId, staffId: locals.user.id, notificationId, @@ -233,7 +233,7 @@ export const PUT: RequestHandler = async ({ params, locals }) => { const notificationService = await NotificationService.forTenant(tenantId); await notificationService.markAsRead(notificationId, locals.user.id); - log.info("Notification marked as read successfully", { + log.debug("Notification marked as read successfully", { tenantId, staffId: locals.user.id, notificationId, diff --git a/src/routes/api/tenants/[id]/staff/[staffId]/crypto/+server.ts b/src/routes/api/tenants/[id]/staff/[staffId]/crypto/+server.ts index beed199..11edfef 100644 --- a/src/routes/api/tenants/[id]/staff/[staffId]/crypto/+server.ts +++ b/src/routes/api/tenants/[id]/staff/[staffId]/crypto/+server.ts @@ -131,7 +131,6 @@ export const POST: RequestHandler = async ({ params, locals, request, cookies }) const validation = requestSchema.safeParse(body); if (!validation.success) { - console.error("Validation error:", validation.error.issues); throw new ValidationError( "Invalid request data: " + validation.error.issues.map((e) => e.message).join(", "), ); @@ -174,7 +173,6 @@ export const POST: RequestHandler = async ({ params, locals, request, cookies }) log.info("Staff crypto keys stored successfully", { tenantId, staffId, - passkeyId, }); return json({ diff --git a/src/server-hooks/apiAuthHandle.ts b/src/server-hooks/apiAuthHandle.ts index ac5f20a..b75bdb0 100644 --- a/src/server-hooks/apiAuthHandle.ts +++ b/src/server-hooks/apiAuthHandle.ts @@ -91,7 +91,7 @@ export const apiAuthHandle: Handle = async ({ event, resolve }) => { // Protected auth paths require any authenticated user (no specific role required) // The authentication check above is sufficient - logger.debug(`User authenticated: ${sessionData?.user.email ?? "unauthenticated"} for ${path}`); + logger.debug(`User authenticated: ${sessionData?.user.id ?? "unauthenticated"} for ${path}`); return resolve(event); }; From c710142295f8ca97d03001c2b61f9940555082ea Mon Sep 17 00:00:00 2001 From: Hendrik Date: Wed, 11 Mar 2026 06:20:22 +0100 Subject: [PATCH 7/9] Do not apply shortname for system tenant generateBaseUrl (#223) * Do not apply shortname for system tenant generateBaseUrl Do not remove domain name parts if hostname already is a subdomain * Use domain instead of shortName for tenant domain * Fixed tests --- .../email/__tests__/generate-base-url.test.ts | 22 +++++++++---------- src/lib/server/email/email-service.ts | 16 ++++---------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/lib/server/email/__tests__/generate-base-url.test.ts b/src/lib/server/email/__tests__/generate-base-url.test.ts index af384a9..a34d581 100644 --- a/src/lib/server/email/__tests__/generate-base-url.test.ts +++ b/src/lib/server/email/__tests__/generate-base-url.test.ts @@ -143,6 +143,7 @@ describe("generateBaseUrl", () => { const tenant: SelectTenant = { id: "tenant-1", shortName: "acme", + domain: "acme", longName: "ACME Corp", descriptions: { en: "" }, languages: ["en"], @@ -151,7 +152,6 @@ describe("generateBaseUrl", () => { setupState: "SETTINGS", logo: null, links: { website: "", imprint: "", privacyStatement: "" }, - domain: "tenant.example.com", createdAt: new Date(), updatedAt: new Date(), }; @@ -173,7 +173,7 @@ describe("generateBaseUrl", () => { setupState: "SETTINGS", logo: null, links: { website: "", imprint: "", privacyStatement: "" }, - domain: "tenant.example.com", + domain: "acme", createdAt: new Date(), updatedAt: new Date(), }; @@ -195,13 +195,13 @@ describe("generateBaseUrl", () => { setupState: "SETTINGS", logo: null, links: { website: "", imprint: "", privacyStatement: "" }, - domain: "tenant.example.com", + domain: "new-tenant", createdAt: new Date(), updatedAt: new Date(), }; const result = generateBaseUrl(requestUrl, tenant); - expect(result).toBe("https://new-tenant.example.com"); + expect(result).toBe("https://new-tenant.old-tenant.example.com"); }); it("should replace existing subdomain with port", () => { @@ -217,13 +217,13 @@ describe("generateBaseUrl", () => { setupState: "SETTINGS", logo: null, links: { website: "", imprint: "", privacyStatement: "" }, - domain: "tenant.example.com", + domain: "new-tenant", createdAt: new Date(), updatedAt: new Date(), }; const result = generateBaseUrl(requestUrl, tenant); - expect(result).toBe("https://new-tenant.example.com:8443"); + expect(result).toBe("https://new-tenant.old-tenant.example.com:8443"); }); it("should handle complex subdomains (keep last two parts)", () => { @@ -239,13 +239,13 @@ describe("generateBaseUrl", () => { setupState: "SETTINGS", logo: null, links: { website: "", imprint: "", privacyStatement: "" }, - domain: "tenant.example.com", + domain: "tenant", createdAt: new Date(), updatedAt: new Date(), }; const result = generateBaseUrl(requestUrl, tenant); - expect(result).toBe("https://tenant.example.com"); + expect(result).toBe("https://tenant.admin.api.example.com"); }); it("should handle http protocol", () => { @@ -261,7 +261,7 @@ describe("generateBaseUrl", () => { setupState: "SETTINGS", logo: null, links: { website: "", imprint: "", privacyStatement: "" }, - domain: "tenant.example.com", + domain: "acme", createdAt: new Date(), updatedAt: new Date(), }; @@ -283,7 +283,7 @@ describe("generateBaseUrl", () => { setupState: "SETTINGS", logo: null, links: { website: "", imprint: "", privacyStatement: "" }, - domain: "tenant.example.com", + domain: "", createdAt: new Date(), updatedAt: new Date(), }; @@ -308,7 +308,7 @@ describe("generateBaseUrl", () => { setupState: "SETTINGS", logo: null, links: { website: "", imprint: "", privacyStatement: "" }, - domain: "tenant.example.com", + domain: "acme", createdAt: new Date(), updatedAt: new Date(), }; diff --git a/src/lib/server/email/email-service.ts b/src/lib/server/email/email-service.ts index 7426e36..2c57f3a 100644 --- a/src/lib/server/email/email-service.ts +++ b/src/lib/server/email/email-service.ts @@ -409,18 +409,10 @@ export function generateBaseUrl(requestUrl: URL, tenant: SelectTenant | null): s return `${protocol}//${hostname}${port}`; } - // In production, handle tenant subdomains - if (tenant?.shortName) { - const parts = hostname.split("."); - - if (parts.length > 2) { - // Complex subdomain - use only the last two parts (domain.tld) and add tenant - const domain = parts.slice(-2).join("."); - return `${protocol}//${tenant.shortName}.${domain}${port}`; - } else { - // Main domain, prepend tenant subdomain - return `${protocol}//${tenant.shortName}.${hostname}${port}`; - } + // In production, handle tenant subdomains. + // Exclude the system tenant when determining if we should use the tenant's domain for the URL, as the system tenant does not have a domain and should use the main domain. + if (tenant?.domain && tenant.id !== "system") { + return `${protocol}//${tenant.domain}.${hostname}${port}`; } // For global admin or no tenant, use main domain From 010855efa8b7018e3c8efa9b2a42295d3d760d45 Mon Sep 17 00:00:00 2001 From: Karl Ludwig Weise Date: Wed, 11 Mar 2026 11:58:59 +0100 Subject: [PATCH 8/9] Added deployment docs (#228) * Added deployment docs * Fix lint --------- Co-authored-by: Karl Ludwig Weise --- Caddyfile | 26 +-------------- docker-compose.prod.yml | 72 ++++++++++++++++++++++++----------------- docs/deployment.md | 29 +++++++++++++++++ 3 files changed, 72 insertions(+), 55 deletions(-) create mode 100644 docs/deployment.md diff --git a/Caddyfile b/Caddyfile index cad756b..4a3c3a8 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,27 +1,14 @@ # Production Caddyfile for appointment booking application -# Replace your-domain.com with your actual domain +# Replace your-admin-domain.com with your actual admin domain { on_demand_tls { ask http://app:3000/api/public/domains - burst 5 - interval 60s } } :443 { reverse_proxy app:3000 - # Security headers - header { - -Server - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" - X-Content-Type-Options "nosniff" - X-Frame-Options "DENY" - X-XSS-Protection "1; mode=block" - Referrer-Policy "strict-origin-when-cross-origin" - Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'" - } - encode gzip log { @@ -41,17 +28,6 @@ your-admin-domain.com { reverse_proxy app:3000 - # Security headers - header { - -Server - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" - X-Content-Type-Options "nosniff" - X-Frame-Options "DENY" - X-XSS-Protection "1; mode=block" - Referrer-Policy "strict-origin-when-cross-origin" - Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'" - } - encode gzip log { diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 65cda14..db303b1 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,9 +1,8 @@ services: postgres: image: postgres:16-alpine - container_name: open-reception-postgres + container_name: postgres restart: unless-stopped - user: postgres environment: POSTGRES_DB_FILE: /run/secrets/postgres_db POSTGRES_USER_FILE: /run/secrets/postgres_user @@ -15,7 +14,6 @@ services: - postgres_password volumes: - postgres_data:/var/lib/postgresql/data - - ./init-db:/docker-entrypoint-initdb.d:ro networks: - open-reception-internal healthcheck: @@ -24,10 +22,10 @@ services: "CMD-SHELL", "pg_isready -U $$(cat /run/secrets/postgres_user) -d $$(cat /run/secrets/postgres_db)", ] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s + interval: 5s + timeout: 3s + retries: 10 + start_period: 60s security_opt: - no-new-privileges:true cap_drop: @@ -44,23 +42,25 @@ services: read_only: true app: - image: openreception/open-reception:${VERSION:-latest} - container_name: open-reception-app + # use specific version here to watch for breaking changes + image: openreception/open-reception:latest + container_name: app restart: unless-stopped user: "1001:1001" environment: NODE_ENV: production - SMTP_HOST: /run/secrets/smtp_host - SMTP_PORT: /run/secrets/smtp_port - SMTP_SECURE: /run/secrets/smtp_secure - SMTP_USER: /run/secrets/smtp_user - SMTP_PASS: /run/secrets/smtp_pass - SMTP_FROM_NAME: /run/secrets/smtp_from_name - SMTP_FROM_EMAIL: /run/secrets/smtp_from_email secrets: - postgres_db - postgres_user - postgres_password + - smtp_host + - smtp_port + - smtp_secure + - smtp_user + - smtp_pass + - smtp_from_name + - smtp_from_email + - jwt_secret depends_on: postgres: condition: service_healthy @@ -71,8 +71,8 @@ services: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/api/health"] interval: 30s timeout: 10s - retries: 3 - start_period: 40s + retries: 5 + start_period: 60s security_opt: - no-new-privileges:true cap_drop: @@ -85,26 +85,30 @@ services: export POSTGRES_USER=$$(cat /run/secrets/postgres_user) export POSTGRES_PASSWORD=$$(cat /run/secrets/postgres_password) export DATABASE_URL=\"postgres://$$POSTGRES_USER:$$POSTGRES_PASSWORD@postgres:5432/$$POSTGRES_DB\" + export SMTP_HOST=$$(cat /run/secrets/smtp_host) + export SMTP_PORT=$$(cat /run/secrets/smtp_port) + export SMTP_SECURE=$$(cat /run/secrets/smtp_secure) + export SMTP_USER=$$(cat /run/secrets/smtp_user) + export SMTP_PASS=$$(cat /run/secrets/smtp_pass) + export SMTP_FROM_NAME=$$(cat /run/secrets/smtp_from_name) + export SMTP_FROM_EMAIL=$$(cat /run/secrets/smtp_from_email) + export JWT_SECRET=$$(cat /run/secrets/jwt_secret) exec node build/index.js " read_only: true caddy: - image: caddy:2-alpine - container_name: open-reception-caddy + image: caddy:2 + container_name: caddy restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config + - caddy-data:/data" networks: - open-reception-internal - depends_on: - app: - condition: service_healthy security_opt: - no-new-privileges:true cap_drop: @@ -119,10 +123,10 @@ secrets: file: ./secrets/postgres_user.txt postgres_password: file: ./secrets/postgres_password.txt - smtp_port: - file: ./secrets/smtp_port.txt smtp_host: file: ./secrets/smtp_host.txt + smtp_port: + file: ./secrets/smtp_port.txt smtp_secure: file: ./secrets/smtp_secure.txt smtp_user: @@ -133,14 +137,22 @@ secrets: file: ./secrets/smtp_from_name.txt smtp_from_email: file: ./secrets/smtp_from_email.txt + jwt_secret: + file: ./secrets/jwt_secret.txt volumes: postgres_data: driver: local - caddy_data: - driver: local - caddy_config: + driver_opts: + type: none + o: bind + device: /opt/openreception/postgres + caddy-data: driver: local + driver_opts: + type: "none" + o: "bind" + device: "/opt/openreception/caddy/data" networks: open-reception-internal: diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..20af379 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,29 @@ +# Deploy OpenReception to a single server + +This guide will lead you through setting up OpenReception on linux a server of your choice. + +> Too complicated? Managed Hosting is available at [open-reception.com](https://open-reception.com). + +## Requirements + +This setup requires + +- shell access to a root server +- docker with docker compose installed +- a small server with 2 cores and 2gb of ram should get you started + +## Recommendations/ Considerations + +- Update your operating system on a regular basis. Use automatic security updates like `unattended-upgrades` for the very short term issues. +- Encrypt the drive where your database is stored to prevent data theft by just pulling the hard drive. You can use `luks`. +- Protect access with `fail2ban`, `ufw`, `crowdsec`, proper user permissions and other best-practices. +- Make regular backups of your database and test them regularly. You can use `restic`. +- Monitor your server loads and logs over time to detect peak usages (with insufficient ram or cpu power) and malicious activities. + +## Setup + +1. Use our [production docker-compose example](../docker-compose.prod.yml) +1. Use our [Caddyfile example](../Caddyfile) +1. Adjust the settings in these two files above to your needs. +1. Run `docker-compose up -d` or `docker compose up -d` depending on your docker installation. +1. Proceed to secure your instance by opening your admin domain in a browser. From 4622ad714c88f0f17dedaddf66a66a7013a48cb9 Mon Sep 17 00:00:00 2001 From: Hendrik Date: Thu, 12 Mar 2026 17:08:28 +0100 Subject: [PATCH 9/9] Obfuscate error data from public endpoints (#230) * Do not apply shortname for system tenant generateBaseUrl Do not remove domain name parts if hostname already is a subdomain * Use domain instead of shortName for tenant domain * Fixed tests * Obfuscate error output --- src/lib/server/db/index.ts | 4 ++-- src/lib/server/services/client-pin-reset-service.ts | 4 ++-- .../appointments/[appointmentId]/delete-by-client/+server.ts | 2 +- src/routes/api/tenants/[id]/appointments/challenge/+server.ts | 4 ++-- .../appointments/challenge/__tests__/challenge-api.test.ts | 2 +- .../api/tenants/[id]/appointments/my-appointments/+server.ts | 2 +- .../my-appointments/__tests__/my-appointments.test.ts | 2 +- .../api/tenants/[id]/appointments/verify-challenge/+server.ts | 2 +- .../api/tenants/[id]/clients/pin-reset/complete/+server.ts | 2 +- src/routes/api/tenants/[id]/clients/pin-reset/init/+server.ts | 2 +- .../clients/pin-reset/init/__tests__/pin-reset-init.test.ts | 2 +- 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts index 64e4e54..4830d97 100644 --- a/src/lib/server/db/index.ts +++ b/src/lib/server/db/index.ts @@ -36,7 +36,7 @@ export async function getTenantDb( .limit(1); if (tenant.length === 0) { - throw new Error(`Tenant with ID ${tenantId} not found`); + throw new Error(`Tenant or client not found`); } // Create tenant-specific database connection @@ -62,7 +62,7 @@ export async function getTenant(tenantId: string): Promise { logger.warn("Client tunnel not found for challenge", { tenantId, }); - return json({ error: "Client not found" }, { status: 404 }); + return json({ error: "Tenant or client not found" }, { status: 404 }); } const tunnel = tunnelResult[0]; diff --git a/src/routes/api/tenants/[id]/appointments/challenge/__tests__/challenge-api.test.ts b/src/routes/api/tenants/[id]/appointments/challenge/__tests__/challenge-api.test.ts index 153efc5..36af689 100644 --- a/src/routes/api/tenants/[id]/appointments/challenge/__tests__/challenge-api.test.ts +++ b/src/routes/api/tenants/[id]/appointments/challenge/__tests__/challenge-api.test.ts @@ -174,7 +174,7 @@ describe("Challenge API Route", () => { const data = await response.json(); expect(response.status).toBe(404); - expect(data.error).toBe("Client not found"); + expect(data.error).toBe("Tenant or client not found"); }); it("should handle database errors", async () => { diff --git a/src/routes/api/tenants/[id]/appointments/my-appointments/+server.ts b/src/routes/api/tenants/[id]/appointments/my-appointments/+server.ts index 5059257..9103b92 100644 --- a/src/routes/api/tenants/[id]/appointments/my-appointments/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/my-appointments/+server.ts @@ -171,7 +171,7 @@ export const GET: RequestHandler = async ({ request, params }) => { tenantId, emailHashPrefix: validatedEmailHash.slice(0, 8), }); - throw new ValidationError("Client not found"); + throw new ValidationError("Tenant or client not found"); } const tunnel = tunnelResult[0]; diff --git a/src/routes/api/tenants/[id]/appointments/my-appointments/__tests__/my-appointments.test.ts b/src/routes/api/tenants/[id]/appointments/my-appointments/__tests__/my-appointments.test.ts index 785a58d..3714c21 100644 --- a/src/routes/api/tenants/[id]/appointments/my-appointments/__tests__/my-appointments.test.ts +++ b/src/routes/api/tenants/[id]/appointments/my-appointments/__tests__/my-appointments.test.ts @@ -182,7 +182,7 @@ describe("GET /api/tenants/[id]/appointments/my-appointments", () => { const data = await response.json(); expect(response.status).toBe(422); - expect(data.error).toBe("Client not found"); + expect(data.error).toBe("Tenant or client not found"); }); it("should return 422 when email hash is empty", async () => { diff --git a/src/routes/api/tenants/[id]/appointments/verify-challenge/+server.ts b/src/routes/api/tenants/[id]/appointments/verify-challenge/+server.ts index 92fd4ed..3b3162e 100644 --- a/src/routes/api/tenants/[id]/appointments/verify-challenge/+server.ts +++ b/src/routes/api/tenants/[id]/appointments/verify-challenge/+server.ts @@ -185,7 +185,7 @@ export const POST: RequestHandler = async ({ request, params }) => { tenantId, challengeId, }); - throw new NotFoundError("Client not found"); + throw new NotFoundError("Tenant or client not found"); } const tunnel = tunnelResult[0]; diff --git a/src/routes/api/tenants/[id]/clients/pin-reset/complete/+server.ts b/src/routes/api/tenants/[id]/clients/pin-reset/complete/+server.ts index a424858..3916b92 100644 --- a/src/routes/api/tenants/[id]/clients/pin-reset/complete/+server.ts +++ b/src/routes/api/tenants/[id]/clients/pin-reset/complete/+server.ts @@ -100,7 +100,7 @@ registerOpenAPIRoute("/tenants/{id}/clients/pin-reset/complete", "POST", { }, }, "404": { - description: "Token or client not found", + description: "Tenant or client not found", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" }, diff --git a/src/routes/api/tenants/[id]/clients/pin-reset/init/+server.ts b/src/routes/api/tenants/[id]/clients/pin-reset/init/+server.ts index 4abc037..80e9c15 100644 --- a/src/routes/api/tenants/[id]/clients/pin-reset/init/+server.ts +++ b/src/routes/api/tenants/[id]/clients/pin-reset/init/+server.ts @@ -99,7 +99,7 @@ registerOpenAPIRoute("/tenants/{id}/clients/pin-reset/init", "POST", { }, }, "404": { - description: "Client not found", + description: "Tenant or client not found", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" }, diff --git a/src/routes/api/tenants/[id]/clients/pin-reset/init/__tests__/pin-reset-init.test.ts b/src/routes/api/tenants/[id]/clients/pin-reset/init/__tests__/pin-reset-init.test.ts index a92501f..75bf6e1 100644 --- a/src/routes/api/tenants/[id]/clients/pin-reset/init/__tests__/pin-reset-init.test.ts +++ b/src/routes/api/tenants/[id]/clients/pin-reset/init/__tests__/pin-reset-init.test.ts @@ -80,7 +80,7 @@ describe("POST /api/tenants/[id]/clients/pin-reset/init", () => { }); it("should return 404 when client not found", async () => { - mockPinResetService.createResetToken.mockRejectedValue(new Error("Client not found")); + mockPinResetService.createResetToken.mockRejectedValue(new Error("Tenant or client not found")); const request = new Request("http://localhost/api", { method: "POST",