mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #2976 from appwrite/fix-project-services-region-and-sdk-list
fix(console): regional Projects API for service toggles + full ApiSer…
This commit is contained in:
@@ -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=="],
|
||||
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
@@ -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) });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
<UpdateName />
|
||||
<UpdateLabels />
|
||||
<UpdateProtocols />
|
||||
<UpdateServices />
|
||||
<UpdateInstallations {...data.installations} limit={data.limit} offset={data.offset} />
|
||||
<UpdateVariables
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<script lang="ts">
|
||||
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<boolean | null>(null);
|
||||
let apiProtocolUpdates = new SvelteSet<ProtocolId>();
|
||||
const protocolDescriptions: Record<ProtocolId, string> = {
|
||||
[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));
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Protocols</svelte:fragment>
|
||||
Choose which protocols clients can use to access your project. Disabled protocols remain unavailable
|
||||
until re-enabled.
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="protocols-list">
|
||||
<div class="protocol-toolbar">
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
<Button
|
||||
extraCompact
|
||||
on:click={() => {
|
||||
showUpdateProtocolDialog = true;
|
||||
updateProtocolsEnabledMode = true;
|
||||
}}
|
||||
disabled={shouldDisableEnableAllButton}>Enable all</Button>
|
||||
<span style:height="20px">
|
||||
<Divider vertical />
|
||||
</span>
|
||||
<Button
|
||||
extraCompact
|
||||
on:click={() => {
|
||||
showUpdateProtocolDialog = true;
|
||||
updateProtocolsEnabledMode = false;
|
||||
}}
|
||||
disabled={shouldDisableDisableAllButton}>Disable all</Button>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
<Divider />
|
||||
<div class="protocol-list-content">
|
||||
<Layout.Stack gap="xs">
|
||||
{#each $protocols.list as protocol, index}
|
||||
<div class="protocol-row">
|
||||
<div class="protocol-control">
|
||||
<InputSwitch
|
||||
id={protocol.method}
|
||||
label={protocol.label}
|
||||
description={protocolDescriptions[protocol.method]}
|
||||
bind:value={protocol.value}
|
||||
on:change={() => protocolUpdate(protocol)}
|
||||
disabled={apiProtocolUpdates.has(protocol.method)} />
|
||||
|
||||
{#if apiProtocolUpdates.has(protocol.method)}
|
||||
<span class="protocol-spinner">
|
||||
<Spinner size="s" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if index < $protocols.list.length - 1}
|
||||
<Divider />
|
||||
{/if}
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<Dialog title={dialogDetails.title} bind:open={showUpdateProtocolDialog}>
|
||||
<p class="text" data-private>{dialogDetails.message}</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
|
||||
<Button text on:click={() => (showUpdateProtocolDialog = false)}>Cancel</Button>
|
||||
|
||||
<Button
|
||||
secondary
|
||||
submissionLoader
|
||||
disabled={isUpdatingAllProtocols}
|
||||
forceShowLoader={isUpdatingAllProtocols}
|
||||
on:click={() => toggleAllProtocols(updateProtocolsEnabledMode)}>
|
||||
{dialogDetails.actionButton}
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Dialog>
|
||||
|
||||
<style>
|
||||
.protocols-list {
|
||||
max-width: 36rem;
|
||||
}
|
||||
|
||||
.protocol-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.protocol-row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.protocol-list-content {
|
||||
padding-top: var(--space-6);
|
||||
}
|
||||
|
||||
.protocol-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.protocol-control :global(label) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.protocol-spinner {
|
||||
opacity: 0.75;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -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<boolean | null>(null);
|
||||
|
||||
let apiServiceUpdates = new SvelteSet<ApiService>();
|
||||
let apiServiceUpdates = new SvelteSet<ServiceId>();
|
||||
|
||||
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);
|
||||
|
||||
|
||||
+3
-3
@@ -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({
|
||||
|
||||
+63
-10
@@ -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<Models.Webhook>;
|
||||
};
|
||||
|
||||
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 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Confirm title="Regenerate Key" bind:open={show} onSubmit={regenerate} action="Regenerate">
|
||||
Are you sure you want to generate a new Signature key?
|
||||
<b>You will not be able to recover the current key.</b>
|
||||
</Confirm>
|
||||
<Modal title="Rotate Webhook Secret" bind:show onSubmit={regenerate}>
|
||||
<Layout.Stack gap="l">
|
||||
{#if revealedSecret}
|
||||
<Typography.Text>
|
||||
This secret is only shown once after webhook creation or secret rotation. Copy it
|
||||
now.
|
||||
</Typography.Text>
|
||||
<Secret label="Secret" copyEvent="signature" bind:value={revealedSecret} />
|
||||
{:else}
|
||||
<Typography.Text>
|
||||
Leave this empty to rotate the webhook secret automatically, or enter a value to set
|
||||
a custom secret.
|
||||
</Typography.Text>
|
||||
<Typography.Text variant="m-400">
|
||||
Used to validate incoming webhook payloads.
|
||||
</Typography.Text>
|
||||
<InputPassword
|
||||
id="webhook-secret"
|
||||
label="Secret"
|
||||
placeholder="Leave empty to auto-generate"
|
||||
minlength={0}
|
||||
autocomplete
|
||||
bind:value={secret} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
{#if revealedSecret}
|
||||
<Button on:click={() => (show = false)}>Done</Button>
|
||||
{:else}
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>{secret.trim() ? 'Set custom secret' : 'Rotate secret'}</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
+3
-3
@@ -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;
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+20
-16
@@ -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 @@
|
||||
<Typography.Title size="s">HTTP Authentication</Typography.Title>
|
||||
<p class="text">Use to secure your endpoint from untrusted sources.</p>
|
||||
</div>
|
||||
<InputText label="User" id="user" placeholder="Enter username" bind:value={httpUser} />
|
||||
<InputText
|
||||
label="User"
|
||||
id="user"
|
||||
placeholder="Enter username"
|
||||
bind:value={authUsername} />
|
||||
<InputPassword
|
||||
label="Password"
|
||||
id="password"
|
||||
minlength={0}
|
||||
placeholder="Enter password"
|
||||
bind:value={httpPass} />
|
||||
bind:value={authPassword} />
|
||||
|
||||
<Selector.Checkbox
|
||||
id="security"
|
||||
id="tls"
|
||||
label="Certificate verification (SSL/TLS)"
|
||||
bind:checked={security}
|
||||
bind:checked={tls}
|
||||
description="Placeholder" />
|
||||
<!-- <span class="u-color-text-danger">Warning:</span> Untrusted or self-signed certificates
|
||||
may not be secure.
|
||||
@@ -85,9 +89,9 @@
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
disabled={httpUser === $webhook.httpUser &&
|
||||
httpPass === $webhook.httpPass &&
|
||||
security === $webhook.security}
|
||||
disabled={authUsername === $webhook.authUsername &&
|
||||
authPassword === $webhook.authPassword &&
|
||||
tls === $webhook.tls}
|
||||
submit>
|
||||
Update
|
||||
</Button>
|
||||
|
||||
+8
-9
@@ -1,26 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Secret } from '$lib/components';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Link } from '@appwrite.io/pink-svelte';
|
||||
import { Alert, Link } from '@appwrite.io/pink-svelte';
|
||||
import Regenerate from './regenerate.svelte';
|
||||
import { webhook } from './store';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
let showRegenerate = false;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Signature key</svelte:fragment>
|
||||
Add the Signature Key to the X-Appwrite-Webhook-Signature header to validate your webhooks.
|
||||
<svelte:fragment slot="title">Webhook secret</svelte:fragment>
|
||||
Used to validate incoming webhook payloads with the `X-Appwrite-Webhook-Signature` header.
|
||||
<Link.Anchor
|
||||
href="https://appwrite.io/docs/advanced/platform/webhooks#verification"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Learn more</Link.Anchor>
|
||||
<svelte:fragment slot="aside">
|
||||
<div>
|
||||
<Secret label="Key" copyEvent="signature" bind:value={$webhook.signatureKey} />
|
||||
</div>
|
||||
<Alert.Inline status="info">
|
||||
This secret is only shown once after webhook creation or secret rotation.
|
||||
</Alert.Inline>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
@@ -29,7 +28,7 @@
|
||||
trackEvent(Click.SettingsWebhookUpdateSignatureClick);
|
||||
}}
|
||||
secondary
|
||||
submit>Regenerate key</Button>
|
||||
submit>Rotate secret</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
|
||||
+3
-3
@@ -24,10 +24,10 @@
|
||||
name: $webhook.name,
|
||||
events: $webhook.events,
|
||||
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);
|
||||
|
||||
+47
-12
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import Modal from '$lib/components/modal.svelte';
|
||||
import { Secret } from '$lib/components';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
import { Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import Form from '$lib/elements/forms/form.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
@@ -15,15 +17,28 @@
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
|
||||
let showExitModal = false;
|
||||
let showSecretModal = false;
|
||||
let createdWebhookId = '';
|
||||
let createdSecret = '';
|
||||
|
||||
let name = '',
|
||||
events: string[] = [],
|
||||
url = '',
|
||||
security = true,
|
||||
httpUser = '',
|
||||
httpPass = '';
|
||||
tls = true,
|
||||
authUsername = '',
|
||||
authPassword = '';
|
||||
|
||||
export let data;
|
||||
|
||||
async function openWebhook() {
|
||||
showSecretModal = false;
|
||||
if (!createdWebhookId) return;
|
||||
|
||||
await goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/settings/webhooks/${createdWebhookId}`
|
||||
);
|
||||
}
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
const webhook = await sdk
|
||||
@@ -33,21 +48,28 @@
|
||||
name,
|
||||
events,
|
||||
url,
|
||||
security,
|
||||
tls,
|
||||
enabled: true,
|
||||
httpUser: httpUser || undefined,
|
||||
httpPass: httpPass || undefined
|
||||
authUsername: authUsername || undefined,
|
||||
authPassword: authPassword || undefined
|
||||
});
|
||||
addNotification({
|
||||
message: 'Webhook has been created',
|
||||
message: 'Webhook created. Secret ready to copy.',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.WebhookCreate, {
|
||||
events: events
|
||||
});
|
||||
goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/settings/webhooks/${webhook.$id}`
|
||||
);
|
||||
|
||||
createdWebhookId = webhook.$id;
|
||||
createdSecret = webhook.secret;
|
||||
|
||||
if (createdSecret) {
|
||||
showSecretModal = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await openWebhook();
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
@@ -68,7 +90,7 @@
|
||||
<Layout.Stack gap="xxl">
|
||||
<Step1 bind:name bind:url />
|
||||
<Step2 bind:events />
|
||||
<Step3 bind:httpUser bind:httpPass bind:security />
|
||||
<Step3 bind:authUsername bind:authPassword bind:tls />
|
||||
</Layout.Stack>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
@@ -76,3 +98,16 @@
|
||||
</svelte:fragment>
|
||||
</Wizard>
|
||||
</Form>
|
||||
|
||||
<Modal title="Webhook Created" bind:show={showSecretModal} onSubmit={openWebhook}>
|
||||
<Layout.Stack gap="l">
|
||||
<Typography.Text>
|
||||
This secret is only shown once after webhook creation or secret rotation. Copy it now.
|
||||
</Typography.Text>
|
||||
<Secret label="Secret" copyEvent="signature" bind:value={createdSecret} />
|
||||
</Layout.Stack>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button submit>Continue</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
+12
-8
@@ -3,9 +3,9 @@
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import { Alert, Fieldset, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let httpUser: string;
|
||||
export let httpPass: string;
|
||||
export let security: boolean;
|
||||
export let authUsername: string;
|
||||
export let authPassword: string;
|
||||
export let tls: boolean;
|
||||
</script>
|
||||
|
||||
<Fieldset legend="Security">
|
||||
@@ -13,10 +13,10 @@
|
||||
<Layout.Stack gap="s">
|
||||
<InputChoice
|
||||
type="switchbox"
|
||||
id="Security"
|
||||
id="tls"
|
||||
label="Certificate verification (SSL/TLS)"
|
||||
bind:value={security} />
|
||||
{#if !security}
|
||||
bind:value={tls} />
|
||||
{#if !tls}
|
||||
<Alert.Inline
|
||||
status="warning"
|
||||
title="Untrusted or self-signed certificates may not be secure.">
|
||||
@@ -34,13 +34,17 @@
|
||||
<Typography.Text
|
||||
>Use to secure your endpoint from untrusted sources.</Typography.Text>
|
||||
</div>
|
||||
<InputText label="User" id="user" placeholder="Enter username" bind:value={httpUser} />
|
||||
<InputText
|
||||
label="User"
|
||||
id="user"
|
||||
placeholder="Enter username"
|
||||
bind:value={authUsername} />
|
||||
<InputPassword
|
||||
label="Password"
|
||||
id="password"
|
||||
placeholder="Enter password"
|
||||
minlength={0}
|
||||
bind:value={httpPass} />
|
||||
bind:value={authPassword} />
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
|
||||
Reference in New Issue
Block a user