Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 89 additions & 78 deletions bun.lock

Large diffs are not rendered by default.

55 changes: 28 additions & 27 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@
"@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",
"@codemirror/autocomplete": "^6.20.2",
"@codemirror/commands": "^6.10.3",
"@codemirror/language": "^6.12.3",
"@codemirror/lint": "^6.9.6",
"@codemirror/search": "^6.7.0",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.43.0",
"@faker-js/faker": "^9.9.0",
"@lezer/highlight": "^1.2.1",
"@plausible-analytics/tracker": "^0.4.4",
"@lezer/highlight": "^1.2.3",
"@plausible-analytics/tracker": "^0.4.5",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.55.1",
"@sentry/sveltekit": "^8.55.2",
"@stripe/stripe-js": "^3.5.0",
"@threlte/core": "^8.5.2",
"@threlte/extras": "^9.13.0",
"ai": "^6.0.138",
"@threlte/core": "^8.5.14",
"@threlte/extras": "^9.18.0",
"ai": "^6.0.184",
"analytics": "^0.8.19",
"codemirror-json5": "^1.0.3",
"cron-parser": "^4.9.0",
Expand All @@ -50,7 +50,7 @@
"flatted": "^3.4.2",
"ignore": "^6.0.2",
"json5": "^2.2.3",
"nanoid": "^5.1.7",
"nanoid": "^5.1.11",
"nanotar": "^0.3.0",
"pretty-bytes": "^6.1.1",
"remarkable": "^2.0.1",
Expand All @@ -61,12 +61,12 @@
"devDependencies": {
"@eslint/compat": "^1.4.1",
"@eslint/js": "^9.39.4",
"@lezer/common": "^1.5.0",
"@lezer/common": "^1.5.2",
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.86.6",
"@playwright/test": "^1.58.2",
"@playwright/test": "^1.60.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.57.1",
"@sveltejs/kit": "^2.60.1",
"@sveltejs/vite-plugin-svelte": "^5.1.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
Expand All @@ -75,27 +75,27 @@
"@types/deep-equal": "^1.0.4",
"@types/remarkable": "^2.0.8",
"@types/three": "^0.182.0",
"@typescript-eslint/eslint-plugin": "^8.57.2",
"@typescript-eslint/parser": "^8.57.2",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"@vitest/ui": "^3.2.4",
"color": "^5.0.3",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.16.0",
"eslint-plugin-svelte": "^3.17.1",
"globals": "^16.5.0",
"jsdom": "^26.1.0",
"kleur": "^4.1.5",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.5.1",
"sass": "^1.98.0",
"svelte": "^5.55.0",
"svelte-check": "^4.4.5",
"prettier": "^3.8.3",
"prettier-plugin-svelte": "^3.5.2",
"sass": "^1.99.0",
"svelte": "^5.55.7",
"svelte-check": "^4.4.8",
"svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.3",
"tldts": "^7.0.27",
"tldts": "^7.0.30",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.2",
"typescript-eslint": "^8.59.3",
"vite": "^7.3.1",
"vitest": "^3.2.4"
},
Expand All @@ -105,6 +105,7 @@
"brace-expansion": ">=5.0.5",
"immutable": "^5.1.5",
"flatted": "^3.4.2",
"devalue": "^5.8.1",
"yaml": "^1.10.3",
"picomatch": "^4.0.4",
"cookie": "^0.7.0"
Expand Down
47 changes: 42 additions & 5 deletions src/lib/appwrite/impersonation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { writable } from 'svelte/store';
import { building } from '$app/environment';
import { derived, writable } from 'svelte/store';
import { browser, building } from '$app/environment';

const KEY_TARGET_USER_ID = 'console.impersonation.targetUserId';
const KEY_OPERATOR = 'console.impersonation.operator';
Expand All @@ -17,6 +17,9 @@ export type TargetSnapshot = {
email: string;
};

type ResourceUrl = string | URL;
type ResourceQueryParams = Record<string, string | number | boolean | undefined>;

/**
* Incrementing revision triggers reactive re-fetches after impersonation changes.
* Consumers can depend() on this or subscribe to it.
Expand Down Expand Up @@ -67,7 +70,7 @@ export function clearPersistedImpersonation(): void {
}

export function readTargetSnapshot(): TargetSnapshot | null {
if (building) return null;
if (building || !browser) return null;
const raw = sessionStorage.getItem(KEY_TARGET);
if (!raw) return null;
try {
Expand All @@ -78,12 +81,46 @@ export function readTargetSnapshot(): TargetSnapshot | null {
}

export function readImpersonationTargetUserId(): string | null {
if (building) return null;
if (building || !browser) return null;
return sessionStorage.getItem(KEY_TARGET_USER_ID);
}

export function createImpersonatedResourceUrl(
url: ResourceUrl,
queryParams: ResourceQueryParams = {}
): string {
const urlString = url.toString();
const baseUrl = browser ? window.location.origin : 'http://localhost';
const parsedUrl = new URL(urlString, baseUrl);
const targetUserId = readImpersonationTargetUserId();
Comment thread
greptile-apps[bot] marked this conversation as resolved.

for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
parsedUrl.searchParams.set(key, value.toString());
}
}

const isAbsoluteUrl = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(urlString);
const serializedUrl =
!browser && !isAbsoluteUrl
? `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`
: parsedUrl.toString();

if (!targetUserId) return serializedUrl;

parsedUrl.searchParams.set('impersonateUserId', targetUserId);

return parsedUrl.toString();
}

export const impersonatedResourceUrl = derived(
impersonationRevision,
() => (url: ResourceUrl, queryParams?: ResourceQueryParams) =>
createImpersonatedResourceUrl(url, queryParams)
);
Comment thread
greptile-apps[bot] marked this conversation as resolved.

export function readOperatorSnapshot(): OperatorSnapshot | null {
if (building) return null;
if (building || !browser) return null;
const raw = sessionStorage.getItem(KEY_OPERATOR);
if (!raw) return null;
try {
Expand Down
11 changes: 8 additions & 3 deletions src/lib/components/billing/alerts/paymentAuthRequired.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@
import { page } from '$app/state';
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
import { impersonatedResourceUrl } from '$lib/appwrite/impersonation';
import { actionRequiredInvoices, hideBillingHeaderRoutes } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { getApiEndpoint } from '$lib/stores/sdk';
const endpoint = getApiEndpoint();

function invoiceUrl(invoiceId: string) {
return $impersonatedResourceUrl(
`${endpoint}/organizations/${$organization.$id}/invoices/${invoiceId}/view`
);
}
</script>

{#if $actionRequiredInvoices && $actionRequiredInvoices?.invoices?.length && !hideBillingHeaderRoutes.includes(page.url.pathname)}
<HeaderAlert title="Authorization required" type="error">
Please authorize your upcoming payment for {$organization.name}. Your bank requires this
security measure to proceed with payment.
<svelte:fragment slot="buttons">
<Button
text
href={`${endpoint}/organizations/${$organization.$id}/invoices/${$actionRequiredInvoices.invoices[0].$id}/view`}>
<Button text href={invoiceUrl($actionRequiredInvoices.invoices[0].$id)}>
View invoice
</Button>
<Button
Expand Down
19 changes: 9 additions & 10 deletions src/lib/components/filePicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import { addNotification } from '$lib/stores/notifications';
import { isCloud } from '$lib/system';
import { currentPlan } from '$lib/stores/organization';
import { impersonatedResourceUrl } from '$lib/appwrite/impersonation';

export let show: boolean;
export let mimeTypeQuery: string = 'image/';
Expand Down Expand Up @@ -82,16 +83,14 @@
}

function getPreview(bucketId: string, fileId: string, size: number = 64) {
return (
sdk
.forProject(page.params.region, page.params.project)
.storage.getFilePreview({
bucketId,
fileId,
width: size,
height: size
})
.toString() + '&mode=admin'
return $impersonatedResourceUrl(
sdk.forProject(page.params.region, page.params.project).storage.getFilePreview({
bucketId,
fileId,
width: size,
height: size
}),
{ mode: 'admin' }
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { type Models, Query } from '@appwrite.io/console';
import { trackEvent } from '$lib/actions/analytics';
import { selectedInvoice, showRetryModal } from './store';
import { impersonatedResourceUrl } from '$lib/appwrite/impersonation';
import {
ActionMenu,
Badge,
Expand Down Expand Up @@ -64,6 +65,12 @@
$showRetryModal = true;
}

function invoiceUrl(invoiceId: string, action: 'view' | 'download') {
return $impersonatedResourceUrl(
`${endpoint}/organizations/${page.params.organization}/invoices/${invoiceId}/${action}`
);
}

$effect(() => {
if (page.url.searchParams.get('type') === 'validate-invoice') {
window.history.replaceState({}, '', page.url.pathname);
Expand Down Expand Up @@ -155,12 +162,12 @@
<ActionMenu.Item.Anchor
leadingIcon={IconExternalLink}
external
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
href={invoiceUrl(invoice.$id, 'view')}>
View invoice
</ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor
leadingIcon={IconDownload}
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}>
href={invoiceUrl(invoice.$id, 'download')}>
Download PDF
</ActionMenu.Item.Anchor>
{#if status === 'overdue' || status === 'failed' || status === 'abandoned'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { getApiEndpoint, sdk } from '$lib/stores/sdk';
import { formatCurrency } from '$lib/helpers/numbers';
import { resolve } from '$app/paths';
import { impersonatedResourceUrl } from '$lib/appwrite/impersonation';
import type { PaymentMethod as StripePaymentMethod } from '@stripe/stripe-js';
import type { Models } from '@appwrite.io/console';

Expand All @@ -37,6 +38,12 @@

const endpoint = getApiEndpoint();

function invoiceUrl(invoiceId: string) {
return $impersonatedResourceUrl(
`${endpoint}/organizations/${page.params.organization}/invoices/${invoiceId}/view`
);
}

onMount(async () => {
if (!$organization.paymentMethodId && !$organization.backupPaymentMethodId) {
paymentMethodId = $paymentMethods?.total ? $paymentMethods.paymentMethods[0].$id : null;
Expand Down Expand Up @@ -173,11 +180,7 @@
)} has failed. Retry your payment to avoid service interruptions with your projects.
</p>

<Button
external
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
View invoice
</Button>
<Button external href={invoiceUrl(invoice.$id)}>View invoice</Button>

<PaymentBoxes
bind:paymentMethod
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getApiEndpoint, sdk } from '$lib/stores/sdk';
import { createImpersonatedResourceUrl } from '$lib/appwrite/impersonation';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';

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

return redirect(
302,
`${endpoint}/organizations/${params.organization}/invoices/${invoice.$id}/download`
createImpersonatedResourceUrl(
`${endpoint}/organizations/${params.organization}/invoices/${invoice.$id}/download`
)
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getApiEndpoint, sdk } from '$lib/stores/sdk';
import { createImpersonatedResourceUrl } from '$lib/appwrite/impersonation';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';

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

return redirect(
302,
`${endpoint}/organizations/${params.organization}/invoices/${invoice.$id}/view`
createImpersonatedResourceUrl(
`${endpoint}/organizations/${params.organization}/invoices/${invoice.$id}/view`
)
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { Button } from '$lib/elements/forms';
import { formatCurrency } from '$lib/helpers/numbers';
import { trackEvent } from '$lib/actions/analytics';
import { impersonatedResourceUrl } from '$lib/appwrite/impersonation';
import { ActionMenu, Badge, Icon, Link, Popover, Table } from '@appwrite.io/pink-svelte';
import {
IconDotsHorizontal,
Expand All @@ -24,6 +25,12 @@
$selectedInvoice = invoice;
$showRetryModal = true;
}

function invoiceUrl(invoiceId: string, action: 'view' | 'download') {
return $impersonatedResourceUrl(
`${endpoint}/organizations/${page.params.organization}/invoices/${invoiceId}/${action}`
);
}
</script>

<Table.Root
Expand Down Expand Up @@ -106,12 +113,12 @@
<ActionMenu.Item.Anchor
trailingIcon={IconExternalLink}
external
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
href={invoiceUrl(invoice.$id, 'view')}>
View invoice
</ActionMenu.Item.Anchor>
<ActionMenu.Item.Anchor
trailingIcon={IconDownload}
href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}>
href={invoiceUrl(invoice.$id, 'download')}>
Download PDF
</ActionMenu.Item.Anchor>
{#if status === 'overdue' || status === 'failed'}
Expand Down
Loading