diff --git a/package-lock.json b/package-lock.json index 4d07139..980989b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "2.4.0", "license": "AGPL-3.0", "dependencies": { + "@date-fns/tz": "^1.5.0", "@noble/hashes": "^1.8.0", "@noble/post-quantum": "^0.4.1", "@simplewebauthn/server": "^11.0.0", @@ -23,7 +24,7 @@ "drizzle-orm": "^0.45.2", "jose": "^6.0.11", "mode-watcher": "^1.0.8", - "nodemailer": "^9.0.1", + "nodemailer": "^9.1.1", "postgres": "^3.4.7", "secrets.js-34r7h": "^2.0.2", "uuidv7": "^1.0.2", @@ -47,7 +48,7 @@ "@types/dotenv": "^6.1.1", "@types/node": "^24", "@types/nodemailer": "^6.4.17", - "bits-ui": "^2.15.4", + "bits-ui": "^2.19.0", "clsx": "^2.1.1", "drizzle-kit": "^0.31.1", "eslint": "^9.29.0", @@ -71,7 +72,7 @@ "typescript-eslint": "^8.34.1", "vaul-svelte": "^1.0.0-next.7", "vite": "^6.4.2", - "vitest": "^4.1.9" + "vitest": "^4.1.11" } }, "node_modules/@adobe/css-tools": { @@ -404,6 +405,12 @@ "kuler": "^2.0.0" } }, + "node_modules/@date-fns/tz": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz", + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==", + "license": "MIT" + }, "node_modules/@drizzle-team/brocli": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", @@ -1526,41 +1533,41 @@ "license": "MIT" }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -3500,16 +3507,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -3518,13 +3525,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.9", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3545,9 +3552,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -3558,13 +3565,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -3572,14 +3579,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3588,9 +3595,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -3598,13 +3605,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -3803,9 +3810,9 @@ "license": "MIT" }, "node_modules/bits-ui": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.1.tgz", - "integrity": "sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.19.0.tgz", + "integrity": "sha512-dHDm5Jw2NZJgLRvHAwPeuR4BnZij+AqTIq33OqVfo+qiMXontoC/yIvGT5TZaviv7OuBdJ2QUy2JPQ2yKBvF6w==", "dev": true, "license": "MIT", "dependencies": { @@ -5431,9 +5438,9 @@ } }, "node_modules/joi": { - "version": "17.13.4", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.4.tgz", - "integrity": "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==", + "version": "17.13.7", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.7.tgz", + "integrity": "sha512-MF80Dm5Y2veNy8QWVx9Bj3ui4mo7+VPSPsR1M+oaHXV0Gx6zGX9a2F+OZG3Blby9tOlzU9Rs5FUimlEhbKtfnQ==", "dev": true, "license": "BSD-3-Clause", "optional": true, @@ -5469,9 +5476,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -6169,9 +6176,9 @@ } }, "node_modules/nodemailer": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz", - "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.1.1.tgz", + "integrity": "sha512-izw9mVKFix6YSnC9eLgV6g1opl9DUlRio9ZNcq+Wu9Ujn2UwF+8Nl0B8nz22kEC+CTZCvinkxwJ0DeFbb6NwcQ==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -7311,9 +7318,9 @@ } }, "node_modules/svelte-eslint-parser/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -7610,9 +7617,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -8535,19 +8542,19 @@ } }, "node_modules/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -8575,12 +8582,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.9", - "@vitest/browser-preview": "4.1.9", - "@vitest/browser-webdriverio": "4.1.9", - "@vitest/coverage-istanbul": "4.1.9", - "@vitest/coverage-v8": "4.1.9", - "@vitest/ui": "4.1.9", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index 37271e0..2b82e3a 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@types/dotenv": "^6.1.1", "@types/node": "^24", "@types/nodemailer": "^6.4.17", - "bits-ui": "^2.15.4", + "bits-ui": "^2.19.0", "clsx": "^2.1.1", "drizzle-kit": "^0.31.1", "eslint": "^9.29.0", @@ -81,9 +81,10 @@ "typescript-eslint": "^8.34.1", "vaul-svelte": "^1.0.0-next.7", "vite": "^6.4.2", - "vitest": "^4.1.9" + "vitest": "^4.1.11" }, "dependencies": { + "@date-fns/tz": "^1.5.0", "@noble/hashes": "^1.8.0", "@noble/post-quantum": "^0.4.1", "@simplewebauthn/server": "^11.0.0", @@ -98,7 +99,7 @@ "drizzle-orm": "^0.45.2", "jose": "^6.0.11", "mode-watcher": "^1.0.8", - "nodemailer": "^9.0.1", + "nodemailer": "^9.1.1", "postgres": "^3.4.7", "secrets.js-34r7h": "^2.0.2", "uuidv7": "^1.0.2", diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json index 0f83b81..cb1c3ab 100644 --- a/project.inlang/messages/de.json +++ b/project.inlang/messages/de.json @@ -459,6 +459,18 @@ } ], "error": "Fehler beim Senden" + }, + "missingCryptoKeys": { + "title": "Kryptografische Schlüssel fehlen", + "description": "Dieser Tab enthält nicht die kryptografischen Schlüssel zum Entschlüsseln und Verschlüsseln von Daten. Lade die Schlüssel, um Termine/Daten anzuzeigen und hinzuzufügen.", + "cancel": "Ich will nur Einstellungen ändern", + "success": "Kryptografische Schlüssel geladen", + "error": "Fehler beim Laden der kryptografischen Schlüssel" + }, + "inactiveSession": { + "title": "Bist du noch da?", + "description": "Du wirst in Kürze automatisch abgemeldet.", + "cancel": "Ich bin noch da" } }, "agents": { diff --git a/project.inlang/messages/en.json b/project.inlang/messages/en.json index cc0c7a0..1ac6c2f 100644 --- a/project.inlang/messages/en.json +++ b/project.inlang/messages/en.json @@ -467,6 +467,18 @@ } ], "error": "Failed to send reminders" + }, + "missingCryptoKeys": { + "title": "Cryptography Keys Missing", + "description": "This tab is missing the cryptographic keys to decrypt and encrypt data. Load keys to view and add appointments/data.", + "cancel": "I just want to change settings", + "success": "Cryptography keys loaded", + "error": "Failed to load cryptography keys" + }, + "inactiveSession": { + "title": "Are you still here?", + "description": "You will be automatically logged out soon.", + "cancel": "I am still here" } }, "agents": { diff --git a/src/lib/client/appointment-crypto.ts b/src/lib/client/appointment-crypto.ts index f91e7f7..252c10c 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(); @@ -803,9 +801,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) { @@ -837,10 +833,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); @@ -975,9 +967,7 @@ export class UnifiedAppointmentCrypto { logoutStaff(): void { this.staffKeyPair = null; this.staffId = null; - this.tenantId = null; this.staffAuthenticated = false; - this.keyExpiry = null; } /** @@ -1744,10 +1734,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/components/layouts/sidebar-layout/components/notification-item.svelte b/src/lib/components/layouts/sidebar-layout/components/notification-item.svelte index 0dbdc95..925e7b4 100644 --- a/src/lib/components/layouts/sidebar-layout/components/notification-item.svelte +++ b/src/lib/components/layouts/sidebar-layout/components/notification-item.svelte @@ -52,10 +52,10 @@ } }; - const decryptPayload = async () => { + const decryptPayload = async (retry = true) => { if (item.metaData?.encryptedPayload) { if (!$staffCrypto.isAuthenticated || !$staffCrypto.crypto) { - const maxWaitTime = 5000; // 5 seconds + const maxWaitTime = 5000; const startTime = Date.now(); while (!$staffCrypto.isAuthenticated && Date.now() - startTime < maxWaitTime) { @@ -63,6 +63,13 @@ } if (!$staffCrypto.isAuthenticated || !$staffCrypto.crypto) { + const user = $auth.user; + if (retry && user && user.tenantId) { + console.warn("Staff crypto not initialized, retrying notification item decryption..."); + await staffCrypto.authenticateAndWait(user.id, user.tenantId); + decryptPayload(false); + return; + } console.error("Staff crypto not initialized after waiting"); return; } diff --git a/src/lib/components/ui/responsive-dialog/responsive-dialog.svelte b/src/lib/components/ui/responsive-dialog/responsive-dialog.svelte index 4da3c65..b90e4b5 100644 --- a/src/lib/components/ui/responsive-dialog/responsive-dialog.svelte +++ b/src/lib/components/ui/responsive-dialog/responsive-dialog.svelte @@ -52,6 +52,7 @@ triggerHidden = false, triggerVariant = "default", isActionLoading = false, + isDismissable = true, actions, children, }: HTMLAttributes & { @@ -62,6 +63,7 @@ description?: string; triggerVariant?: ButtonVariant; isActionLoading?: boolean; + isDismissable?: boolean; actions?: ListItemAction[]; } = $props(); @@ -150,40 +152,49 @@ {/if} {/if} - 0 && "[&>button:last-child]:hidden", // hides default close button - )} - onOpenAutoFocus={(e) => e.preventDefault()} - > - -
- {title} - {#if description} - - {description} - - {/if} -
- {#if actions && actions.length > 0} -
- {@render actionsSnippet?.()} - - - + + + 0 ? "[&>button:last-child]:hidden" : "", + )} + onOpenAutoFocus={(e) => e.preventDefault()} + escapeKeydownBehavior={isDismissable === false ? "ignore" : "close"} + interactOutsideBehavior={isDismissable === false ? "ignore" : "close"} + showCloseButton={isDismissable === true} + > + +
+ {title} + {#if description} + + {description} + + {/if}
- {/if} -
- -
- {@render children?.()} -
-
-
+ {#if actions && actions.length > 0} +
+ {@render actionsSnippet?.()} + + + +
+ {/if} + + +
+ {@render children?.()} +
+
+ +
{:else} - + {#if !triggerHidden} {#if typeof triggerLabel === "string"} @@ -193,29 +204,40 @@ {/if} {/if} - e.preventDefault()} - > - -
- {title} - {#if description} - - {description} - - {/if} -
-
- {@render actionsSnippet?.()} -
-
- - {@render children?.()} - - - {m.cancel()} - -
+ + + e.preventDefault()} + escapeKeydownBehavior={isDismissable === false ? "ignore" : "close"} + interactOutsideBehavior={isDismissable === false ? "ignore" : "close"} + > + +
+ {title} + {#if description} + + {description} + + {/if} +
+
+ {@render actionsSnippet?.()} +
+
+ + {@render children?.()} + + {#if isDismissable !== false} + + {m.cancel()} + + {/if} +
+
{/if} diff --git a/src/lib/const/tenants.ts b/src/lib/const/tenants.ts index 5484034..455a29f 100644 --- a/src/lib/const/tenants.ts +++ b/src/lib/const/tenants.ts @@ -1,5 +1,5 @@ export const SETUP_STATES_LIST = ["SETTINGS", "AGENTS", "CHANNELS", "STAFF", "READY"]; -export const TENANT_FEATURE_FLAGS = [ +export const TENANT_FEATURE_FLAGS = { // List of supported features flags -]; +} as const; diff --git a/src/lib/server/services/schedule-service.ts b/src/lib/server/services/schedule-service.ts index b2af647..4b765de 100644 --- a/src/lib/server/services/schedule-service.ts +++ b/src/lib/server/services/schedule-service.ts @@ -2,18 +2,20 @@ import { getTenantDb } from "../db"; import * as tenantSchema from "../db/tenant-schema"; import { type SelectAgent, + type SelectAgentAbsence, + type SelectAppointment, type SelectChannel, type SelectSlotTemplate, - type SelectAppointment, - type SelectAgentAbsence, scheduleCache, } from "../db/tenant-schema"; -import { eq, and, between, sql, or, inArray } from "drizzle-orm"; import logger from "$lib/logger"; +import { tz } from "@date-fns/tz"; +import { endOfDay, startOfDay } from "date-fns"; +import { and, between, eq, inArray, or, sql } from "drizzle-orm"; import { z } from "zod"; -import { ValidationError } from "../utils/errors"; import { WebAuthnService } from "../auth/webauthn-service"; +import { ValidationError } from "../utils/errors"; import { isValidTimeZone, toLocalTime, toLocalTimeIgnoringDst } from "../utils/timezone"; const CACHE_MAX_AHEAD_MONTHS = 14; @@ -698,8 +700,12 @@ export class ScheduleService { // Generate cache for each timezone for (const timeZone of timeZones) { await this.getSchedule({ - startDate: startDate.toISOString(), - endDate: endDate.toISOString(), + startDate: startOfDay(startDate, { in: tz(timeZone) }) + .withTimeZone("UTC") + .toISOString(), + endDate: endOfDay(endDate, { in: tz(timeZone) }) + .withTimeZone("UTC") + .toISOString(), tenantId: this.tenantId, channelId, timeZone, diff --git a/src/lib/stores/auth.ts b/src/lib/stores/auth.ts index cffbb43..859d853 100644 --- a/src/lib/stores/auth.ts +++ b/src/lib/stores/auth.ts @@ -1,7 +1,10 @@ import { browser } from "$app/environment"; +import { goto } from "$app/navigation"; +import { resolve } from "$app/paths"; import type { SupportedLocale } from "$lib/const/locales"; +import { ROUTES } from "$lib/const/routes"; import type { UserRole } from "$lib/server/auth/authorization-service"; -import { writable } from "svelte/store"; +import { get, writable } from "svelte/store"; export interface PasskeyAuthData { authenticatorData: string; @@ -15,6 +18,8 @@ export interface AuthState { refreshPromise: Promise | null; user?: AuthStateUser; passkeyAuthData?: PasskeyAuthData; + lastActive?: Date | undefined; + isInactive?: boolean; } export type AuthStateUser = { @@ -31,6 +36,7 @@ function createAuthStore() { const store = writable({ isAuthenticated: false, refreshPromise: null, + isInactive: false, }); return { @@ -64,6 +70,7 @@ function createAuthStore() { refreshPromise: null, user: undefined, passkeyAuthData: undefined, + lastActive: undefined, }); }, setPasskeyAuthData: (data: PasskeyAuthData) => { @@ -98,6 +105,26 @@ function createAuthStore() { unsubscribe(); return authState!.user?.tenantId || null; }, + refreshLastActive: () => { + store.update((state) => ({ ...state, lastActive: new Date(), isInactive: false })); + }, + checkLastActive: () => { + const lastActive = get(store).lastActive; + if (!lastActive) return; + + const now = new Date(); + const diff = now.getTime() - lastActive.getTime(); + + // Logs user out after 15 minutes of inactivity + if (diff > 15 * 60 * 1000) { + goto(resolve(ROUTES.LOGOUT)); + } + + // Triggers inactivity modal after 10 minutes + if (diff > 10 * 60 * 1000) { + store.update((state) => ({ ...state, isInactive: true })); + } + }, waitForRefresh: async () => { let authState: AuthState; const unsubscribe = store.subscribe((state) => { 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/lib/types/calendar.ts b/src/lib/types/calendar.ts index 3a24f30..9a92750 100644 --- a/src/lib/types/calendar.ts +++ b/src/lib/types/calendar.ts @@ -1,3 +1,4 @@ +import type { ClientTunnelResponse } from "$lib/server/services/appointment-service"; import type { CalendarAgent, DaySchedule } from "$lib/server/services/schedule-service"; export type AppointmentStatus = "available" | "booked" | "reserved" | "rejected"; @@ -48,6 +49,7 @@ export type TAppointmentInClipboard = { email?: string; phone?: string; dateTime?: Date; + tunnel?: ClientTunnelResponse; }; export type TCalendarItem = TCalendarSlot & { diff --git a/src/lib/types/tenant.ts b/src/lib/types/tenant.ts index a2f95fd..cb42cd3 100644 --- a/src/lib/types/tenant.ts +++ b/src/lib/types/tenant.ts @@ -1,4 +1,5 @@ import type { SelectTenant } from "$lib/server/db/central-schema"; +import { TENANT_FEATURE_FLAGS } from "$lib/const/tenants"; export type TTenant = Pick< SelectTenant, @@ -28,3 +29,5 @@ export type TTenantSettings = Omit => { const response = await fetch("/api/admin/tenant", { @@ -23,3 +25,10 @@ export const isSetupStateDone = (tenant: TTenant, step: TTenant["setupState"]): const stepIndex = SETUP_STATES_LIST.indexOf(step); return curStateIndex > stepIndex; }; + +export const hasFeatureFlag = (flag: TTenantFeatureFlag): boolean => { + const curTenant = get(tenants).currentTenant; + if (!curTenant) return false; + + return curTenant.features.includes(flag) || false; +}; diff --git a/src/routes/(pages)/dashboard/(components)/EmailReminder.svelte b/src/routes/(pages)/dashboard/(components)/EmailReminder.svelte index 0eb9f17..20523c6 100644 --- a/src/routes/(pages)/dashboard/(components)/EmailReminder.svelte +++ b/src/routes/(pages)/dashboard/(components)/EmailReminder.svelte @@ -10,6 +10,7 @@ import { onMount } from "svelte"; import { getNextAppointments, sendAppointmentReminders } from "./utils"; import { m } from "$i18n/messages"; + import { auth } from "$lib/stores/auth"; let status: "init" | "loading" | "sending" | "success" | "error" = $state("init"); let appointments: TEmailAppointmentReminder[] = $state([]); @@ -27,6 +28,8 @@ // Also gives staff crypto time to load, if not already loaded await new Promise((resolve) => setTimeout(resolve, 1000)); + await auth.waitForRefresh(); + const allAppointments = await getNextAppointments(tenantId); const decryptPromises = allAppointments.map(async (appointment) => { if ( diff --git a/src/routes/(pages)/dashboard/(components)/InactivityWarning.svelte b/src/routes/(pages)/dashboard/(components)/InactivityWarning.svelte new file mode 100644 index 0000000..60fe63f --- /dev/null +++ b/src/routes/(pages)/dashboard/(components)/InactivityWarning.svelte @@ -0,0 +1,33 @@ + + + +
+ + +
+
diff --git a/src/routes/(pages)/dashboard/(components)/MissingStaffCrypto.svelte b/src/routes/(pages)/dashboard/(components)/MissingStaffCrypto.svelte new file mode 100644 index 0000000..e1724dd --- /dev/null +++ b/src/routes/(pages)/dashboard/(components)/MissingStaffCrypto.svelte @@ -0,0 +1,100 @@ + + + +
+ + +
+
diff --git a/src/routes/(pages)/dashboard/+layout.svelte b/src/routes/(pages)/dashboard/+layout.svelte index afb2486..44b937b 100644 --- a/src/routes/(pages)/dashboard/+layout.svelte +++ b/src/routes/(pages)/dashboard/+layout.svelte @@ -9,6 +9,11 @@ import { staff } from "$lib/stores/staff"; import { QueryClient, QueryClientProvider } from "@tanstack/svelte-query"; import { browser } from "$app/environment"; + import { page } from "$app/state"; + import MissingStaffCrypto from "./(components)/MissingStaffCrypto.svelte"; + import { openDialog } from "$lib/components/ui/responsive-dialog"; + import { clock } from "$lib/stores/time"; + import InactivityWarning from "./(components)/InactivityWarning.svelte"; let { data, children }: LayoutProps = $props(); @@ -59,6 +64,18 @@ }; }); + $effect(() => { + if (page.url.pathname) { + auth.refreshLastActive(); + } + }); + + $effect(() => { + if ($clock) { + auth.checkLastActive(); + } + }); + const updateStores = async () => { staff.load(); notifications.load(); @@ -80,6 +97,7 @@ if (success) { console.log("✅ Staff crypto initialized successfully"); } else { + openDialog("missing-staff-crypto"); console.log("ℹ️ Staff crypto not initialized (no session data or error)"); } } @@ -88,4 +106,6 @@ {@render children()} + + diff --git a/src/routes/(pages)/dashboard/absences/(components)/add-absence-form/add-absence-form.svelte b/src/routes/(pages)/dashboard/absences/(components)/add-absence-form/add-absence-form.svelte index e075f6e..87f67c9 100644 --- a/src/routes/(pages)/dashboard/absences/(components)/add-absence-form/add-absence-form.svelte +++ b/src/routes/(pages)/dashboard/absences/(components)/add-absence-form/add-absence-form.svelte @@ -6,8 +6,11 @@ import { InputDateTime } from "$lib/components/ui/input-date-time"; import RadioCards from "$lib/components/ui/radio-cards/radio-cards.svelte"; import * as Select from "$lib/components/ui/select"; + import { times } from "$lib/components/ui/slot-template/utils"; import { Text } from "$lib/components/ui/typography"; import { agents as agentsStore } from "$lib/stores/agents"; + import { auth } from "$lib/stores/auth"; + import { cn } from "$lib/utils"; import { getDefaultEndTime, getDefaultStartTime, @@ -24,8 +27,6 @@ import type { AbsenceType } from "../types"; import { reasons, types } from "../utils"; import { formSchema } from "./schema"; - import { cn } from "$lib/utils"; - import { times } from "$lib/components/ui/slot-template/utils"; let { done }: { done: () => void } = $props(); @@ -46,6 +47,7 @@ dataType: "json", validators: zodClient(formSchema), onResult: async (event) => { + auth.refreshLastActive(); if (event.result.type === "success") { toast.success(m["absences.add.success"]()); done(); diff --git a/src/routes/(pages)/dashboard/absences/(components)/delete-absence-form/delete-absence-form.svelte b/src/routes/(pages)/dashboard/absences/(components)/delete-absence-form/delete-absence-form.svelte index 2ee5784..ec8a33f 100644 --- a/src/routes/(pages)/dashboard/absences/(components)/delete-absence-form/delete-absence-form.svelte +++ b/src/routes/(pages)/dashboard/absences/(components)/delete-absence-form/delete-absence-form.svelte @@ -4,6 +4,7 @@ import { Input } from "$lib/components/ui/input"; import { Text } from "$lib/components/ui/typography"; import { agents as agentsStore } from "$lib/stores/agents"; + import { auth } from "$lib/stores/auth"; import type { TAbsence } from "$lib/types/absence"; import { toast } from "svelte-sonner"; import { superForm } from "sveltekit-superforms"; @@ -20,6 +21,7 @@ { validators: zodClient(formSchema), onResult: async (event) => { + auth.refreshLastActive(); if (event.result.type === "success") { toast.success(m["absences.delete.success"]()); done(); diff --git a/src/routes/(pages)/dashboard/absences/(components)/edit-absence-form/edit-absence-form.svelte b/src/routes/(pages)/dashboard/absences/(components)/edit-absence-form/edit-absence-form.svelte index f52c617..261c148 100644 --- a/src/routes/(pages)/dashboard/absences/(components)/edit-absence-form/edit-absence-form.svelte +++ b/src/routes/(pages)/dashboard/absences/(components)/edit-absence-form/edit-absence-form.svelte @@ -4,14 +4,13 @@ import * as Form from "$lib/components/ui/form"; import { Input } from "$lib/components/ui/input"; import { InputDateTime } from "$lib/components/ui/input-date-time"; + import { RadioCards } from "$lib/components/ui/radio-cards"; import * as Select from "$lib/components/ui/select"; + import { times } from "$lib/components/ui/slot-template/utils"; import { agents as agentsStore } from "$lib/stores/agents"; + import { auth } from "$lib/stores/auth"; import type { TAbsence } from "$lib/types/absence"; - import { toast } from "svelte-sonner"; - import { superForm } from "sveltekit-superforms"; - import { zod4Client as zodClient } from "sveltekit-superforms/adapters"; - import { formSchema } from "."; - import { reasons, types } from "../utils"; + import { cn } from "$lib/utils"; import { timeLocalWithoutOffsetToUTC, timeUTCToLocalWithoutOffset, @@ -19,10 +18,12 @@ toWeekdaysLabel, weekdays, } from "$lib/utils/datetime"; + import { toast } from "svelte-sonner"; import { SvelteDate } from "svelte/reactivity"; - import { times } from "$lib/components/ui/slot-template/utils"; - import { RadioCards } from "$lib/components/ui/radio-cards"; - import { cn } from "$lib/utils"; + import { superForm } from "sveltekit-superforms"; + import { zod4Client as zodClient } from "sveltekit-superforms/adapters"; + import { formSchema } from "."; + import { reasons, types } from "../utils"; let { entity, done }: { entity: TAbsence; done: () => void } = $props(); @@ -45,6 +46,7 @@ dataType: "json", validators: zodClient(formSchema), onResult: async (event) => { + auth.refreshLastActive(); if (event.result.type === "success") { toast.success(m["absences.edit.success"]()); done(); diff --git a/src/routes/(pages)/dashboard/account/change-passphrase/(components)/change-passphrase-form/change-passphrase-form.svelte b/src/routes/(pages)/dashboard/account/change-passphrase/(components)/change-passphrase-form/change-passphrase-form.svelte index 2546ae6..4262d7d 100644 --- a/src/routes/(pages)/dashboard/account/change-passphrase/(components)/change-passphrase-form/change-passphrase-form.svelte +++ b/src/routes/(pages)/dashboard/account/change-passphrase/(components)/change-passphrase-form/change-passphrase-form.svelte @@ -19,6 +19,7 @@ dataType: "json", validators: zodClient(formSchema), onResult: async (event) => { + auth.refreshLastActive(); if (event.result.type === "success") { toast.success(m["account.change-passphrase.success"]()); } else if (event.result.type === "failure") { diff --git a/src/routes/(pages)/dashboard/account/general/(components)/account-settings-form/account-settings-form.svelte b/src/routes/(pages)/dashboard/account/general/(components)/account-settings-form/account-settings-form.svelte index 2c213db..6ff0a08 100644 --- a/src/routes/(pages)/dashboard/account/general/(components)/account-settings-form/account-settings-form.svelte +++ b/src/routes/(pages)/dashboard/account/general/(components)/account-settings-form/account-settings-form.svelte @@ -21,6 +21,7 @@ dataType: "json", validators: zodClient(formSchema), onResult: async (event) => { + auth.refreshLastActive(); if (event.result.type === "success") { toast.success(m["account.general.success"]()); } else if (event.result.type === "failure") { diff --git a/src/routes/(pages)/dashboard/account/passkeys/(components)/add-passkey-form/add-passkey-form.svelte b/src/routes/(pages)/dashboard/account/passkeys/(components)/add-passkey-form/add-passkey-form.svelte index ab55ad4..530ad5e 100644 --- a/src/routes/(pages)/dashboard/account/passkeys/(components)/add-passkey-form/add-passkey-form.svelte +++ b/src/routes/(pages)/dashboard/account/passkeys/(components)/add-passkey-form/add-passkey-form.svelte @@ -38,6 +38,9 @@ }, { validators: zodClient(formSchema), + onResult: () => { + auth.refreshLastActive(); + }, onChange: (event) => { if (event.paths.includes("email")) { setProperPasskeyState(); diff --git a/src/routes/(pages)/dashboard/account/passkeys/(components)/delete-passkey-form/delete-passkey-form.svelte b/src/routes/(pages)/dashboard/account/passkeys/(components)/delete-passkey-form/delete-passkey-form.svelte index e1a5d9f..877cfc9 100644 --- a/src/routes/(pages)/dashboard/account/passkeys/(components)/delete-passkey-form/delete-passkey-form.svelte +++ b/src/routes/(pages)/dashboard/account/passkeys/(components)/delete-passkey-form/delete-passkey-form.svelte @@ -5,6 +5,7 @@ import { Input } from "$lib/components/ui/input"; import { TranslationWithComponent } from "$lib/components/ui/translation-with-component"; import { Text } from "$lib/components/ui/typography"; + import { auth } from "$lib/stores/auth"; import { toast } from "svelte-sonner"; import { superForm } from "sveltekit-superforms"; import { zod4Client as zodClient } from "sveltekit-superforms/adapters"; @@ -29,6 +30,7 @@ ), ), onResult: async (event) => { + auth.refreshLastActive(); if (event.result.type === "success") { toast.success(m["account.passkeys.delete.success"]()); done(); diff --git a/src/routes/(pages)/dashboard/account/passkeys/(components)/edit-passkey-form/edit-passkey-form.svelte b/src/routes/(pages)/dashboard/account/passkeys/(components)/edit-passkey-form/edit-passkey-form.svelte index 0c64b61..4d70f01 100644 --- a/src/routes/(pages)/dashboard/account/passkeys/(components)/edit-passkey-form/edit-passkey-form.svelte +++ b/src/routes/(pages)/dashboard/account/passkeys/(components)/edit-passkey-form/edit-passkey-form.svelte @@ -2,6 +2,7 @@ import { m } from "$i18n/messages.js"; import * as Form from "$lib/components/ui/form"; import { Input } from "$lib/components/ui/input"; + import { auth } from "$lib/stores/auth"; import type { RedactedPasskeyHydrated } from "$lib/types/passkeys"; import { untrack } from "svelte"; import { toast } from "svelte-sonner"; @@ -20,6 +21,7 @@ dataType: "json", validators: zodClient(formSchema), onResult: async (event) => { + auth.refreshLastActive(); if (event.result.type === "success") { toast.success(m["account.passkeys.edit.success"]()); done(); diff --git a/src/routes/(pages)/dashboard/agents/(components)/add-agent-form/add-agent-form.svelte b/src/routes/(pages)/dashboard/agents/(components)/add-agent-form/add-agent-form.svelte index 27a92e9..b26d7df 100644 --- a/src/routes/(pages)/dashboard/agents/(components)/add-agent-form/add-agent-form.svelte +++ b/src/routes/(pages)/dashboard/agents/(components)/add-agent-form/add-agent-form.svelte @@ -5,13 +5,14 @@ import { InputCroppedImageBlob } from "$lib/components/ui/input-cropped-image-blob"; import { LanguageTabs } from "$lib/components/ui/language-tabs"; import { Textarea } from "$lib/components/ui/textarea"; + import { auth } from "$lib/stores/auth"; + import { tenants } from "$lib/stores/tenants"; import ItemIcon from "@lucide/svelte/icons/user-star"; import { toast } from "svelte-sonner"; + import { get } from "svelte/store"; import { superForm } from "sveltekit-superforms"; import { zod4Client as zodClient } from "sveltekit-superforms/adapters"; import { formSchema } from "."; - import { tenants } from "$lib/stores/tenants"; - import { get } from "svelte/store"; let { done }: { done: () => void } = $props(); @@ -29,6 +30,7 @@ dataType: "json", validators: zodClient(formSchema), onResult: async (event) => { + auth.refreshLastActive(); if (event.result.type === "success") { toast.success(m["agents.add.success"]()); done(); diff --git a/src/routes/(pages)/dashboard/agents/(components)/delete-agent-form/delete-agent-form.svelte b/src/routes/(pages)/dashboard/agents/(components)/delete-agent-form/delete-agent-form.svelte index fdc2b4f..b923114 100644 --- a/src/routes/(pages)/dashboard/agents/(components)/delete-agent-form/delete-agent-form.svelte +++ b/src/routes/(pages)/dashboard/agents/(components)/delete-agent-form/delete-agent-form.svelte @@ -11,6 +11,7 @@ import { zod4Client as zodClient } from "sveltekit-superforms/adapters"; import { z } from "zod"; import { formSchema } from "."; + import { auth } from "$lib/stores/auth"; let { entity, done }: { entity: TAgent; done: () => void } = $props(); @@ -30,6 +31,7 @@ ), ), onResult: async (event) => { + auth.refreshLastActive(); if (event.result.type === "success") { toast.success(m["agents.delete.success"]()); done(); diff --git a/src/routes/(pages)/dashboard/agents/(components)/edit-agent-form/edit-agent-form.svelte b/src/routes/(pages)/dashboard/agents/(components)/edit-agent-form/edit-agent-form.svelte index a78218f..23c5a72 100644 --- a/src/routes/(pages)/dashboard/agents/(components)/edit-agent-form/edit-agent-form.svelte +++ b/src/routes/(pages)/dashboard/agents/(components)/edit-agent-form/edit-agent-form.svelte @@ -5,14 +5,15 @@ import { InputCroppedImageBlob } from "$lib/components/ui/input-cropped-image-blob"; import { LanguageTabs } from "$lib/components/ui/language-tabs"; import { Textarea } from "$lib/components/ui/textarea"; + import { auth } from "$lib/stores/auth"; + import { tenants } from "$lib/stores/tenants"; import type { TAgent } from "$lib/types/agent"; import ItemIcon from "@lucide/svelte/icons/user-star"; import { toast } from "svelte-sonner"; + import { get } from "svelte/store"; import { superForm } from "sveltekit-superforms"; import { zod4Client as zodClient } from "sveltekit-superforms/adapters"; import { formSchema } from "."; - import { tenants } from "$lib/stores/tenants"; - import { get } from "svelte/store"; let { entity, done }: { entity: TAgent; done: () => void } = $props(); @@ -33,6 +34,7 @@ dataType: "json", validators: zodClient(formSchema), onResult: async (event) => { + auth.refreshLastActive(); if (event.result.type === "success") { toast.success(m["agents.edit.success"]()); done(); diff --git a/src/routes/(pages)/dashboard/calendar/(components)/Appointment.svelte b/src/routes/(pages)/dashboard/calendar/(components)/Appointment.svelte index 1d43e3e..68f7064 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/Appointment.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/Appointment.svelte @@ -6,13 +6,16 @@ import { ResponsiveDialog } from "$lib/components/ui/responsive-dialog"; import { type SupportedLocale } from "$lib/const/locales"; import { agents as agentsStore } from "$lib/stores/agents"; + import { auth } from "$lib/stores/auth"; import { type CurAppointmentItem } from "$lib/stores/calendar"; import { channels as channelsStore } from "$lib/stores/channels"; import type { TAppointmentFilter, TCalendarMode } from "$lib/types/calendar"; import { getCurrentTranlslation } from "$lib/utils/localizations"; import { CalendarPlus, Move, Trash2 } from "@lucide/svelte"; + import { onMount } from "svelte"; import { toast } from "svelte-sonner"; import { cancelAppointment, confirmAppointment, denyAppointment } from "./utils"; + import { getClientTunnel } from "./add-appointment/utils"; let { tenantId, @@ -41,6 +44,7 @@ let isDeleting = $state(false); const denyItem = async () => { + auth.refreshLastActive(); const proceed = confirm( `${m["calendar.notificationHint"]()} ${m["calendar.denyAppointment.confirm"]()}`, ); @@ -64,6 +68,7 @@ }; const confirmItem = async () => { + auth.refreshLastActive(); isConfirming = true; const success = await confirmAppointment({ tenant: tenantId, @@ -80,6 +85,10 @@ } isConfirming = false; }; + + onMount(() => { + auth.refreshLastActive(); + }); { - mode = { - mode: "ADD_FOLLOW_UP", - appointment: { - email: item.decrypted.email, - shareEmail: item.decrypted.shareEmail, - dateTime: item.appointment.appointment?.dateTime, - hasNoEmail: Boolean(item.decrypted.email), - phone: item.decrypted.phone, - name: item.decrypted.name, - locale: item.decrypted.locale || getLocale(), - }, - }; - shownAppointments = "available"; - close(); + onClick: async () => { + const tunnel = item.decrypted.email + ? await getClientTunnel(tenantId, item.decrypted.email) + : // Client without email + null; + if (tunnel !== undefined) { + mode = { + mode: "ADD_FOLLOW_UP", + appointment: { + email: item.decrypted.email, + shareEmail: item.decrypted.shareEmail, + dateTime: item.appointment.appointment?.dateTime, + hasNoEmail: Boolean(item.decrypted.email), + phone: item.decrypted.phone, + name: item.decrypted.name, + locale: item.decrypted.locale || getLocale(), + tunnel: tunnel || undefined, + }, + }; + shownAppointments = "available"; + close(); + } else { + console.error("Error getting client tunnel for follow-up appointment"); + } }, }, { diff --git a/src/routes/(pages)/dashboard/calendar/(components)/AppointmentPreview.svelte b/src/routes/(pages)/dashboard/calendar/(components)/AppointmentPreview.svelte index 1799c0f..49e912d 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/AppointmentPreview.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/AppointmentPreview.svelte @@ -4,11 +4,11 @@ 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"; import { cn } from "$lib/utils"; + import { auth } from "$lib/stores/auth"; let { item, @@ -21,15 +21,23 @@ let decrypted = $state(); let error = $state(); - onMount(() => { - decrypt(); + $effect(() => { + if (!decrypted) { + if ($staffCrypto.isAuthenticated) { + decrypt(); + } else { + error = "Keys missing"; + } + } }); const errors = { missingKeyShare: "Missing key share", }; - const decrypt = async () => { + 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"; @@ -52,7 +60,7 @@ // Wait for crypto to be initialized (max 5 seconds) if (!$staffCrypto.isAuthenticated || !$staffCrypto.crypto) { - const maxWaitTime = 5000; // 5 seconds + const maxWaitTime = 5000; const startTime = Date.now(); while (!$staffCrypto.isAuthenticated && Date.now() - startTime < maxWaitTime) { @@ -60,6 +68,13 @@ } if (!$staffCrypto.isAuthenticated || !$staffCrypto.crypto) { + const user = $auth.user; + if (retry && user && user.tenantId) { + console.warn("Staff crypto not initialized, retrying decryption..."); + await staffCrypto.authenticateAndWait(user.id, user.tenantId); + decrypt(false); + return; + } error = "Crypto not initialized"; console.error("Staff crypto not initialized after waiting"); return; diff --git a/src/routes/(pages)/dashboard/calendar/(components)/CalendarFilters.svelte b/src/routes/(pages)/dashboard/calendar/(components)/CalendarFilters.svelte index 6875ad3..7e928be 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/CalendarFilters.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/CalendarFilters.svelte @@ -12,6 +12,7 @@ import * as Sidebar from "$lib/components/ui/sidebar"; import { Text } from "$lib/components/ui/typography"; import { agents as agentsStore } from "$lib/stores/agents"; + import { auth } from "$lib/stores/auth"; import { channels as channelsStore } from "$lib/stores/channels"; import { sidebar as sidebarStore } from "$lib/stores/sidebar"; import type { TAppointmentFilter } from "$lib/types/calendar"; @@ -52,6 +53,7 @@ let agents = $derived($agentsStore.agents.filter((x) => !x.archived)); const zoom = (direction: number) => { + auth.refreshLastActive(); const currentIndex = CALENDAR_ZOOM_STEPS.indexOf(scale); if (currentIndex === -1) return; const nextIndex = currentIndex + direction; @@ -60,11 +62,13 @@ }; const changeView = (newView: CalendarView) => { + auth.refreshLastActive(); view = newView; document.cookie = `calendarView=${newView}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Strict`; }; const clearFilters = () => { + auth.refreshLastActive(); shownAppointments = "all"; shownChannels = []; shownAgents = []; @@ -239,6 +243,7 @@ {shownAgents} {shownChannels} onSelectDay={(v: DateValue | undefined) => { + auth.refreshLastActive(); sidebarStore.setCalendarExpanded(!sidebar.isCalendarExpanded); if (view === "week-workdays" && v && isWeekend(v, getLocale())) { changeView("week"); diff --git a/src/routes/(pages)/dashboard/calendar/(components)/CalendarHeader.svelte b/src/routes/(pages)/dashboard/calendar/(components)/CalendarHeader.svelte index d0f0c71..8bb3a00 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/CalendarHeader.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/CalendarHeader.svelte @@ -3,6 +3,7 @@ import { getLocale } from "$i18n/runtime"; import { Button, buttonVariants } from "$lib/components/ui/button"; import * as Popover from "$lib/components/ui/popover/index.js"; + import { auth } from "$lib/stores/auth"; import type { TAppointmentFilter, TCalendarMode } from "$lib/types/calendar"; import { cn } from "$lib/utils"; import { @@ -37,6 +38,7 @@ let isWeekView = $derived(view !== "day"); const prev = () => { + auth.refreshLastActive(); const nextDate = new CalendarDate( selectedDate.year, selectedDate.month, @@ -48,6 +50,7 @@ }; const next = () => { + auth.refreshLastActive(); const nextDate = new CalendarDate(selectedDate.year, selectedDate.month, selectedDate.day).add({ days: isWeekView ? 7 : 1, }); @@ -55,10 +58,12 @@ }; const setToToday = () => { + auth.refreshLastActive(); selectedDate = today(getLocalTimeZone()); }; const onSelectDay = (v: DateValue | undefined) => { + auth.refreshLastActive(); open = false; if (view === "week-workdays" && v && isWeekend(v, getLocale())) { view = "week"; diff --git a/src/routes/(pages)/dashboard/calendar/(components)/MoveAppointment.svelte b/src/routes/(pages)/dashboard/calendar/(components)/MoveAppointment.svelte index 5133afe..6f83696 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/MoveAppointment.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/MoveAppointment.svelte @@ -4,6 +4,7 @@ import Button from "$lib/components/ui/button/button.svelte"; import { ResponsiveDialog } from "$lib/components/ui/responsive-dialog"; import { Text } from "$lib/components/ui/typography"; + import { auth } from "$lib/stores/auth"; import { channels as channelsStore } from "$lib/stores/channels"; import type { TCalendarModeMove, TCalendarSlot } from "$lib/types/calendar"; import { utcToLocalWithoutDST } from "$lib/utils/datetime"; @@ -66,15 +67,15 @@ }); if (success) { toast.success(m["calendar.moveAppointment.success"]()); - isSubmitting = false; updateCalendar(); } else { toast.error(m["calendar.moveAppointment.error"]()); } + isSubmitting = false; }; onMount(() => { - console.log("item.start", item.start); + auth.refreshLastActive(); if (mode.agentId && item.availableAgents) { const availableAgents = item.availableAgents.map((it) => it.id); if (availableAgents.includes(mode.agentId)) { 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 dad442b..62e80a6 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/AddAppointment.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/AddAppointment.svelte @@ -16,6 +16,7 @@ import SelectAgent from "./SelectAgent.svelte"; import Summary from "./Summary.svelte"; import type { TAddAppointment, TAddAppointmentStep } from "./types"; + import { auth } from "$lib/stores/auth"; let { tenantId, @@ -46,6 +47,7 @@ let isSubmitting = $state(false); const proceed = (data: TAddAppointment) => { + auth.refreshLastActive(); switch (true) { case !data.agentId: { newAppointment = { @@ -77,6 +79,7 @@ }; const addAppointment = async () => { + auth.refreshLastActive(); if ( newAppointment.name && newAppointment.agentId && @@ -133,6 +136,7 @@ phone: appointment.phone, locale: appointment.locale, hasNoEmail: Boolean(appointment.email), + tunnel: appointment.tunnel, }); } } @@ -146,7 +150,7 @@ {:else if step === "summary"}
- {#if newAppointment.hasNoEmail || !newAppointment.tunnel} + {#if !newAppointment.tunnel} {m["calendar.addAppointment.steps.summary.hint"]()} diff --git a/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/search-client-form/search-client-form.svelte b/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/search-client-form/search-client-form.svelte index 830ce42..f320cc3 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/search-client-form/search-client-form.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/add-appointment/search-client-form/search-client-form.svelte @@ -1,15 +1,14 @@