Account and org keys

This commit is contained in:
Matej Bačo
2025-12-22 18:03:23 +01:00
parent 87854c55de
commit 87a473ed06
21 changed files with 804 additions and 206 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@9b32107",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@b369c68",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc",
"@appwrite.io/pink-legacy": "^1.0.3",
+5 -5
View File
@@ -12,8 +12,8 @@ importers:
specifier: ^1.1.24
version: 1.1.24(svelte@5.25.3)(zod@3.24.3)
'@appwrite.io/console':
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/console@9b32107
version: https://pkg.vc/-/@appwrite/@appwrite.io/console@9b32107
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/console@b369c68
version: https://pkg.vc/-/@appwrite/@appwrite.io/console@b369c68
'@appwrite.io/pink-icons':
specifier: 0.25.0
version: 0.25.0
@@ -272,8 +272,8 @@ packages:
'@analytics/type-utils@0.6.2':
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@9b32107':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/console@9b32107}
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@b369c68':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/console@b369c68}
version: 1.10.0
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
@@ -3823,7 +3823,7 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@9b32107': {}
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@b369c68': {}
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)':
dependencies:
+208 -56
View File
@@ -161,309 +161,455 @@ export const scopes: {
description: string;
category: string;
icon: string;
type: 'api' | 'organization' | 'account';
}[] = [
// Project keys
{
scope: 'sessions.write',
description: "Access to create, update and delete your project's sessions",
category: 'Auth',
icon: 'user-group'
icon: 'user-group',
type: 'api'
},
{
scope: 'users.read',
description: "Access to read your project's users",
category: 'Auth',
icon: 'user-group'
icon: 'user-group',
type: 'api'
},
{
scope: 'users.write',
description: "Access to create, update, and delete your project's users",
category: 'Auth',
icon: 'user-group'
icon: 'user-group',
type: 'api'
},
{
scope: 'teams.read',
description: "Access to read your project's teams",
category: 'Auth',
icon: 'user-group'
icon: 'user-group',
type: 'api'
},
{
scope: 'teams.write',
description: "Access to create, update, and delete your project's teams",
category: 'Auth',
icon: 'user-group'
icon: 'user-group',
type: 'api'
},
{
scope: 'databases.read',
description: "Access to read your project's databases",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'databases.write',
description: "Access to create, update, and delete your project's databases",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'collections.read',
description: "Access to read your project's database collections",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'collections.write',
description: "Access to create, update, and delete your project's database collections",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'tables.read',
description: "Access to read your project's database tables",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'tables.write',
description: "Access to create, update, and delete your project's database tables",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'attributes.read',
description: "Access to read your project's database collection's attributes",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'attributes.write',
description:
"Access to create, update, and delete your project's database collection's attributes",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'columns.read',
description: "Access to read your project's database table's columns",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'columns.write',
description: "Access to create, update, and delete your project's database table's columns",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'indexes.read',
description: "Access to read your project's database table's indexes",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'indexes.write',
description: "Access to create, update, and delete your project's database table's indexes",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'documents.read',
description: "Access to read your project's database documents",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'documents.write',
description: "Access to create, update, and delete your project's database documents",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'rows.read',
description: "Access to read your project's database rows",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'rows.write',
description: "Access to create, update, and delete your project's database rows",
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'files.read',
description: "Access to read your project's storage files and preview images",
category: 'Storage',
icon: 'folder'
icon: 'folder',
type: 'api'
},
{
scope: 'files.write',
description: "Access to create, update, and delete your project's storage files",
category: 'Storage',
icon: 'folder'
icon: 'folder',
type: 'api'
},
{
scope: 'buckets.read',
description: "Access to read your project's storage buckets",
category: 'Storage',
icon: 'folder'
icon: 'folder',
type: 'api'
},
{
scope: 'buckets.write',
description: "Access to create, update, and delete your project's storage buckets",
category: 'Storage',
icon: 'folder'
icon: 'folder',
type: 'api'
},
{
scope: 'functions.read',
description: "Access to read your project's functions and code deployments",
category: 'Functions',
icon: 'lightning-bolt'
icon: 'lightning-bolt',
type: 'api'
},
{
scope: 'functions.write',
description:
"Access to create, update, and delete your project's functions and code deployments",
category: 'Functions',
icon: 'lightning-bolt'
icon: 'lightning-bolt',
type: 'api'
},
{
scope: 'execution.read',
description: "Access to read your project's execution logs",
category: 'Functions',
icon: 'lightning-bolt'
icon: 'lightning-bolt',
type: 'api'
},
{
scope: 'execution.write',
description: "Access to execute your project's functions",
category: 'Functions',
icon: 'lightning-bolt'
icon: 'lightning-bolt',
type: 'api'
},
{
scope: 'targets.read',
description: "Access to read your project's messaging targets",
category: 'Messaging',
icon: 'send'
icon: 'send',
type: 'api'
},
{
scope: 'targets.write',
description: "Access to create, update, and delete your project's messaging targets",
category: 'Messaging',
icon: 'send'
icon: 'send',
type: 'api'
},
{
scope: 'providers.read',
description: "Access to read your project's messaging providers",
category: 'Messaging',
icon: 'send'
icon: 'send',
type: 'api'
},
{
scope: 'providers.write',
description: "Access to create, update, and delete your project's messaging providers",
category: 'Messaging',
icon: 'send'
icon: 'send',
type: 'api'
},
{
scope: 'messages.read',
description: "Access to read your project's messages",
category: 'Messaging',
icon: 'send'
icon: 'send',
type: 'api'
},
{
scope: 'messages.write',
description: "Access to create, update, and delete your project's messages",
category: 'Messaging',
icon: 'send'
icon: 'send',
type: 'api'
},
{
scope: 'topics.read',
description: "Access to read your project's messaging topics",
category: 'Messaging',
icon: 'send'
icon: 'send',
type: 'api'
},
{
scope: 'topics.write',
description: "Access to create, update, and delete your project's messaging topics",
category: 'Messaging',
icon: 'send'
icon: 'send',
type: 'api'
},
{
scope: 'subscribers.read',
description: "Access to read your project's messaging topic subscribers",
category: 'Messaging',
icon: 'send'
icon: 'send',
type: 'api'
},
{
scope: 'subscribers.write',
description:
"Access to create, update, and delete your project's messaging topic subscribers",
category: 'Messaging',
icon: 'send'
icon: 'send',
type: 'api'
},
{
scope: 'locale.read',
description: "Access to access your project's Locale service",
category: 'Other',
icon: 'globe'
icon: 'globe',
type: 'api'
},
{
scope: 'avatars.read',
description: "Access to access your project's Avatars service",
category: 'Other',
icon: 'globe'
icon: 'globe',
type: 'api'
},
{
scope: 'health.read',
description: "Access to read your project's health status",
category: 'Other',
icon: 'globe'
icon: 'globe',
type: 'api'
},
{
scope: 'migrations.read',
description: "Access to read your project's migration status",
category: 'Other',
icon: 'globe'
icon: 'globe',
type: 'api'
},
{
scope: 'migrations.write',
description: 'Access to create migrations',
category: 'Other',
icon: 'globe'
icon: 'globe',
type: 'api'
},
{
scope: 'tokens.read',
description: "Access to read your project's file tokens",
category: 'Other',
icon: 'globe'
icon: 'globe',
type: 'api'
},
{
scope: 'tokens.write',
description: 'Access to create file tokens',
category: 'Other',
icon: 'globe'
icon: 'globe',
type: 'api'
},
{
scope: 'sites.read',
description: "Access to read your project's sites and deployments",
category: 'Sites',
icon: 'globe'
icon: 'globe',
type: 'api'
},
{
scope: 'sites.write',
description: "Access to create, update, and delete your project's sites and deployments",
category: 'Sites',
icon: 'globe'
icon: 'globe',
type: 'api'
},
{
scope: 'log.read',
description: "Access to read your sites's logs",
category: 'Sites',
icon: 'globe'
icon: 'globe',
type: 'api'
},
{
scope: 'log.write',
description: "Access to delete your site's logs",
category: 'Sites',
icon: 'globe'
icon: 'globe',
type: 'api'
},
// Organization keys
{
scope: 'platforms.read',
description: 'Access to read platforms of projects in organization.',
category: 'Projects',
icon: 'globe',
type: 'organization'
},
{
scope: 'platforms.write',
description: 'Access to create, update, and delete platforms of projects in organization.',
category: 'Projects',
icon: 'globe',
type: 'organization'
},
{
scope: 'keys.read',
description: 'Access to read API keys of projects in organization.',
category: 'Projects',
icon: 'globe',
type: 'organization'
},
{
scope: 'keys.write',
description: 'Access to create, update, and delete API keys of projects in organization.',
category: 'Projects',
icon: 'globe',
type: 'organization'
},
{
scope: 'devKeys.read',
description: 'Access to read dev keys of projects in organization.',
category: 'Projects',
icon: 'globe',
type: 'organization'
},
{
scope: 'devKeys.write',
description: 'Access to create, update, and delete dev keys of projects in organization.',
category: 'Projects',
icon: 'globe',
type: 'organization'
},
{
scope: 'webhooks.read',
description: 'Access to read webhooks of projects in organization.',
category: 'Projects',
icon: 'globe',
type: 'organization'
},
{
scope: 'webhooks.write',
description: 'Access to create, update, and delete webhooks of projects in organization.',
category: 'Projects',
icon: 'globe',
type: 'organization'
},
{
scope: 'projects.read',
description: 'Access to read projects in organization.',
category: 'Organization',
icon: 'globe',
type: 'organization'
},
{
scope: 'projects.write',
description: 'Access to create, update, and delete projects in organization.',
category: 'Organization',
icon: 'globe',
type: 'organization'
},
// Account keys
{
scope: 'account',
description: "Access to manage account, it's organizations, sessions, tokens, and billing.",
category: 'Other',
icon: 'globe',
type: 'account'
},
{
scope: 'teams.read',
description: 'Access to read organization detail.',
category: 'Organizations',
icon: 'globe',
type: 'account'
},
{
scope: 'teams.write',
description:
"Access to update organization detail, delete it, and manage it's memberships.",
category: 'Organizations',
icon: 'globe',
type: 'account'
}
];
@@ -472,37 +618,43 @@ export const cloudOnlyBackupScopes = [
scope: 'policies.read',
description: 'Access to read your database backup policies',
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'policies.write',
description: 'Access to create, update and delete your backup policies',
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'archives.read',
description: 'Access to read your database backup archives',
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'archives.write',
description: 'Access to create and delete your backup archives',
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'restorations.read',
description: 'Access to read your backup restorations',
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
},
{
scope: 'restorations.write',
description: 'Access to create backup restorations',
category: 'Database',
icon: 'database'
icon: 'database',
type: 'api'
}
];
+40 -36
View File
@@ -11,42 +11,46 @@
const path = `${base}/account`;
$: permanentTabs = [
{
href: path,
title: 'Overview',
event: 'overview'
},
{
href: `${path}/sessions`,
title: 'Sessions',
event: 'sessions'
},
{
href: `${path}/activity`,
title: 'Activity',
event: 'activity',
hasChildren: true
},
{
href: `${path}/organizations`,
title: 'Organizations',
event: 'organizations',
hasChildren: true
}
];
$: tabs = isCloud
? [
...permanentTabs,
{
href: `${path}/payments`,
title: 'Payments',
event: 'payments',
hasChildren: true
}
]
: permanentTabs;
const tabs = $derived(
[
{
href: path,
title: 'Overview',
event: 'overview'
},
{
href: `${path}/sessions`,
title: 'Sessions',
event: 'sessions'
},
{
href: `${path}/activity`,
title: 'Activity',
event: 'activity',
hasChildren: true
},
{
href: `${path}/organizations`,
title: 'Organizations',
event: 'organizations',
hasChildren: true
},
{
href: `${path}/payments`,
title: 'Payments',
event: 'payments',
hasChildren: true,
disabled: !isCloud
},
{
href: `${path}/integrations`,
title: 'Integrations',
event: 'integrations',
hasChildren: true,
disabled: !isCloud
}
].filter((tab) => !tab.disabled)
);
</script>
<Cover>
@@ -0,0 +1,11 @@
<script lang="ts">
import { Container } from '$lib/layout';
import type { PageData } from './$types';
import Keys from './keys.svelte';
export let data: PageData;
</script>
<Container>
<Keys keys={data.keys} />
</Container>
@@ -0,0 +1,15 @@
import type { PageLoad } from './$types';
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
export const load: PageLoad = async ({ depends }) => {
depends(Dependencies.ACCOUNT);
const keys = await sdk.forConsole.account.listKeys({
total: true
});
return {
keys
};
};
@@ -0,0 +1,5 @@
<script lang="ts">
import CreateKey from '../../../project-[region]-[project]/overview/(components)/create.svelte';
</script>
<CreateKey type="account" />
@@ -0,0 +1,20 @@
<script lang="ts">
import { CardGrid } from '$lib/components';
import Link from '$lib/elements/link.svelte';
import type { Models } from '@appwrite.io/console';
import Table from '../../project-[region]-[project]/overview/(components)/table.svelte';
export let keys: Models.KeyList;
</script>
<CardGrid>
<svelte:fragment slot="title">Account access tokens</svelte:fragment>
API keys to perform actions on your account using Platform API and SDK. <Link
href="https://appwrite.io/docs/platform"
external>
Learn more.
</Link>
<svelte:fragment slot="aside">
<Table keyType="account" showCreateButton={true} {keys} />
</svelte:fragment>
</CardGrid>
@@ -35,6 +35,15 @@
keys: ['g', 's'],
disabled: page.url.pathname.endsWith('/settings') || !$isOwner,
group: 'navigation'
},
{
label: 'Go to integrations',
callback: () => {
goto(`${base}/organization-${data.organization.$id}/integrations`);
},
keys: ['g', 'i'],
disabled: page.url.pathname.endsWith('/integrations') || !$isOwner,
group: 'navigation'
}
]);
</script>
@@ -84,6 +84,12 @@
title: 'Billing',
disabled: !(isCloud && $canSeeBilling)
},
{
href: `${path}/integrations`,
event: 'integrations',
title: 'Integrations',
disabled: !$isOwner || !isCloud
},
{
href: `${path}/settings`,
event: 'settings',
@@ -0,0 +1,15 @@
<script lang="ts">
import { Container } from '$lib/layout';
import type { PageData } from './$types';
import Apps from './apps.svelte';
import Installations from './installations.svelte';
import Keys from './keys.svelte';
export let data: PageData;
</script>
<Container>
<Keys keys={data.keys} />
<Installations />
<Apps />
</Container>
@@ -0,0 +1,16 @@
import type { PageLoad } from './$types';
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
export const load: PageLoad = async ({ depends, params }) => {
depends(Dependencies.ORGANIZATION);
const keys = await sdk.forConsole.organizations.listKeys({
organizationId: params.organization,
total: true
});
return {
keys
};
};
@@ -0,0 +1,23 @@
<script lang="ts">
import { Box, CardGrid } from '$lib/components';
import Link from '$lib/elements/link.svelte';
</script>
<CardGrid>
<svelte:fragment slot="title">Applications</svelte:fragment>
Create and manage apps to integrate your platform with Appwrite Cloud. <Link
href="https://appwrite.io/docs/apps"
external>
Learn more.
</Link>
<svelte:fragment slot="aside">
<Box>
<h6>
<b>Coming soon</b>
</h6>
<p class="text u-margin-block-start-8">
This feature is currently in testing phase and will be available for public soon.
</p>
</Box>
</svelte:fragment>
</CardGrid>
@@ -0,0 +1,5 @@
<script lang="ts">
import CreateKey from '../../../project-[region]-[project]/overview/(components)/create.svelte';
</script>
<CreateKey type="organization" />
@@ -0,0 +1,23 @@
<script lang="ts">
import { Box, CardGrid } from '$lib/components';
import Link from '$lib/elements/link.svelte';
</script>
<CardGrid>
<svelte:fragment slot="title">Installations</svelte:fragment>
Manage applications installed for your organizations, their access and permissions. <Link
href="https://appwrite.io/docs/apps"
external>
Learn more.
</Link>
<svelte:fragment slot="aside">
<Box>
<h6>
<b>Coming soon</b>
</h6>
<p class="text u-margin-block-start-8">
This feature is currently in testing phase and will be available for public soon.
</p>
</Box>
</svelte:fragment>
</CardGrid>
@@ -0,0 +1,20 @@
<script lang="ts">
import { CardGrid } from '$lib/components';
import Link from '$lib/elements/link.svelte';
import type { Models } from '@appwrite.io/console';
import Table from '../../project-[region]-[project]/overview/(components)/table.svelte';
export let keys: Models.KeyList;
</script>
<CardGrid>
<svelte:fragment slot="title">Organization keys</svelte:fragment>
API keys to perform actions on your organization using Platform API and SDK. <Link
href="https://appwrite.io/docs/platform"
external>
Learn more.
</Link>
<svelte:fragment slot="aside">
<Table keyType="organization" showCreateButton={true} {keys} />
</svelte:fragment>
</CardGrid>
@@ -16,6 +16,9 @@
import Scopes from '../api-keys/scopes.svelte';
import { page } from '$app/state';
import { copy } from '$lib/helpers/copy';
import type { Models } from '@appwrite.io/console';
export let type: 'api' | 'organization' | 'account' = 'api';
const projectId = page.params.project;
@@ -27,40 +30,95 @@
let name = '';
let expire: string | null = null;
async function createProjectKey(): Promise<Models.Key> {
const key = await sdk.forConsole.projects.createKey({
projectId,
name,
scopes,
expire: expire || undefined
});
if ($onboarding) {
await invalidate(Dependencies.PROJECT);
}
await goto(
`${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/${key.$id}`
);
return key;
}
async function createOrganizationKey(): Promise<Models.Key> {
const key = await sdk.forConsole.organizations.createKey({
organizationId: page.params.organization,
name,
scopes,
expire: expire || undefined
});
await invalidate(Dependencies.ORGANIZATION);
await goto(getResourcePath());
return key;
}
async function createAccountKey(): Promise<Models.Key> {
const key = await sdk.forConsole.account.createKey({
name,
scopes,
expire: expire || undefined
});
await invalidate(Dependencies.ACCOUNT);
await goto(getResourcePath());
return key;
}
async function create() {
try {
const { $id, secret } = await sdk.forConsole.projects.createKey({
projectId,
name,
scopes,
expire: expire || undefined
});
if ($onboarding) {
await invalidate(Dependencies.PROJECT);
let key: Models.Key;
switch (type) {
case 'api':
key = await createProjectKey();
break;
case 'organization':
key = await createOrganizationKey();
break;
case 'account':
key = await createAccountKey();
break;
}
trackEvent(Submit.KeyCreate);
await goto(
`${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/${$id}`
);
addNotification({
message: `API key has been created`,
type: 'success',
buttons: [
{
name: 'Copy API key',
method: async () => {
await copy(secret);
}
},
{
name: 'Copy endpoint',
method: async () => {
await copy(sdk.forConsole.client.config.endpoint);
}
const resource = getResource().charAt(0).toUpperCase() + getResource().slice(1);
const buttons = [
{
name: 'Copy ' + getResource(),
method: async () => {
await copy(key.secret);
}
]
}
];
if (showCopyEndpoint()) {
buttons.push({
name: 'Copy endpoint',
method: async () => {
await copy(sdk.forConsole.client.config.endpoint);
}
});
}
addNotification({
message: `${resource} has been created`,
type: 'success',
buttons
});
} catch (error) {
addNotification({
@@ -70,11 +128,44 @@
trackError(error, Submit.KeyCreate);
}
}
function showCopyEndpoint() {
switch (type) {
case 'api':
return true;
case 'organization':
return false;
case 'account':
return false;
}
}
function getResourcePath() {
switch (type) {
case 'api':
return `${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/`;
case 'organization':
return `${base}/organization-${page.params.organization}/integrations`;
case 'account':
return `${base}/account/integrations`;
}
}
function getResource() {
switch (type) {
case 'api':
return 'API key';
case 'organization':
return 'organization key';
case 'account':
return 'account token';
}
}
</script>
<Wizard
title="Create API key"
href={`${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/`}
title={'Create ' + getResource()}
href={getResourcePath()}
bind:showExitModal
column
columnSize="s"
@@ -97,10 +188,10 @@
<Fieldset legend="Scopes">
<Layout.Stack gap="xl">
<Typography.Text>
Choose which permission scopes to grant your application. It is best
practice to allow only the permissions you need to meet your project goals.
Choose which permission scopes to grant your {getResource()}. It is best
practice to allow only the permissions you need to meet your goals.
</Typography.Text>
<Scopes bind:scopes />
<Scopes bind:scopes {type} />
</Layout.Stack>
</Fieldset>
</Layout.Stack>
@@ -10,33 +10,109 @@
export let showDelete = false;
export let keyIds: string[] = [];
export let keyType: 'api' | 'dev' = 'api';
export let keyType: 'api' | 'dev' | 'organization' | 'account' = 'api';
let error: string;
const isApiKey = keyType === 'api';
const label = isApiKey ? 'API' : 'dev';
const projectId = page.params.project;
const organizationId = page.params.organization;
function getLabel(): string {
switch (keyType) {
case 'api':
return 'API';
case 'dev':
return 'dev';
case 'organization':
return 'organization';
case 'account':
return 'account';
}
}
function getSlug(): string {
switch (keyType) {
case 'api':
return 'api-keys';
case 'dev':
return 'dev-keys';
case 'organization':
return 'organization-keys';
case 'account':
return 'account-keys';
}
}
function getEvent() {
switch (keyType) {
case 'api':
return Submit.KeyDelete;
case 'dev':
return Submit.DevKeyDelete;
case 'organization':
return Submit.KeyDelete;
case 'account':
return Submit.KeyDelete;
}
}
function getDependency() {
switch (keyType) {
case 'api':
return Dependencies.KEYS;
case 'dev':
return Dependencies.DEV_KEYS;
case 'organization':
return Dependencies.ORGANIZATION;
case 'account':
return Dependencies.ACCOUNT;
}
}
function deleteKey(keyId: string) {
switch (keyType) {
case 'api':
return sdk.forConsole.projects.deleteKey({
projectId,
keyId
});
case 'dev':
return sdk.forConsole.projects.deleteDevKey({
projectId,
keyId
});
case 'organization':
return sdk.forConsole.organizations.deleteKey({
organizationId,
keyId
});
case 'account':
return sdk.forConsole.account.deleteKey({
keyId
});
}
}
async function postDeleteRedirect() {
switch (keyType) {
case 'api':
await goto(
`${base}/project-${page.params.region}-${page.params.project}/overview/${slug}`
);
break;
default:
break;
}
}
const label = getLabel();
const slug = getSlug();
const event = getEvent();
const dependency = getDependency();
async function handleDelete() {
const slug = isApiKey ? 'api-keys' : 'dev-keys';
const event = isApiKey ? Submit.KeyDelete : Submit.DevKeyDelete;
const dependency = isApiKey ? Dependencies.KEYS : Dependencies.DEV_KEYS;
try {
await Promise.all(
keyIds.map((key) =>
isApiKey
? sdk.forConsole.projects.deleteKey({
projectId,
keyId: key
})
: sdk.forConsole.projects.deleteDevKey({
projectId,
keyId: key
})
)
);
await Promise.all(keyIds.map((key) => deleteKey(key)));
await invalidate(dependency);
showDelete = false;
@@ -47,9 +123,8 @@
});
trackEvent(event);
await goto(
`${base}/project-${page.params.region}-${page.params.project}/overview/${slug}`
);
postDeleteRedirect();
} catch (e) {
error = e.message;
trackError(e, event);
@@ -3,31 +3,104 @@
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { Empty, MultiSelectionTable } from '$lib/components';
import { canWriteKeys } from '$lib/stores/roles';
import { canWriteKeys, canWriteTeams, canWriteProjects } from '$lib/stores/roles';
import type { Models } from '@appwrite.io/console';
import { diffDays } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { devKeyColumns, keyColumns, showDevKeysCreateModal } from '../store';
import { Badge, Layout, Table } from '@appwrite.io/pink-svelte';
import {
devKeyColumns,
keyColumns,
showDevKeysCreateModal,
organizationKeyColumns,
accountKeyColumns
} from '../store';
import { Badge, Icon, Layout, Table } from '@appwrite.io/pink-svelte';
import DeleteBatch from './deleteBatch.svelte';
import { capitalize } from '$lib/helpers/string';
import { getEffectiveScopes } from '../api-keys/scopes.svelte';
import type { Column } from '$lib/helpers/types';
import { Button } from '$lib/elements/forms';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
let {
keyType = 'api',
keys
keys,
showCreateButton = false
}: {
keyType?: 'api' | 'dev';
keyType?: 'api' | 'dev' | 'organization' | 'account';
keys: Models.KeyList | Models.DevKeyList;
showCreateButton?: boolean;
} = $props();
let selectedKeys = $state([]);
let showDeleteModal = $state(false);
const isApiKey = keyType === 'api';
const label = isApiKey ? 'API' : 'dev';
const slug = isApiKey ? 'api-keys' : 'dev-keys';
const columns = isApiKey ? $keyColumns : $devKeyColumns;
function canWrite() {
switch (keyType) {
case 'api':
return $canWriteKeys;
case 'dev':
return $canWriteProjects;
case 'organization':
return $canWriteTeams;
case 'account':
return true;
}
}
function getLabel(): string {
switch (keyType) {
case 'api':
return 'API key';
case 'dev':
return 'dev key';
case 'organization':
return 'organization key';
case 'account':
return 'account token';
}
}
function getSlug(): string {
switch (keyType) {
case 'api':
return 'api-keys';
case 'dev':
return 'dev-keys';
case 'organization':
return 'organization-keys';
case 'account':
return 'account-keys';
}
}
function getColumns(): Column[] {
switch (keyType) {
case 'api':
return $keyColumns;
case 'dev':
return $devKeyColumns;
case 'organization':
return $organizationKeyColumns;
case 'account':
return $accountKeyColumns;
}
}
function getExpiredColor(): 'error' | 'warning' {
switch (keyType) {
case 'api':
case 'organization':
case 'account':
return 'error';
case 'dev':
return 'warning';
}
}
const label = getLabel();
const slug = getSlug();
const columns = getColumns();
function getApiKeyScopeCount(key: Models.Key | Models.DevKey) {
const apiKey = key as Models.Key;
@@ -43,20 +116,64 @@
return {
message: isExpired ? 'Expired' : isExpiring ? 'Expires soon' : null,
status: isExpired ? (isApiKey ? 'error' : 'warning') : isExpiring ? 'warning' : null
status: isExpired ? getExpiredColor() : isExpiring ? 'warning' : null
};
}
function getKeys(): Models.Key[] | Models.DevKey[] {
if (isApiKey) return keys['keys'] as Models.Key[];
else return keys['devKeys'] as Models.DevKey[];
switch (keyType) {
case 'api':
case 'organization':
case 'account':
return keys['keys'] as Models.Key[];
case 'dev':
return keys['devKeys'] as Models.DevKey[];
}
}
function getDescription(): string {
if (isApiKey)
return 'Use API keys to authenticate your app’s requests in production, granting secure access to live data and services.';
else
return 'Dev keys allow bypassing rate limits and CORS errors in your development environment.';
switch (keyType) {
case 'api':
return 'Use API keys to authenticate your app’s requests in production, granting secure access to live data and services.';
case 'organization':
return "Use organization keys to manage projects in your organization and organization's settings.";
case 'account':
return 'Use account tokens to manage your organizations and your account settings.';
case 'dev':
return 'Dev keys allow bypassing rate limits and CORS errors in your development environment.';
}
}
function hasScopes(): boolean {
switch (keyType) {
case 'api':
case 'organization':
case 'account':
return true;
case 'dev':
return false;
}
}
async function onKeyCreate() {
switch (keyType) {
case 'api':
await goto(
`${base}/project-${page.params.region}-${page.params.project}/overview/${slug}/create`
);
break;
case 'organization':
await goto(
`${base}/organization-${page.params.organization}/integrations/create-key`
);
break;
case 'account':
await goto(`${base}/account/integrations/create-token`);
break;
case 'dev':
$showDevKeysCreateModal = true;
break;
}
}
</script>
@@ -64,9 +181,9 @@
<MultiSelectionTable
{columns}
confirmDeletion={false}
allowSelection={$canWriteKeys}
allowSelection={canWrite()}
showSuccessNotification={false}
resource={`${capitalize(label)} key`}
resource={capitalize(label)}
onDelete={(selectedRows) => {
showDeleteModal = true;
selectedKeys = selectedRows;
@@ -108,7 +225,7 @@
{/if}
</Layout.Stack>
</Table.Cell>
{#if isApiKey}
{#if hasScopes()}
<Table.Cell {root}>
{getApiKeyScopeCount(key)} Scopes
</Table.Cell>
@@ -120,19 +237,20 @@
{:else}
<Empty
single
allowCreate={$canWriteKeys}
allowCreate={canWrite()}
href="https://appwrite.io/docs/advanced/platform/{slug}"
target="{label} key"
target={label}
description={getDescription()}
on:click={async () => {
if (isApiKey) {
await goto(
`${base}/project-${page.params.region}-${page.params.project}/overview/${slug}/create`
);
} else {
$showDevKeysCreateModal = true;
}
}} />
on:click={onKeyCreate} />
{/if}
<DeleteBatch {keyType} bind:keyIds={selectedKeys} bind:showDelete={showDeleteModal} />
{#if showCreateButton && keys.total}
<div>
<Button compact on:click={onKeyCreate}>
<Icon icon={IconPlus} slot="start" size="s" />
Create {getLabel()}
</Button>
</div>
{/if}
@@ -34,6 +34,7 @@
import { Accordion, Divider, Layout, Selector } from '@appwrite.io/pink-svelte';
export let scopes: string[];
export let type: 'api' | 'organization' | 'account' = 'api';
const baseFilteredScopes = allScopes.filter((scope) => {
const val = scope.scope;
@@ -43,16 +44,19 @@
return !legacyPrefixes.some((prefix) => val.startsWith(prefix));
});
console.log(baseFilteredScopes);
// insert cloud-only scopes right after databases.write
const databasesWriteIndex = baseFilteredScopes.findIndex((s) => s.scope === 'databases.write');
const filteredScopes =
const filteredScopes = (
isCloud && databasesWriteIndex !== -1
? [
...baseFilteredScopes.slice(0, databasesWriteIndex + 1),
...cloudOnlyBackupScopes,
...baseFilteredScopes.slice(databasesWriteIndex + 1)
]
: baseFilteredScopes;
: baseFilteredScopes
).filter((scope) => scope.type === type);
// include all scopes
const scopeCatalog = new Set([
@@ -60,25 +64,7 @@
...(isCloud ? cloudOnlyBackupScopes.map((s) => s.scope) : [])
]);
enum Category {
Auth = 'Auth',
Database = 'Database',
Functions = 'Functions',
Messaging = 'Messaging',
Sites = 'Sites',
Storage = 'Storage',
Other = 'Other'
}
const categories = [
Category.Auth,
Category.Database,
Category.Functions,
Category.Storage,
Category.Messaging,
Category.Sites,
Category.Other
];
const categories = Array.from(new Set(filteredScopes.map((scope) => scope.category)));
let mounted = false;
@@ -150,7 +136,7 @@
return 'indeterminate';
}
function onCategoryChange(event: CustomEvent<boolean | 'indeterminate'>, category: Category) {
function onCategoryChange(event: CustomEvent<boolean | 'indeterminate'>, category: string) {
if (event.detail === 'indeterminate') return;
filteredScopes.forEach((s) => {
if (s.category === category) {
@@ -37,3 +37,7 @@ export const keyColumns = readable<Column[]>([
...get(devKeyColumns),
{ id: 'scopes', title: 'Scopes', type: 'string', width: { min: 120 } }
]);
export const organizationKeyColumns = readable<Column[]>([...get(keyColumns)]);
export const accountKeyColumns = readable<Column[]>([...get(keyColumns)]);