diff --git a/bun.lock b/bun.lock index cf2eb304b..10ce13d78 100644 --- a/bun.lock +++ b/bun.lock @@ -6,12 +6,20 @@ "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@95547fc", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@88c189e", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8dcaa17", + "@codemirror/autocomplete": "^6.19.0", + "@codemirror/commands": "^6.9.0", + "@codemirror/language": "^6.11.3", + "@codemirror/lint": "^6.9.0", + "@codemirror/search": "^6.5.11", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.38.6", "@faker-js/faker": "^9.9.0", + "@lezer/highlight": "^1.2.1", "@plausible-analytics/tracker": "^0.4.4", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.55.1", @@ -20,12 +28,14 @@ "@threlte/extras": "^9.13.0", "ai": "^6.0.138", "analytics": "^0.8.19", + "codemirror-json5": "^1.0.3", "cron-parser": "^4.9.0", "dayjs": "^1.11.20", "deep-equal": "^2.2.3", "echarts": "^5.6.0", "flatted": "^3.4.2", "ignore": "^6.0.2", + "json5": "^2.2.3", "nanoid": "^5.1.7", "nanotar": "^0.3.0", "pretty-bytes": "^6.1.1", @@ -37,6 +47,7 @@ "devDependencies": { "@eslint/compat": "^1.4.1", "@eslint/js": "^9.39.4", + "@lezer/common": "^1.5.0", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", "@playwright/test": "^1.58.2", @@ -82,7 +93,7 @@ "flatted": "^3.4.2", "immutable": "^5.1.5", "minimatch": "10.2.3", - "picomatch": "^2.3.2", + "picomatch": "^4.0.4", "vite": "npm:rolldown-vite@latest", "yaml": "^1.10.3", }, @@ -113,7 +124,7 @@ "@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="], - "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@95547fc", { "dependencies": { "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@88c189e", { "dependencies": { "json-bigint": "1.0.0" } }], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], @@ -159,6 +170,20 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A=="], + + "@codemirror/commands": ["@codemirror/commands@6.10.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q=="], + + "@codemirror/language": ["@codemirror/language@6.12.3", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA=="], + + "@codemirror/lint": ["@codemirror/lint@6.9.5", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA=="], + + "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="], + + "@codemirror/state": ["@codemirror/state@6.6.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ=="], + + "@codemirror/view": ["@codemirror/view@6.41.1", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-ToDnWKbBnke+ZLrP6vgTTDScGi5H37YYuZGniQaBzxMVdtCxMrslsmtnOvbPZk4RX9bvkQqnWR/WS/35tJA0qg=="], + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], @@ -225,6 +250,14 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@lezer/common": ["@lezer/common@1.5.2", "", {}, "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="], + + "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="], + + "@lezer/lr": ["@lezer/lr@1.4.10", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A=="], + + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], + "@melt-ui/pp": ["@melt-ui/pp@0.3.2", "", { "dependencies": { "estree-walker": "^3.0.3", "magic-string": "^0.30.5" }, "peerDependencies": { "@melt-ui/svelte": ">= 0.29.0", "svelte": "^3.55.0 || ^4.0.0 || ^5.0.0-next.1" } }, "sha512-xKkPvaIAFinklLXcQOpwZ8YSpqAFxykjWf8Y/fSJQwsixV/0rcFs07hJ49hJjPy5vItvw5Qa0uOjzFUbXzBypQ=="], "@melt-ui/svelte": ["@melt-ui/svelte@0.86.6", "", { "dependencies": { "@floating-ui/core": "^1.3.1", "@floating-ui/dom": "^1.4.5", "@internationalized/date": "^3.5.0", "dequal": "^2.0.3", "focus-trap": "^7.5.2", "nanoid": "^5.0.4" }, "peerDependencies": { "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.118" } }, "sha512-Jer+M7DgIwT5IHfTayb4Iw/fkkxWNmC/mqn/nMh9JrbPbkxmyabfLQnhJ+JDn5HK77f84j34lubO3iqFtYAfMg=="], @@ -647,6 +680,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "codemirror-json5": ["codemirror-json5@1.0.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "json5": "^2.2.1", "lezer-json5": "^2.0.2" } }, "sha512-HmmoYO2huQxoaoG5ARKjqQc9mz7/qmNPvMbISVfIE2Gk1+4vZQg9X3G6g49MYM5IK00Ol3aijd7OKrySuOkA7Q=="], + "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="], "color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="], @@ -663,6 +698,8 @@ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], + "cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -1009,6 +1046,8 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lezer-json5": ["lezer-json5@2.0.2", "", { "dependencies": { "@lezer/lr": "^1.0.0" } }, "sha512-NRmtBlKW/f8mA7xatKq8IUOq045t8GVHI4kZXrUtYYUdiVeGiO6zKGAV7/nUAnf5q+rYTY+SWX/gvQdFXMjNxQ=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], @@ -1145,7 +1184,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], @@ -1289,6 +1328,8 @@ "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], + "style-value-types": ["style-value-types@5.1.2", "", { "dependencies": { "hey-listen": "^1.0.8", "tslib": "2.4.0" } }, "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -1409,6 +1450,8 @@ "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], "webgl-sdf-generator": ["webgl-sdf-generator@1.1.1", "", {}, "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="], diff --git a/package.json b/package.json index 641fd4316..a00683318 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@95547fc", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@88c189e", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", diff --git a/src/lib/appwrite/impersonation.ts b/src/lib/appwrite/impersonation.ts new file mode 100644 index 000000000..9339a9924 --- /dev/null +++ b/src/lib/appwrite/impersonation.ts @@ -0,0 +1,125 @@ +import { writable } from 'svelte/store'; +import { building } from '$app/environment'; + +const KEY_TARGET_USER_ID = 'console.impersonation.targetUserId'; +const KEY_OPERATOR = 'console.impersonation.operator'; +const KEY_TARGET = 'console.impersonation.target'; + +export type OperatorSnapshot = { + $id: string; + name: string; + email: string; +}; + +export type TargetSnapshot = { + $id: string; + name: string; + email: string; +}; + +/** + * Incrementing revision triggers reactive re-fetches after impersonation changes. + * Consumers can depend() on this or subscribe to it. + */ +export const impersonationRevision = writable(0); + +function bumpRevision() { + impersonationRevision.update((n) => n + 1); +} + +/** + * All SDK clients that need impersonation headers applied/cleared. + * Populated lazily by sdk.ts via registerImpersonationClients(). + */ +let _clients: import('@appwrite.io/console').Client[] = []; + +export function registerImpersonationClients( + clients: import('@appwrite.io/console').Client[] +): void { + _clients = clients; +} + +export function applyImpersonation(targetUserId: string): void { + for (const client of _clients) { + client.setImpersonateUserId(targetUserId); + } +} + +export function clearImpersonation(): void { + for (const client of _clients) { + // Use the typed setters to empty the values so the SDK removes the headers cleanly. + client.setImpersonateUserId(''); + client.setImpersonateUserEmail(''); + client.setImpersonateUserPhone(''); + } +} + +export function persistImpersonation(target: TargetSnapshot, operator: OperatorSnapshot): void { + sessionStorage.setItem(KEY_TARGET_USER_ID, target.$id); + sessionStorage.setItem(KEY_TARGET, JSON.stringify(target)); + sessionStorage.setItem(KEY_OPERATOR, JSON.stringify(operator)); +} + +export function clearPersistedImpersonation(): void { + sessionStorage.removeItem(KEY_TARGET_USER_ID); + sessionStorage.removeItem(KEY_TARGET); + sessionStorage.removeItem(KEY_OPERATOR); +} + +export function readTargetSnapshot(): TargetSnapshot | null { + if (building) return null; + const raw = sessionStorage.getItem(KEY_TARGET); + if (!raw) return null; + try { + return JSON.parse(raw) as TargetSnapshot; + } catch { + return null; + } +} + +export function readImpersonationTargetUserId(): string | null { + if (building) return null; + return sessionStorage.getItem(KEY_TARGET_USER_ID); +} + +export function readOperatorSnapshot(): OperatorSnapshot | null { + if (building) return null; + const raw = sessionStorage.getItem(KEY_OPERATOR); + if (!raw) return null; + try { + return JSON.parse(raw) as OperatorSnapshot; + } catch { + return null; + } +} + +/** + * Call once on app startup (e.g. root layout onMount). + * Re-applies impersonation headers from sessionStorage to all registered clients. + */ +export function restoreImpersonation(): void { + const targetUserId = readImpersonationTargetUserId(); + if (targetUserId) { + applyImpersonation(targetUserId); + bumpRevision(); + } +} + +/** + * Start impersonating a user. Applies headers, persists to sessionStorage, + * and bumps the revision store so callers can invalidate/refetch. + */ +export function startImpersonation(target: TargetSnapshot, operator: OperatorSnapshot): void { + applyImpersonation(target.$id); + persistImpersonation(target, operator); + bumpRevision(); +} + +/** + * Stop impersonating. Clears headers, removes sessionStorage, bumps revision. + */ +export function stopImpersonation(): void { + clearImpersonation(); + clearPersistedImpersonation(); + bumpRevision(); +} diff --git a/src/lib/components/impersonation/banner.svelte b/src/lib/components/impersonation/banner.svelte new file mode 100644 index 000000000..98bcb3b3e --- /dev/null +++ b/src/lib/components/impersonation/banner.svelte @@ -0,0 +1,63 @@ + + +{#if isImpersonating} + + + You are operating as {effectiveLabel} on behalf of operator + {operatorLabel}. + + + + + +{/if} diff --git a/src/lib/components/impersonation/picker.svelte b/src/lib/components/impersonation/picker.svelte new file mode 100644 index 000000000..f150e140f --- /dev/null +++ b/src/lib/components/impersonation/picker.svelte @@ -0,0 +1,368 @@ + + + + + Matches the start of name, email, phone, or user ID. The Console runs with the selected + account's access until you end impersonation. + + + + +
+ + + + + + {#if loading} + + + + {/if} +
+ + + {#if showNoResults} + No users found + {/if} + + + {#if showRecents} + Recent + {/if} + + {#each listToShow.length ? listToShow : showRecents ? recents : [] as item (item.$id)} + {@const disabled = isDisabled(item.$id)} + {@const label = disabledLabel(item.$id)} + {@const active = item.$id === impersonatingId} + + +
selectUser(item)}> + +
+ {displayName(item)} + {#if item.email && item.email !== displayName(item)} + {item.email} + {/if} + + +
+ {#if label} + {label} + {:else if active} + Active + {/if} +
+ {/each} +
+
+ + diff --git a/src/lib/components/navbar.svelte b/src/lib/components/navbar.svelte index ce5b64a2b..97a150ef1 100644 --- a/src/lib/components/navbar.svelte +++ b/src/lib/components/navbar.svelte @@ -50,6 +50,12 @@ import { page } from '$app/state'; import type { Models } from '@appwrite.io/console'; import { isProjectBlocked as getIsProjectBlocked } from '$lib/helpers/project'; + import ImpersonationPicker from '$lib/components/impersonation/picker.svelte'; + import { + readImpersonationTargetUserId, + impersonationRevision + } from '$lib/appwrite/impersonation'; + import { IconEye } from '@appwrite.io/pink-icons-svelte'; let showSupport = false; @@ -111,6 +117,19 @@ $: currentOrg = organizations.find((org) => org.isSelected); $: isProjectBlocked = getIsProjectBlocked(currentProject); + let canImpersonate = false; + + // Show impersonation picker when the operator has the capability or impersonation is active. + $: { + void $impersonationRevision; + canImpersonate = + $user?.impersonator === true || + !!$user?.impersonatorUserId || + !!readImpersonationTargetUserId(); + } + + let showImpersonationPicker = false; + beforeNavigate(() => (showAccountMenu = false)); @@ -198,6 +217,22 @@ + {#if canImpersonate} + + + { + showImpersonationPicker = true; + (event.currentTarget as HTMLElement).blur(); + }} + icon> + + + Impersonate user + + {/if} header'); - const sidebar: HTMLElement = document.querySelector('main > div > nav'); - const contentSection: HTMLElement = document.querySelector('main > div > section'); + const sidebar: HTMLElement = document.querySelector('main nav'); + const contentSection: HTMLElement = document.querySelector('main .main-content'); if (alertHeight) { // for sidebar and sub-navigation! diff --git a/src/lib/stores/auth-methods.ts b/src/lib/stores/auth-methods.ts index 0f31134c8..4e2c0cf78 100644 --- a/src/lib/stores/auth-methods.ts +++ b/src/lib/stores/auth-methods.ts @@ -1,9 +1,9 @@ import { writable } from 'svelte/store'; -import { type Models, AuthMethod as AuthMethodEnum } from '@appwrite.io/console'; +import { type Models, MethodId } from '@appwrite.io/console'; export type AuthMethod = { label: string; - method: AuthMethodEnum; + method: MethodId; value: boolean | null; }; @@ -11,37 +11,37 @@ const setAuthMethod = (project: Models.Project): AuthMethod[] => { return [ { label: 'Email/Password', - method: AuthMethodEnum.Emailpassword, + method: MethodId.Emailpassword, value: project?.authEmailPassword }, { label: 'Phone', - method: AuthMethodEnum.Phone, + method: MethodId.Phone, value: project?.authPhone }, { label: 'Magic URL', - method: AuthMethodEnum.Magicurl, + method: MethodId.Magicurl, value: project?.authUsersAuthMagicURL }, { label: 'Email OTP', - method: AuthMethodEnum.Emailotp, + method: MethodId.Emailotp, value: project?.authEmailOtp }, { label: 'Anonymous', - method: AuthMethodEnum.Anonymous, + method: MethodId.Anonymous, value: project?.authAnonymous }, { label: 'Team Invites', - method: AuthMethodEnum.Invites, + method: MethodId.Invites, value: project?.authInvites }, { label: 'JWT', - method: AuthMethodEnum.Jwt, + method: MethodId.Jwt, value: project?.authJWT } ]; diff --git a/src/lib/stores/headerAlert.ts b/src/lib/stores/headerAlert.ts index 6accad1f7..1d5faffee 100644 --- a/src/lib/stores/headerAlert.ts +++ b/src/lib/stores/headerAlert.ts @@ -30,8 +30,18 @@ function createHeaderAlertStore() { } }); }, + updateShow: (id: string, show: boolean) => { + update((n) => { + const component = n.components.find((c) => c.id === id); + if (!component || component.show === show) return n; + + component.show = show; + n.components = [...n.components]; + return n; + }); + }, get: (): HeaderAlert => { - //return highest importance component + // return highest importance visible component let component = { id: '', show: false, @@ -40,7 +50,7 @@ function createHeaderAlertStore() { }; update((n) => { n.components.forEach((c) => { - if (c.importance > component.importance) { + if (c.show && c.importance > component.importance) { component = c; } }); diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index b94d1782f..aa8049485 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -1,4 +1,5 @@ import { isMultiRegionSupported, VARS } from '$lib/system'; +import { registerImpersonationClients, restoreImpersonation } from '$lib/appwrite/impersonation'; import { Account, Assistant, @@ -68,6 +69,7 @@ function createConsoleSdk(client: Client) { const endpoint = getApiEndpoint(); const clientConsole = new Client(); +const clientConsoleOperator = new Client(); const scopedConsoleClient = new Client(); const clientProject = new Client(); @@ -76,9 +78,18 @@ const clientRealtime = new Client(); if (!building) { scopedConsoleClient.setProject('console'); clientConsole.setEndpoint(endpoint).setProject('console'); + clientConsoleOperator.setEndpoint(endpoint).setProject('console'); clientProject.setEndpoint(endpoint).setMode('admin'); clientRealtime.setEndpoint(endpoint).setProject('console'); + + registerImpersonationClients([ + clientConsole, + scopedConsoleClient, + clientProject, + clientRealtime + ]); + restoreImpersonation(); } const sdkForProject = { @@ -139,6 +150,10 @@ export const realtime = { export const sdk = { forConsole: createConsoleSdk(clientConsole), + // Operator-only console client. It is intentionally not registered with the + // impersonation module so admin actions like switching targets stay scoped + // to the real operator session. + forConsoleAsOperator: createConsoleSdk(clientConsoleOperator), forConsoleIn(region: string) { const regionAwareEndpoint = getApiEndpoint(region); diff --git a/src/routes/(console)/bottomAlerts.ts b/src/routes/(console)/bottomAlerts.ts index b4082f799..0e406c22a 100644 --- a/src/routes/(console)/bottomAlerts.ts +++ b/src/routes/(console)/bottomAlerts.ts @@ -1,32 +1,33 @@ import { isCloud } from '$lib/system'; import { isSameDay } from '$lib/helpers/date'; import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts'; -import WebhooksApi from '$lib/images/promos/webhooks-api.png'; +import TerraformProvider from '$lib/images/promos/terraform-provider.png'; const listOfPromotions: BottomModalAlertItem[] = []; if (isCloud) { - const webhooksApiPromo: BottomModalAlertItem = { - id: 'modal:webhooks_api_announcement', + const terraformProviderPromo: BottomModalAlertItem = { + id: 'modal:terraform_provider_announcement', src: { - dark: WebhooksApi, - light: WebhooksApi + dark: TerraformProvider, + light: TerraformProvider }, - title: 'Announcing the Webhooks API', - message: 'Manage webhooks programmatically with Server SDKs and API keys.', + title: 'Introducing Terraform support for Appwrite projects', + message: + 'Manage your entire Appwrite project as code with the official Terraform provider.', plan: 'free', importance: 8, scope: 'everywhere', cta: { text: 'Read announcement', - link: () => 'https://appwrite.io/blog/post/announcing-webhooks-api', + link: () => 'https://appwrite.io/blog/post/introducing-terraform-provider-for-appwrite', external: true, hideOnClick: true, skipUpgradeRedirect: true }, show: true }; - listOfPromotions.push(webhooksApiPromo); + listOfPromotions.push(terraformProviderPromo); } export function addBottomModalAlerts() { diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte index 22898b060..6ddb4a4a4 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/passwordPolicies.svelte @@ -56,20 +56,17 @@ async function updatePasswordPolicies() { try { - const projectSdk = sdk.forConsole.projects; + const projectSdk = sdk.forProject(project.region, project.$id).project; - await projectSdk.updateAuthPasswordHistory({ - projectId: project.$id, - limit: passwordHistoryEnabled ? passwordHistory : 0 + await projectSdk.updatePasswordHistoryPolicy({ + total: passwordHistoryEnabled ? passwordHistory : 0 }); - await projectSdk.updateAuthPasswordDictionary({ - projectId: project.$id, + await projectSdk.updatePasswordDictionaryPolicy({ enabled: passwordDictionary }); - await projectSdk.updatePersonalDataCheck({ - projectId: project.$id, + await projectSdk.updatePasswordPersonalDataPolicy({ enabled: authPersonalDataCheck }); diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/sessionSecurity.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/sessionSecurity.svelte index 6babbbecc..f9924befb 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/sessionSecurity.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/sessionSecurity.svelte @@ -29,12 +29,11 @@ async function updateSessionSecurity() { try { - await sdk.forConsole.projects.updateSessionAlerts({ - projectId: project.$id, - alerts: authSessionAlerts + const projectSdk = sdk.forProject(project.region, project.$id).project; + await projectSdk.updateSessionAlertPolicy({ + enabled: authSessionAlerts }); - await sdk.forConsole.projects.updateSessionInvalidation({ - projectId: project.$id, + await projectSdk.updateSessionInvalidationPolicy({ enabled: sessionInvalidation }); diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte index 69c4deb97..074a39c7d 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte @@ -16,17 +16,30 @@ } = $props(); let authMembershipsMfa = $state(project?.authMembershipsMfa ?? true); + let authMembershipsUserId = $state(project?.authMembershipsUserId ?? true); let authMembershipsUserName = $state(project?.authMembershipsUserName ?? true); let authMembershipsUserEmail = $state(project?.authMembershipsUserEmail ?? true); + let authMembershipsUserPhone = $state(project?.authMembershipsUserPhone ?? true); + + const isSubmitDisabled = $derived( + authMembershipsUserId === (project?.authMembershipsUserId ?? true) && + authMembershipsUserName === (project?.authMembershipsUserName ?? true) && + authMembershipsUserEmail === (project?.authMembershipsUserEmail ?? true) && + authMembershipsUserPhone === (project?.authMembershipsUserPhone ?? true) && + authMembershipsMfa === (project?.authMembershipsMfa ?? true) + ); async function updateMembershipsPrivacy() { try { - await sdk.forConsole.projects.updateMembershipsPrivacy({ - projectId: project.$id, - userName: authMembershipsUserName, - userEmail: authMembershipsUserEmail, - mfa: authMembershipsMfa - }); + await sdk + .forProject(project.region, project.$id) + .project.updateMembershipPrivacyPolicy({ + userId: authMembershipsUserId, + userName: authMembershipsUserName, + userEmail: authMembershipsUserEmail, + userPhone: authMembershipsUserPhone, + userMFA: authMembershipsMfa + }); await invalidate(Dependencies.PROJECT); addNotification({ type: 'success', @@ -48,6 +61,11 @@ Memberships privacy Set privacy preferences to manage which details team members can view about one another. + + - + diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateMockNumbers.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateMockNumbers.svelte index 88c2e91fd..3a7242cf4 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateMockNumbers.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateMockNumbers.svelte @@ -1,12 +1,11 @@ -
- - Mock phone numbers - Generate fictional numbers to simulate phone verification when testing demo accounts - for submitting your application to the App Store or Google Play. - - Learn more - - {#if isComponentDisabled} - - -
+ + Mock phone numbers + Generate fictional numbers to simulate phone verification when testing demo accounts for + submitting your application to the App Store or Google Play. + + Learn more + + {#if isComponentDisabled} + + +
+ {#if $app.themeInUse === 'dark'} + Mock Numbers Example + {:else} + Mock Numbers Example + {/if} +
+
+ {#if $app.themeInUse === 'dark'} Mock Numbers Example {:else} Mock Numbers Example {/if} -
-
- - {#if $app.themeInUse === 'dark'} - Mock Numbers Example - {:else} - Mock Numbers Example - {/if} - -
-
- {emptyStateTitle} - {emptyStateDescription} - - - -
- {:else if numbers?.length > 0} - {#each numbers as number, index} - + +
+
+ {emptyStateTitle} + {emptyStateDescription} + + + +
+ {:else if isLoadingMockNumbers} + + + + {:else if savedNumbers.length > 0 || draftNumbers.length > 0} + {#each savedNumbers as number, index (getSavedRowKey(number, index))} + + + + + + + (number.otp = generateOTP())} /> + Regenerate + + + {#if isSaved(number)} + + {/if} + + {#if pendingRow === getSavedRowKey(number, index)} + + + + {/if} + + + {/each} + {#each draftNumbers as number, index (getDraftRowKey(number, index))} + + + (number.phone = generateNumber())} /> + on:click={() => + setDraftRow(index, { + ...number, + number: generateNumber() + })} /> Regenerate - - - - (number.otp = generateOTP())} /> - Regenerate - - - - - {/each} - {#if numbers?.length < 10} -
+ + + + + setDraftRow(index, { ...number, otp: generateOTP() })} /> + Regenerate + + -
- {/if} - {:else} - { - addPhoneNumber({ - phone: generateNumber(), - otp: generateOTP() - }); - }}>Generate number + + {#if pendingRow === getDraftRowKey(number, index)} + + + + {/if} +
+ + {/each} + {#if savedNumbers.length + draftNumbers.length < 10} +
+ +
{/if} -
+ {:else} + Add a number + {/if} + +
- - - - -
+ + {#if getDeleteTargetRow()} +

+ Are you sure you want to delete {getDeleteTargetRow()?.initialNumber}? +

+ {/if} +
diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte index de62ad384..178e03850 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte @@ -21,8 +21,7 @@ async function updateSessionLength() { try { - await sdk.forConsole.projects.updateAuthDuration({ - projectId: project.$id, + await sdk.forProject(project.region, project.$id).project.updateSessionDurationPolicy({ duration: $baseValue }); await invalidate(Dependencies.PROJECT); diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte index 1c6f0bb90..4ea90f44e 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte @@ -19,9 +19,8 @@ async function updateSessionsLimit() { try { - await sdk.forConsole.projects.updateAuthSessionsLimit({ - projectId: project.$id, - limit: maxSessions + await sdk.forProject(project.region, project.$id).project.updateSessionLimitPolicy({ + total: maxSessions }); await invalidate(Dependencies.PROJECT); diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte index 5296362cb..63a51144e 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte @@ -31,9 +31,8 @@ async function updateLimit() { try { - await sdk.forConsole.projects.updateAuthLimit({ - projectId: project?.$id, - limit: isLimited ? newLimit : 0 + await sdk.forProject(project.region, project.$id).project.updateUserLimitPolicy({ + total: isLimited ? newLimit : 0 }); await invalidate(Dependencies.PROJECT); addNotification({ diff --git a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte index 46f8a745b..c086f5b55 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte @@ -1,19 +1,31 @@ @@ -75,7 +67,12 @@ templateType = type; isTemplateLoading = true; - $emailTemplate = await loadEmailTemplate(page.params.project, type, EmailTemplateLocale.En); + $emailTemplate = await loadEmailTemplate( + page.params.region, + page.params.project, + type, + EmailTemplateLocale.En + ); $baseEmailTemplate = { ...$emailTemplate }; isTemplateLoading = false; diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/email2FATemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/email2FATemplate.svelte index 0cea45a33..92b6f50d0 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/email2FATemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/email2FATemplate.svelte @@ -22,6 +22,7 @@ }, 1000); try { const template = await loadEmailTemplate( + project.region, project.$id, EmailTemplateType.MfaChallenge, locale diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/emailInviteTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/emailInviteTemplate.svelte index 38ec9c54c..ceba2d881 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/emailInviteTemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/emailInviteTemplate.svelte @@ -22,6 +22,7 @@ }, 1000); try { const template = await loadEmailTemplate( + project.region, project.$id, EmailTemplateType.Invitation, locale diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/emailMagicUrlTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/emailMagicUrlTemplate.svelte index 62c79c9d3..76c445847 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/emailMagicUrlTemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/emailMagicUrlTemplate.svelte @@ -22,6 +22,7 @@ }, 1000); try { const template = await loadEmailTemplate( + project.region, project.$id, EmailTemplateType.MagicSession, locale diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/emailOtpSessionTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/emailOtpSessionTemplate.svelte index 76bb1ef5d..8397749a0 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/emailOtpSessionTemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/emailOtpSessionTemplate.svelte @@ -22,6 +22,7 @@ }, 1000); try { const template = await loadEmailTemplate( + project.region, project.$id, EmailTemplateType.OtpSession, locale diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/emailRecoveryTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/emailRecoveryTemplate.svelte index b49a333e1..cd0dab8a7 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/emailRecoveryTemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/emailRecoveryTemplate.svelte @@ -22,6 +22,7 @@ }, 1000); try { const template = await loadEmailTemplate( + project.region, project.$id, EmailTemplateType.Recovery, locale diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/emailSessionAlertTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/emailSessionAlertTemplate.svelte index e8b8f49de..46be203f0 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/emailSessionAlertTemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/emailSessionAlertTemplate.svelte @@ -22,6 +22,7 @@ }, 1000); try { const template = await loadEmailTemplate( + project.region, project.$id, EmailTemplateType.SessionAlert, locale diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/emailTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/emailTemplate.svelte index cd5bb6958..7ed15b39a 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/emailTemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/emailTemplate.svelte @@ -2,15 +2,10 @@ import { Button, Form, InputEmail, InputText, InputTextarea } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import ResetEmail from './resetEmail.svelte'; import { baseEmailTemplate, emailTemplate } from './store'; import deepEqual from 'deep-equal'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; - import { - type EmailTemplateLocale, - type EmailTemplateType, - type Models - } from '@appwrite.io/console'; + import { EmailTemplateLocale, type EmailTemplateType, type Models } from '@appwrite.io/console'; import { Icon, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte'; import { IconInfo } from '@appwrite.io/pink-icons-svelte'; import TemplateSkeleton from './templateSkeleton.svelte'; @@ -28,20 +23,13 @@ children: Snippet; } = $props(); - let openResetModal = $state(false); let eventType = $state(Submit.EmailUpdateInviteTemplate); const isSmtpEnabled = $derived(project?.smtpEnabled); const isButtonDisabled = $derived(deepEqual($emailTemplate, $baseEmailTemplate)); async function saveEmailTemplate() { - if (!$emailTemplate.locale) { - addNotification({ - type: 'error', - message: 'Locale is required' - }); - return; - } + const locale = ($emailTemplate.locale || EmailTemplateLocale.En) as EmailTemplateLocale; // TODO: uncomment after SDK is updated // if (!isValueOfStringEnum(TemplateType, $emailTemplate.type)) { @@ -65,16 +53,15 @@ eventType = Submit.EmailUpdateVerificationTemplate; break; } - // TODO: fix TemplateType and TemplateLocale typing once SDK is updated - await sdk.forConsole.projects.updateEmailTemplate({ - projectId: project.$id, - type: $emailTemplate.type as EmailTemplateType, - locale: $emailTemplate.locale as EmailTemplateLocale, + await sdk.forProject(project.region, project.$id).project.updateEmailTemplate({ + templateId: $emailTemplate.type as EmailTemplateType, + locale, subject: $emailTemplate.subject || undefined, message: $emailTemplate.message || undefined, senderName: $emailTemplate.senderName || undefined, senderEmail: $emailTemplate.senderEmail || undefined, - replyTo: $emailTemplate.replyTo || undefined + replyToEmail: $emailTemplate.replyToEmail || undefined, + replyToName: $emailTemplate.replyToName || undefined }); $baseEmailTemplate = { @@ -119,11 +106,17 @@ disabled={!isSmtpEnabled} /> + + {#if children}

Click to copy variables for the fields below. Learn more

-
- - diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/emailVerificationTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/emailVerificationTemplate.svelte index 4f5f9d953..8d88c7805 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/emailVerificationTemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/emailVerificationTemplate.svelte @@ -22,6 +22,7 @@ }, 1000); try { const template = await loadEmailTemplate( + project.region, project.$id, EmailTemplateType.Verification, locale diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/localeOptions.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/localeOptions.svelte index 08613e159..c033f4053 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/localeOptions.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/localeOptions.svelte @@ -18,5 +18,4 @@ placeholder="Select a language" name="locale" bind:value - required {options} /> diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/resetEmail.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/resetEmail.svelte deleted file mode 100644 index b61c6278b..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/resetEmail.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - - - Reset email template? - - -

- Are you sure you want to reset the email template? - Default values will be set in all inputs. -

- - {#if error} - - {/if} -
- - - - - - - -
diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/resetSms.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/resetSms.svelte deleted file mode 100644 index 7bbed915a..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/resetSms.svelte +++ /dev/null @@ -1,53 +0,0 @@ - diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/smsInvitationTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/smsInvitationTemplate.svelte deleted file mode 100644 index f6967df05..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/smsInvitationTemplate.svelte +++ /dev/null @@ -1,45 +0,0 @@ - diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/smsLoginTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/smsLoginTemplate.svelte deleted file mode 100644 index d3b4cf55e..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/smsLoginTemplate.svelte +++ /dev/null @@ -1,46 +0,0 @@ - diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/smsTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/smsTemplate.svelte deleted file mode 100644 index 6c14adec5..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/smsTemplate.svelte +++ /dev/null @@ -1,96 +0,0 @@ - diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/smsVerificationTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/smsVerificationTemplate.svelte deleted file mode 100644 index adcb0b519..000000000 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/smsVerificationTemplate.svelte +++ /dev/null @@ -1,45 +0,0 @@ - diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/store.ts b/src/routes/(console)/project-[region]-[project]/auth/templates/store.ts index 9ae9f838b..52bdaa70d 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/store.ts +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/store.ts @@ -1,5 +1,5 @@ import { writable } from 'svelte/store'; -import { EmailTemplateType, type Models } from '@appwrite.io/console'; +import { EmailTemplateType, type EmailTemplateLocale, type Models } from '@appwrite.io/console'; // component imports import Email2FaTemplate from './email2FATemplate.svelte'; @@ -10,38 +10,36 @@ import EmailOtpSessionTemplate from './emailOtpSessionTemplate.svelte'; import EmailVerificationTemplate from './emailVerificationTemplate.svelte'; import EmailSessionAlertTemplate from './emailSessionAlertTemplate.svelte'; -export const emailTemplate = writable({ +export type EmailTemplateForm = Models.EmailTemplate & { + type: EmailTemplateType | null; + templateId: EmailTemplateType | string | null; + locale: EmailTemplateLocale | string | null; +}; + +export const emailTemplate = writable({ + templateId: null, type: null, locale: null, message: null, senderName: '', senderEmail: null, - replyTo: null, + replyToEmail: null, + replyToName: null, subject: null }); -export const baseEmailTemplate = writable({ +export const baseEmailTemplate = writable({ + templateId: null, type: null, locale: null, message: null, senderName: null, senderEmail: null, - replyTo: null, + replyToEmail: null, + replyToName: null, subject: null }); -export const smsTemplate = writable({ - type: null, - locale: null, - message: null -}); - -export const baseSmsTemplate = writable({ - type: null, - locale: null, - message: null -}); - export const templates = [ { key: EmailTemplateType.Verification, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte index 6b035712f..0e787e136 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte @@ -88,7 +88,7 @@ return [...idColumn, ...columns.filter((column) => !column.isAction), ...systemColumns]; } - $: selected = preferences.getCustomTableColumns(page.params.table); + $: selected = preferences.getCustomTableColumns(table.$id); $: if (table.fields) { const freshColumns = createTableColumns(table.fields, selected); @@ -154,7 +154,7 @@ onDestroy(() => ($showCreateColumnSheet.show = false)); -{#key page.params.table} +{#key table.$id} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte index 7fdf0470e..7eb17505e 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte @@ -22,7 +22,8 @@ indexes, isTablesCsvImportInProgress, reorderItems, - showCreateIndexSheet + showCreateIndexSheet, + INTERNAL_ACTIONS_COLUMN_ID } from '../store'; import EditColumn from './edit.svelte'; import DeleteColumn from './deleteColumn.svelte'; @@ -335,7 +336,7 @@ minimumWidth: 200, resizable: true }, - { id: 'actions', width: 40, isAction: true, resizable: false } + { id: INTERNAL_ACTIONS_COLUMN_ID, width: 40, isAction: true, resizable: false } ]); $effect(() => { @@ -387,7 +388,7 @@ Indexed Default value - + {#each updatedColumnsForSheet as column, index (column.key)} @@ -543,7 +544,7 @@ {_default} {/if} - + {#if $isTablesCsvImportInProgress}