diff --git a/bun.lock b/bun.lock index ff9de653b..5e452c947 100644 --- a/bun.lock +++ b/bun.lock @@ -1,11 +1,12 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@67539a6", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@f063676", "@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", @@ -112,7 +113,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@67539a6", { "dependencies": { "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@f063676", { "dependencies": { "json-bigint": "1.0.0" } }], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], diff --git a/package.json b/package.json index f14d08510..099ae9cb1 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@67539a6", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@f063676", "@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/stores/project-protocols.ts b/src/lib/stores/project-protocols.ts new file mode 100644 index 000000000..072c3c522 --- /dev/null +++ b/src/lib/stores/project-protocols.ts @@ -0,0 +1,44 @@ +import { writable } from 'svelte/store'; +import { ProtocolId, type Models } from '@appwrite.io/console'; + +export type Protocol = { + label: string; + method: ProtocolId; + value: boolean | null; +}; + +function projectProtocolRows(project: Models.Project | null): Protocol[] { + return [ + { + label: 'REST', + method: ProtocolId.Rest, + value: project?.protocolStatusForRest ?? null + }, + { + label: 'GraphQL', + method: ProtocolId.Graphql, + value: project?.protocolStatusForGraphql ?? null + }, + { + label: 'WebSocket', + method: ProtocolId.Websocket, + value: project?.protocolStatusForWebsocket ?? null + } + ]; +} + +function createProtocols() { + const { subscribe, set } = writable({ + list: projectProtocolRows(null) + }); + + return { + subscribe, + set, + load: (project: Models.Project) => { + set({ list: projectProtocolRows(project) }); + } + }; +} + +export const protocols = createProtocols(); diff --git a/src/lib/stores/project-services.ts b/src/lib/stores/project-services.ts index d9023f57f..7f835c7f9 100644 --- a/src/lib/stores/project-services.ts +++ b/src/lib/stores/project-services.ts @@ -1,120 +1,116 @@ import { writable } from 'svelte/store'; -import { ApiService, type Models } from '@appwrite.io/console'; +import { ServiceId, type Models } from '@appwrite.io/console'; export type Service = { label: string; - method: ApiService; + method: ServiceId; value: boolean | null; }; +function projectServiceRows(project: Models.Project | null): Service[] { + const rows: Service[] = [ + { + label: 'Account', + method: ServiceId.Account, + value: project?.serviceStatusForAccount ?? null + }, + { + label: 'Avatars', + method: ServiceId.Avatars, + value: project?.serviceStatusForAvatars ?? null + }, + { + label: 'Databases', + method: ServiceId.Databases, + value: project?.serviceStatusForDatabases ?? null + }, + { + label: 'Functions', + method: ServiceId.Functions, + value: project?.serviceStatusForFunctions ?? null + }, + { + label: 'GraphQL', + method: ServiceId.Graphql, + value: project?.serviceStatusForGraphql ?? null + }, + { + label: 'Health', + method: ServiceId.Health, + value: project?.serviceStatusForHealth ?? null + }, + { + label: 'Locale', + method: ServiceId.Locale, + value: project?.serviceStatusForLocale ?? null + }, + { + label: 'Messaging', + method: ServiceId.Messaging, + value: project?.serviceStatusForMessaging ?? null + }, + { + label: 'Migrations', + method: ServiceId.Migrations, + value: project?.serviceStatusForMigrations ?? null + }, + { + label: 'Project', + method: ServiceId.Project, + value: project?.serviceStatusForProject ?? null + }, + // @todo Re-enable when Proxy is ready for public release. + // { + // label: 'Proxy', + // method: ServiceId.Proxy, + // value: project?.serviceStatusForProxy ?? null + // }, + { + label: 'Sites', + method: ServiceId.Sites, + value: project?.serviceStatusForSites ?? null + }, + { + label: 'Storage', + method: ServiceId.Storage, + value: project?.serviceStatusForStorage ?? null + }, + { + label: 'TablesDB', + method: ServiceId.Tablesdb, + value: project?.serviceStatusForTablesdb ?? null + }, + { + label: 'Teams', + method: ServiceId.Teams, + value: project?.serviceStatusForTeams ?? null + }, + { + label: 'Users', + method: ServiceId.Users, + value: project?.serviceStatusForUsers ?? null + } + // @todo Re-enable when VCS is ready for public release. + // { + // label: 'VCS', + // method: ServiceId.Vcs, + // value: project?.serviceStatusForVcs ?? null + // } + ]; + + return rows.sort((a, b) => a.label.localeCompare(b.label)); +} + function createServices() { const { subscribe, set } = writable({ - list: [ - { - label: 'Account', - method: ApiService.Account, - value: null - }, - { - label: 'Avatars', - method: ApiService.Avatars, - value: null - }, - { - label: 'Databases', - method: ApiService.Databases, - value: null - }, - { - label: 'Functions', - method: ApiService.Functions, - value: null - }, - { - label: 'Locale', - method: ApiService.Locale, - value: null - }, - { - label: 'Messaging', - method: ApiService.Messaging, - value: null - }, - { - label: 'Storage', - method: ApiService.Storage, - value: null - }, - { - label: 'Teams', - method: ApiService.Teams, - value: null - }, - { - label: 'Users', - method: ApiService.Users, - value: null - }, - { - label: 'GraphQL', - method: ApiService.Graphql, - value: null - } - ] + list: projectServiceRows(null) }); return { subscribe, set, load: (project: Models.Project) => { - const list = [ - { - label: 'Account', - method: ApiService.Account, - value: project.serviceStatusForAccount - }, - { - label: 'Avatars', - method: ApiService.Avatars, - value: project.serviceStatusForAvatars - }, - { - label: 'Databases', - method: ApiService.Databases, - value: project.serviceStatusForDatabases - }, - { - label: 'Functions', - method: ApiService.Functions, - value: project.serviceStatusForFunctions - }, - { - label: 'Locale', - method: ApiService.Locale, - value: project.serviceStatusForLocale - }, - { - label: 'Messaging', - method: ApiService.Messaging, - value: project.serviceStatusForMessaging - }, - { - label: 'Storage', - method: ApiService.Storage, - value: project.serviceStatusForStorage - }, - { - label: 'Teams', - method: ApiService.Teams, - value: project.serviceStatusForTeams - }, - { - label: 'Users', - method: ApiService.Users, - value: project.serviceStatusForUsers - } - ]; - set({ list }); + set({ list: projectServiceRows(project) }); } }; } diff --git a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte index bed3bb541..823f5a6e9 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte @@ -7,6 +7,7 @@ import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; import UpdateName from './updateName.svelte'; + import UpdateProtocols from './updateProtocols.svelte'; import UpdateServices from './updateServices.svelte'; import UpdateInstallations from './updateInstallations.svelte'; import DeleteProject from './deleteProject.svelte'; @@ -89,6 +90,7 @@ {#if $canWriteProjects} + + import { invalidate } from '$app/navigation'; + import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; + import { CardGrid } from '$lib/components'; + import { Dependencies } from '$lib/constants'; + import { InputSwitch } from '$lib/elements/forms'; + import { addNotification } from '$lib/stores/notifications'; + import { protocols, type Protocol } from '$lib/stores/project-protocols'; + import { sdk } from '$lib/stores/sdk'; + import { project } from '../store'; + import Button from '$lib/elements/forms/button.svelte'; + import { Dialog, Divider, Layout, Spinner } from '@appwrite.io/pink-svelte'; + import { SvelteSet } from 'svelte/reactivity'; + import { ProtocolId } from '@appwrite.io/console'; + import { get } from 'svelte/store'; + + let isUpdatingAllProtocols = $state(false); + let showUpdateProtocolDialog = $state(false); + let updateProtocolsEnabledMode = $state(null); + let apiProtocolUpdates = new SvelteSet(); + const protocolDescriptions: Record = { + [ProtocolId.Rest]: 'Standard HTTP API requests from client SDKs.', + [ProtocolId.Graphql]: 'GraphQL API access for queries and mutations.', + [ProtocolId.Websocket]: 'Realtime subscriptions over WebSocket connections.' + }; + const isAnyProtocolUpdating = $derived(apiProtocolUpdates.size > 0); + const isAnyUpdateInProgress = $derived(isUpdatingAllProtocols || isAnyProtocolUpdating); + + const allProtocolsEnabled = $derived.by(() => { + if (isAnyUpdateInProgress) return false; + return $protocols.list.every((protocol) => protocol.value); + }); + + const allProtocolsDisabled = $derived.by(() => { + if (isAnyUpdateInProgress) return false; + return $protocols.list.every((protocol) => !protocol.value); + }); + + const shouldDisableEnableAllButton = $derived(isAnyUpdateInProgress || allProtocolsEnabled); + const shouldDisableDisableAllButton = $derived(isAnyUpdateInProgress || allProtocolsDisabled); + + async function protocolUpdate(protocol: Protocol) { + apiProtocolUpdates.add(protocol.method); + + try { + await sdk.forProject($project.region, $project.$id).project.updateProtocolStatus({ + protocolId: protocol.method, + enabled: protocol.value + }); + + await invalidate(Dependencies.PROJECT); + + addNotification({ + type: 'success', + message: `${protocol.label} protocol has been ${ + protocol.value ? 'enabled' : 'disabled' + }` + }); + trackEvent(Submit.ProjectService, { + method: protocol.method, + value: protocol.value + }); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + trackError(error, Submit.ProjectService); + } finally { + apiProtocolUpdates.delete(protocol.method); + } + } + + async function toggleAllProtocols(status: boolean) { + isUpdatingAllProtocols = true; + + try { + const projectSdk = sdk.forProject($project.region, $project.$id); + for (const protocol of get(protocols).list) { + if (protocol.value === status) continue; + await projectSdk.project.updateProtocolStatus({ + protocolId: protocol.method, + enabled: status + }); + } + + await invalidate(Dependencies.PROJECT); + + addNotification({ + type: 'success', + message: + 'All protocols for ' + + $project.name + + ' have been ' + + (status ? 'enabled.' : 'disabled.') + }); + trackEvent(Submit.ProjectService); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + trackError(error, Submit.ProjectService); + } finally { + isUpdatingAllProtocols = false; + showUpdateProtocolDialog = false; + updateProtocolsEnabledMode = null; + } + } + + const dialogDetails = $derived.by(() => { + if (updateProtocolsEnabledMode) { + return { + title: 'Enable all protocols', + message: 'All project protocols will be enabled.', + actionButton: 'Enable all' + }; + } else { + return { + title: 'Disable all protocols', + message: + 'Are you sure you want to disable all protocols? This will disable client access over those protocols until they are re-enabled.', + actionButton: 'Disable all' + }; + } + }); + + $effect(() => protocols.load($project)); + + + + Protocols + Choose which protocols clients can use to access your project. Disabled protocols remain unavailable + until re-enabled. + +
+
+ + + + + + + +
+ +
+ + {#each $protocols.list as protocol, index} +
+
+ protocolUpdate(protocol)} + disabled={apiProtocolUpdates.has(protocol.method)} /> + + {#if apiProtocolUpdates.has(protocol.method)} + + + + {/if} +
+
+ + {#if index < $protocols.list.length - 1} + + {/if} + {/each} +
+
+
+
+
+ + +

{dialogDetails.message}

+ + + + + + + +
+ + diff --git a/src/routes/(console)/project-[region]-[project]/settings/updateServices.svelte b/src/routes/(console)/project-[region]-[project]/settings/updateServices.svelte index e1f79b7d6..30cac2df3 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/updateServices.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/updateServices.svelte @@ -10,7 +10,8 @@ import { project } from '../store'; import Button from '$lib/elements/forms/button.svelte'; import { Dialog, Divider, Layout, Spinner } from '@appwrite.io/pink-svelte'; - import type { ApiService } from '@appwrite.io/console'; + import type { ServiceId } from '@appwrite.io/console'; + import { get } from 'svelte/store'; import { SvelteSet } from 'svelte/reactivity'; @@ -18,7 +19,7 @@ let showUpdateServiceDialog = $state(false); let updateServicesEnabledMode = $state(null); - let apiServiceUpdates = new SvelteSet(); + let apiServiceUpdates = new SvelteSet(); const isAnyServiceUpdating = $derived(apiServiceUpdates.size > 0); const isAnyUpdateInProgress = $derived(isUpdatingAllServices || isAnyServiceUpdating); @@ -41,10 +42,9 @@ apiServiceUpdates.add(service.method); try { - await sdk.forConsole.projects.updateServiceStatus({ - projectId: $project.$id, - service: service.method, - status: service.value + await sdk.forProject($project.region, $project.$id).project.updateServiceStatus({ + serviceId: service.method, + enabled: service.value }); await invalidate(Dependencies.PROJECT); @@ -74,10 +74,14 @@ isUpdatingAllServices = true; try { - await sdk.forConsole.projects.updateServiceStatusAll({ - projectId: $project.$id, - status - }); + const projectSdk = sdk.forProject($project.region, $project.$id); + for (const s of get(services).list) { + if (s.value === status) continue; + await projectSdk.project.updateServiceStatus({ + serviceId: s.method, + enabled: status + }); + } await invalidate(Dependencies.PROJECT); diff --git a/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/details.svelte b/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/details.svelte index 2907e35ea..25af29889 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/details.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/details.svelte @@ -30,10 +30,10 @@ name: $webhook.name, events: $webhook.events, url: $webhook.url, - security: $webhook.security, + tls: $webhook.tls, enabled, - httpUser: $webhook.httpUser || undefined, - httpPass: $webhook.httpPass || undefined + authUsername: $webhook.authUsername || undefined, + authPassword: $webhook.authPassword || undefined }); await invalidate(Dependencies.WEBHOOK); addNotification({ diff --git a/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/regenerate.svelte b/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/regenerate.svelte index 0018a7307..be924894d 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/regenerate.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/regenerate.svelte @@ -2,25 +2,47 @@ import { invalidate } from '$app/navigation'; import { page } from '$app/state'; import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; - import Confirm from '$lib/components/confirm.svelte'; + import Modal from '$lib/components/modal.svelte'; + import { Secret } from '$lib/components'; import { Dependencies } from '$lib/constants'; + import { Button, InputPassword } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import { webhook } from './store'; + import { Layout, Typography } from '@appwrite.io/pink-svelte'; + import type { Models } from '@appwrite.io/console'; + import { get } from 'svelte/store'; + import { webhook as webhookStore } from './store'; + + type WebhooksWithCustomSecret = { + updateSecret(params: { webhookId: string; secret?: string }): Promise; + }; export let show = false; + const projectId = page.params.project; + let secret = ''; + let revealedSecret = ''; + + $: if (!show) { + secret = ''; + revealedSecret = ''; + } async function regenerate() { try { - await sdk.forProject(page.params.region, projectId).webhooks.updateSignature({ - webhookId: $webhook.$id + const currentWebhook = get(webhookStore); + const customSecret = secret.trim(); + const updatedWebhook = await ( + sdk.forProject(page.params.region, projectId).webhooks as WebhooksWithCustomSecret + ).updateSecret({ + webhookId: currentWebhook.$id, + secret: customSecret || undefined }); await invalidate(Dependencies.WEBHOOK); - show = false; + revealedSecret = customSecret || updatedWebhook.secret; addNotification({ type: 'success', - message: 'Key has been regenerated' + message: customSecret ? 'Webhook secret updated.' : 'Webhook secret rotated.' }); trackEvent(Submit.WebhookUpdateSignature); } catch (error) { @@ -33,7 +55,38 @@ } - - Are you sure you want to generate a new Signature key? - You will not be able to recover the current key. - + + + {#if revealedSecret} + + This secret is only shown once after webhook creation or secret rotation. Copy it + now. + + + {:else} + + Leave this empty to rotate the webhook secret automatically, or enter a value to set + a custom secret. + + + Used to validate incoming webhook payloads. + + + {/if} + + + + {#if revealedSecret} + + {:else} + + + {/if} + + diff --git a/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/updateEvents.svelte b/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/updateEvents.svelte index 56094759b..73172df85 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/updateEvents.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/updateEvents.svelte @@ -31,10 +31,10 @@ name: $webhook.name, events: Array.from($eventSet), url: $webhook.url, - security: $webhook.security, + tls: $webhook.tls, enabled: true, - httpUser: $webhook.httpUser || undefined, - httpPass: $webhook.httpPass || undefined + authUsername: $webhook.authUsername || undefined, + authPassword: $webhook.authPassword || undefined }); await invalidate(Dependencies.WEBHOOK); areEventsDisabled = true; diff --git a/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/updateName.svelte b/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/updateName.svelte index 842b0a75f..0bebf0f1c 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/updateName.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/updateName.svelte @@ -24,10 +24,10 @@ name, events: $webhook.events, url: $webhook.url, - security: $webhook.security, + tls: $webhook.tls, enabled: true, - httpUser: $webhook.httpUser || undefined, - httpPass: $webhook.httpPass || undefined + authUsername: $webhook.authUsername || undefined, + authPassword: $webhook.authPassword || undefined }); await invalidate(Dependencies.WEBHOOK); diff --git a/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/updateSecurity.svelte b/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/updateSecurity.svelte index 7b801473c..6cd871088 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/updateSecurity.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/webhooks/[webhook]/updateSecurity.svelte @@ -12,14 +12,14 @@ import { Selector, Typography } from '@appwrite.io/pink-svelte'; const projectId = page.params.project; - let httpUser: string = null; - let httpPass: string = null; - let security = false; + let authUsername: string = null; + let authPassword: string = null; + let tls = false; onMount(async () => { - httpUser ??= $webhook.httpUser; - httpPass ??= $webhook.httpPass; - security = $webhook.security; + authUsername ??= $webhook.authUsername; + authPassword ??= $webhook.authPassword; + tls = $webhook.tls; }); async function updateSecurity() { @@ -29,10 +29,10 @@ name: $webhook.name, events: $webhook.events, url: $webhook.url, - security, + tls, enabled: true, - httpUser: httpUser || undefined, - httpPass: httpPass || undefined + authUsername: authUsername || undefined, + authPassword: authPassword || undefined }); await invalidate(Dependencies.WEBHOOK); addNotification({ @@ -60,18 +60,22 @@ HTTP Authentication

Use to secure your endpoint from untrusted sources.

- + + bind:value={authPassword} />