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}
+
+
+
+
+
+ {item.$id}
+
+
+
+
+ {#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 @@
-
+ {/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 @@
-
-
-
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}