Skip to content

Commit 0bcef52

Browse files
authored
Merge pull request #3047 from appwrite/fix-impersonated-resource-urls
fix: add impersonation params to resource urls
2 parents c4f6f79 + cf9dba6 commit 0bcef52

17 files changed

Lines changed: 283 additions & 185 deletions

File tree

bun.lock

Lines changed: 89 additions & 78 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,22 @@
2525
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3",
2626
"@appwrite.io/pink-legacy": "^1.0.3",
2727
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8dcaa17",
28-
"@codemirror/autocomplete": "^6.19.0",
29-
"@codemirror/commands": "^6.9.0",
30-
"@codemirror/language": "^6.11.3",
31-
"@codemirror/lint": "^6.9.0",
32-
"@codemirror/search": "^6.5.11",
33-
"@codemirror/state": "^6.5.2",
34-
"@codemirror/view": "^6.38.6",
28+
"@codemirror/autocomplete": "^6.20.2",
29+
"@codemirror/commands": "^6.10.3",
30+
"@codemirror/language": "^6.12.3",
31+
"@codemirror/lint": "^6.9.6",
32+
"@codemirror/search": "^6.7.0",
33+
"@codemirror/state": "^6.6.0",
34+
"@codemirror/view": "^6.43.0",
3535
"@faker-js/faker": "^9.9.0",
36-
"@lezer/highlight": "^1.2.1",
37-
"@plausible-analytics/tracker": "^0.4.4",
36+
"@lezer/highlight": "^1.2.3",
37+
"@plausible-analytics/tracker": "^0.4.5",
3838
"@popperjs/core": "^2.11.8",
39-
"@sentry/sveltekit": "^8.55.1",
39+
"@sentry/sveltekit": "^8.55.2",
4040
"@stripe/stripe-js": "^3.5.0",
41-
"@threlte/core": "^8.5.2",
42-
"@threlte/extras": "^9.13.0",
43-
"ai": "^6.0.138",
41+
"@threlte/core": "^8.5.14",
42+
"@threlte/extras": "^9.18.0",
43+
"ai": "^6.0.184",
4444
"analytics": "^0.8.19",
4545
"codemirror-json5": "^1.0.3",
4646
"cron-parser": "^4.9.0",
@@ -50,7 +50,7 @@
5050
"flatted": "^3.4.2",
5151
"ignore": "^6.0.2",
5252
"json5": "^2.2.3",
53-
"nanoid": "^5.1.7",
53+
"nanoid": "^5.1.11",
5454
"nanotar": "^0.3.0",
5555
"pretty-bytes": "^6.1.1",
5656
"remarkable": "^2.0.1",
@@ -61,12 +61,12 @@
6161
"devDependencies": {
6262
"@eslint/compat": "^1.4.1",
6363
"@eslint/js": "^9.39.4",
64-
"@lezer/common": "^1.5.0",
64+
"@lezer/common": "^1.5.2",
6565
"@melt-ui/pp": "^0.3.2",
6666
"@melt-ui/svelte": "^0.86.6",
67-
"@playwright/test": "^1.58.2",
67+
"@playwright/test": "^1.60.0",
6868
"@sveltejs/adapter-static": "^3.0.10",
69-
"@sveltejs/kit": "^2.57.1",
69+
"@sveltejs/kit": "^2.60.1",
7070
"@sveltejs/vite-plugin-svelte": "^5.1.1",
7171
"@testing-library/dom": "^10.4.1",
7272
"@testing-library/jest-dom": "^6.9.1",
@@ -75,27 +75,27 @@
7575
"@types/deep-equal": "^1.0.4",
7676
"@types/remarkable": "^2.0.8",
7777
"@types/three": "^0.182.0",
78-
"@typescript-eslint/eslint-plugin": "^8.57.2",
79-
"@typescript-eslint/parser": "^8.57.2",
78+
"@typescript-eslint/eslint-plugin": "^8.59.3",
79+
"@typescript-eslint/parser": "^8.59.3",
8080
"@vitest/ui": "^3.2.4",
8181
"color": "^5.0.3",
8282
"eslint": "^9.39.4",
8383
"eslint-config-prettier": "^10.1.8",
84-
"eslint-plugin-svelte": "^3.16.0",
84+
"eslint-plugin-svelte": "^3.17.1",
8585
"globals": "^16.5.0",
8686
"jsdom": "^26.1.0",
8787
"kleur": "^4.1.5",
88-
"prettier": "^3.8.1",
89-
"prettier-plugin-svelte": "^3.5.1",
90-
"sass": "^1.98.0",
91-
"svelte": "^5.55.0",
92-
"svelte-check": "^4.4.5",
88+
"prettier": "^3.8.3",
89+
"prettier-plugin-svelte": "^3.5.2",
90+
"sass": "^1.99.0",
91+
"svelte": "^5.55.7",
92+
"svelte-check": "^4.4.8",
9393
"svelte-preprocess": "^6.0.3",
9494
"svelte-sequential-preprocessor": "^2.0.3",
95-
"tldts": "^7.0.27",
95+
"tldts": "^7.0.30",
9696
"tslib": "^2.8.1",
9797
"typescript": "^5.9.3",
98-
"typescript-eslint": "^8.57.2",
98+
"typescript-eslint": "^8.59.3",
9999
"vite": "^7.3.1",
100100
"vitest": "^3.2.4"
101101
},
@@ -105,6 +105,7 @@
105105
"brace-expansion": ">=5.0.5",
106106
"immutable": "^5.1.5",
107107
"flatted": "^3.4.2",
108+
"devalue": "^5.8.1",
108109
"yaml": "^1.10.3",
109110
"picomatch": "^4.0.4",
110111
"cookie": "^0.7.0"

src/lib/appwrite/impersonation.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { writable } from 'svelte/store';
2-
import { building } from '$app/environment';
1+
import { derived, writable } from 'svelte/store';
2+
import { browser, building } from '$app/environment';
33

44
const KEY_TARGET_USER_ID = 'console.impersonation.targetUserId';
55
const KEY_OPERATOR = 'console.impersonation.operator';
@@ -17,6 +17,9 @@ export type TargetSnapshot = {
1717
email: string;
1818
};
1919

20+
type ResourceUrl = string | URL;
21+
type ResourceQueryParams = Record<string, string | number | boolean | undefined>;
22+
2023
/**
2124
* Incrementing revision triggers reactive re-fetches after impersonation changes.
2225
* Consumers can depend() on this or subscribe to it.
@@ -67,7 +70,7 @@ export function clearPersistedImpersonation(): void {
6770
}
6871

6972
export function readTargetSnapshot(): TargetSnapshot | null {
70-
if (building) return null;
73+
if (building || !browser) return null;
7174
const raw = sessionStorage.getItem(KEY_TARGET);
7275
if (!raw) return null;
7376
try {
@@ -78,12 +81,46 @@ export function readTargetSnapshot(): TargetSnapshot | null {
7881
}
7982

8083
export function readImpersonationTargetUserId(): string | null {
81-
if (building) return null;
84+
if (building || !browser) return null;
8285
return sessionStorage.getItem(KEY_TARGET_USER_ID);
8386
}
8487

88+
export function createImpersonatedResourceUrl(
89+
url: ResourceUrl,
90+
queryParams: ResourceQueryParams = {}
91+
): string {
92+
const urlString = url.toString();
93+
const baseUrl = browser ? window.location.origin : 'http://localhost';
94+
const parsedUrl = new URL(urlString, baseUrl);
95+
const targetUserId = readImpersonationTargetUserId();
96+
97+
for (const [key, value] of Object.entries(queryParams)) {
98+
if (value !== undefined) {
99+
parsedUrl.searchParams.set(key, value.toString());
100+
}
101+
}
102+
103+
const isAbsoluteUrl = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(urlString);
104+
const serializedUrl =
105+
!browser && !isAbsoluteUrl
106+
? `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`
107+
: parsedUrl.toString();
108+
109+
if (!targetUserId) return serializedUrl;
110+
111+
parsedUrl.searchParams.set('impersonateUserId', targetUserId);
112+
113+
return parsedUrl.toString();
114+
}
115+
116+
export const impersonatedResourceUrl = derived(
117+
impersonationRevision,
118+
() => (url: ResourceUrl, queryParams?: ResourceQueryParams) =>
119+
createImpersonatedResourceUrl(url, queryParams)
120+
);
121+
85122
export function readOperatorSnapshot(): OperatorSnapshot | null {
86-
if (building) return null;
123+
if (building || !browser) return null;
87124
const raw = sessionStorage.getItem(KEY_OPERATOR);
88125
if (!raw) return null;
89126
try {

src/lib/components/billing/alerts/paymentAuthRequired.svelte

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,25 @@
33
import { page } from '$app/state';
44
import { Button } from '$lib/elements/forms';
55
import { HeaderAlert } from '$lib/layout';
6+
import { impersonatedResourceUrl } from '$lib/appwrite/impersonation';
67
import { actionRequiredInvoices, hideBillingHeaderRoutes } from '$lib/stores/billing';
78
import { organization } from '$lib/stores/organization';
89
import { getApiEndpoint } from '$lib/stores/sdk';
910
const endpoint = getApiEndpoint();
11+
12+
function invoiceUrl(invoiceId: string) {
13+
return $impersonatedResourceUrl(
14+
`${endpoint}/organizations/${$organization.$id}/invoices/${invoiceId}/view`
15+
);
16+
}
1017
</script>
1118

1219
{#if $actionRequiredInvoices && $actionRequiredInvoices?.invoices?.length && !hideBillingHeaderRoutes.includes(page.url.pathname)}
1320
<HeaderAlert title="Authorization required" type="error">
1421
Please authorize your upcoming payment for {$organization.name}. Your bank requires this
1522
security measure to proceed with payment.
1623
<svelte:fragment slot="buttons">
17-
<Button
18-
text
19-
href={`${endpoint}/organizations/${$organization.$id}/invoices/${$actionRequiredInvoices.invoices[0].$id}/view`}>
24+
<Button text href={invoiceUrl($actionRequiredInvoices.invoices[0].$id)}>
2025
View invoice
2126
</Button>
2227
<Button

src/lib/components/filePicker.svelte

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import { addNotification } from '$lib/stores/notifications';
3737
import { isCloud } from '$lib/system';
3838
import { currentPlan } from '$lib/stores/organization';
39+
import { impersonatedResourceUrl } from '$lib/appwrite/impersonation';
3940
4041
export let show: boolean;
4142
export let mimeTypeQuery: string = 'image/';
@@ -82,16 +83,14 @@
8283
}
8384
8485
function getPreview(bucketId: string, fileId: string, size: number = 64) {
85-
return (
86-
sdk
87-
.forProject(page.params.region, page.params.project)
88-
.storage.getFilePreview({
89-
bucketId,
90-
fileId,
91-
width: size,
92-
height: size
93-
})
94-
.toString() + '&mode=admin'
86+
return $impersonatedResourceUrl(
87+
sdk.forProject(page.params.region, page.params.project).storage.getFilePreview({
88+
bucketId,
89+
fileId,
90+
width: size,
91+
height: size
92+
}),
93+
{ mode: 'admin' }
9594
);
9695
}
9796

src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { type Models, Query } from '@appwrite.io/console';
1010
import { trackEvent } from '$lib/actions/analytics';
1111
import { selectedInvoice, showRetryModal } from './store';
12+
import { impersonatedResourceUrl } from '$lib/appwrite/impersonation';
1213
import {
1314
ActionMenu,
1415
Badge,
@@ -64,6 +65,12 @@
6465
$showRetryModal = true;
6566
}
6667
68+
function invoiceUrl(invoiceId: string, action: 'view' | 'download') {
69+
return $impersonatedResourceUrl(
70+
`${endpoint}/organizations/${page.params.organization}/invoices/${invoiceId}/${action}`
71+
);
72+
}
73+
6774
$effect(() => {
6875
if (page.url.searchParams.get('type') === 'validate-invoice') {
6976
window.history.replaceState({}, '', page.url.pathname);
@@ -155,12 +162,12 @@
155162
<ActionMenu.Item.Anchor
156163
leadingIcon={IconExternalLink}
157164
external
158-
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
165+
href={invoiceUrl(invoice.$id, 'view')}>
159166
View invoice
160167
</ActionMenu.Item.Anchor>
161168
<ActionMenu.Item.Anchor
162169
leadingIcon={IconDownload}
163-
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}>
170+
href={invoiceUrl(invoice.$id, 'download')}>
164171
Download PDF
165172
</ActionMenu.Item.Anchor>
166173
{#if status === 'overdue' || status === 'failed' || status === 'abandoned'}

src/routes/(console)/organization-[organization]/billing/retryPaymentModal.svelte

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import { getApiEndpoint, sdk } from '$lib/stores/sdk';
2121
import { formatCurrency } from '$lib/helpers/numbers';
2222
import { resolve } from '$app/paths';
23+
import { impersonatedResourceUrl } from '$lib/appwrite/impersonation';
2324
import type { PaymentMethod as StripePaymentMethod } from '@stripe/stripe-js';
2425
import type { Models } from '@appwrite.io/console';
2526
@@ -37,6 +38,12 @@
3738
3839
const endpoint = getApiEndpoint();
3940
41+
function invoiceUrl(invoiceId: string) {
42+
return $impersonatedResourceUrl(
43+
`${endpoint}/organizations/${page.params.organization}/invoices/${invoiceId}/view`
44+
);
45+
}
46+
4047
onMount(async () => {
4148
if (!$organization.paymentMethodId && !$organization.backupPaymentMethodId) {
4249
paymentMethodId = $paymentMethods?.total ? $paymentMethods.paymentMethods[0].$id : null;
@@ -173,11 +180,7 @@
173180
)} has failed. Retry your payment to avoid service interruptions with your projects.
174181
</p>
175182

176-
<Button
177-
external
178-
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
179-
View invoice
180-
</Button>
183+
<Button external href={invoiceUrl(invoice.$id)}>View invoice</Button>
181184

182185
<PaymentBoxes
183186
bind:paymentMethod

src/routes/(console)/organization-[organization]/invoices/[invoiceId]/download/+page.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getApiEndpoint, sdk } from '$lib/stores/sdk';
2+
import { createImpersonatedResourceUrl } from '$lib/appwrite/impersonation';
23
import { redirect } from '@sveltejs/kit';
34
import type { PageLoad } from './$types';
45

@@ -12,6 +13,8 @@ export const load: PageLoad = async ({ params }) => {
1213

1314
return redirect(
1415
302,
15-
`${endpoint}/organizations/${params.organization}/invoices/${invoice.$id}/download`
16+
createImpersonatedResourceUrl(
17+
`${endpoint}/organizations/${params.organization}/invoices/${invoice.$id}/download`
18+
)
1619
);
1720
};

src/routes/(console)/organization-[organization]/invoices/[invoiceId]/view/+page.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getApiEndpoint, sdk } from '$lib/stores/sdk';
2+
import { createImpersonatedResourceUrl } from '$lib/appwrite/impersonation';
23
import { redirect } from '@sveltejs/kit';
34
import type { PageLoad } from './$types';
45

@@ -13,6 +14,8 @@ export const load: PageLoad = async ({ params }) => {
1314

1415
return redirect(
1516
302,
16-
`${endpoint}/organizations/${params.organization}/invoices/${invoice.$id}/view`
17+
createImpersonatedResourceUrl(
18+
`${endpoint}/organizations/${params.organization}/invoices/${invoice.$id}/view`
19+
)
1720
);
1821
};

src/routes/(console)/organization-[organization]/settings/invoicesTable.svelte

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { Button } from '$lib/elements/forms';
66
import { formatCurrency } from '$lib/helpers/numbers';
77
import { trackEvent } from '$lib/actions/analytics';
8+
import { impersonatedResourceUrl } from '$lib/appwrite/impersonation';
89
import { ActionMenu, Badge, Icon, Link, Popover, Table } from '@appwrite.io/pink-svelte';
910
import {
1011
IconDotsHorizontal,
@@ -24,6 +25,12 @@
2425
$selectedInvoice = invoice;
2526
$showRetryModal = true;
2627
}
28+
29+
function invoiceUrl(invoiceId: string, action: 'view' | 'download') {
30+
return $impersonatedResourceUrl(
31+
`${endpoint}/organizations/${page.params.organization}/invoices/${invoiceId}/${action}`
32+
);
33+
}
2734
</script>
2835

2936
<Table.Root
@@ -106,12 +113,12 @@
106113
<ActionMenu.Item.Anchor
107114
trailingIcon={IconExternalLink}
108115
external
109-
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
116+
href={invoiceUrl(invoice.$id, 'view')}>
110117
View invoice
111118
</ActionMenu.Item.Anchor>
112119
<ActionMenu.Item.Anchor
113120
trailingIcon={IconDownload}
114-
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}>
121+
href={invoiceUrl(invoice.$id, 'download')}>
115122
Download PDF
116123
</ActionMenu.Item.Anchor>
117124
{#if status === 'overdue' || status === 'failed'}

0 commit comments

Comments
 (0)