From 87a473ed06aa71a4a1e7d82fdb012b5670ee0bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 22 Dec 2025 18:03:23 +0100 Subject: [PATCH] Account and org keys --- package.json | 2 +- pnpm-lock.yaml | 10 +- src/lib/constants.ts | 264 ++++++++++++++---- src/routes/(console)/account/header.svelte | 76 ++--- .../account/integrations/+page.svelte | 11 + .../(console)/account/integrations/+page.ts | 15 + .../integrations/create-token/+page.svelte | 5 + .../account/integrations/keys.svelte | 20 ++ .../+layout.svelte | 9 + .../organization-[organization]/header.svelte | 6 + .../integrations/+page.svelte | 15 + .../integrations/+page.ts | 16 ++ .../integrations/apps.svelte | 23 ++ .../integrations/create-key/+page.svelte | 5 + .../integrations/installations.svelte | 23 ++ .../integrations/keys.svelte | 20 ++ .../overview/(components)/create.svelte | 155 +++++++--- .../overview/(components)/deleteBatch.svelte | 121 ++++++-- .../overview/(components)/table.svelte | 178 ++++++++++-- .../overview/api-keys/scopes.svelte | 30 +- .../overview/store.ts | 4 + 21 files changed, 803 insertions(+), 205 deletions(-) create mode 100644 src/routes/(console)/account/integrations/+page.svelte create mode 100644 src/routes/(console)/account/integrations/+page.ts create mode 100644 src/routes/(console)/account/integrations/create-token/+page.svelte create mode 100644 src/routes/(console)/account/integrations/keys.svelte create mode 100644 src/routes/(console)/organization-[organization]/integrations/+page.svelte create mode 100644 src/routes/(console)/organization-[organization]/integrations/+page.ts create mode 100644 src/routes/(console)/organization-[organization]/integrations/apps.svelte create mode 100644 src/routes/(console)/organization-[organization]/integrations/create-key/+page.svelte create mode 100644 src/routes/(console)/organization-[organization]/integrations/installations.svelte create mode 100644 src/routes/(console)/organization-[organization]/integrations/keys.svelte diff --git a/package.json b/package.json index 15cb88fa7f..1753cf4d40 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7492f49f0..7e353d9e79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f6a61e0d19..b9082de2e1 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -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' } ]; diff --git a/src/routes/(console)/account/header.svelte b/src/routes/(console)/account/header.svelte index 507dbfd912..3f92b2213e 100644 --- a/src/routes/(console)/account/header.svelte +++ b/src/routes/(console)/account/header.svelte @@ -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) + ); diff --git a/src/routes/(console)/account/integrations/+page.svelte b/src/routes/(console)/account/integrations/+page.svelte new file mode 100644 index 0000000000..26046337cc --- /dev/null +++ b/src/routes/(console)/account/integrations/+page.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/src/routes/(console)/account/integrations/+page.ts b/src/routes/(console)/account/integrations/+page.ts new file mode 100644 index 0000000000..f156a81042 --- /dev/null +++ b/src/routes/(console)/account/integrations/+page.ts @@ -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 + }; +}; diff --git a/src/routes/(console)/account/integrations/create-token/+page.svelte b/src/routes/(console)/account/integrations/create-token/+page.svelte new file mode 100644 index 0000000000..060fe15efc --- /dev/null +++ b/src/routes/(console)/account/integrations/create-token/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/(console)/account/integrations/keys.svelte b/src/routes/(console)/account/integrations/keys.svelte new file mode 100644 index 0000000000..30dc1e080e --- /dev/null +++ b/src/routes/(console)/account/integrations/keys.svelte @@ -0,0 +1,20 @@ + + + + Account access tokens + API keys to perform actions on your account using Platform API and SDK. + Learn more. + + + + + diff --git a/src/routes/(console)/organization-[organization]/+layout.svelte b/src/routes/(console)/organization-[organization]/+layout.svelte index f101710f56..2f774ef293 100644 --- a/src/routes/(console)/organization-[organization]/+layout.svelte +++ b/src/routes/(console)/organization-[organization]/+layout.svelte @@ -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' } ]); diff --git a/src/routes/(console)/organization-[organization]/header.svelte b/src/routes/(console)/organization-[organization]/header.svelte index 191131ae49..469577c126 100644 --- a/src/routes/(console)/organization-[organization]/header.svelte +++ b/src/routes/(console)/organization-[organization]/header.svelte @@ -84,6 +84,12 @@ title: 'Billing', disabled: !(isCloud && $canSeeBilling) }, + { + href: `${path}/integrations`, + event: 'integrations', + title: 'Integrations', + disabled: !$isOwner || !isCloud + }, { href: `${path}/settings`, event: 'settings', diff --git a/src/routes/(console)/organization-[organization]/integrations/+page.svelte b/src/routes/(console)/organization-[organization]/integrations/+page.svelte new file mode 100644 index 0000000000..55d9df460d --- /dev/null +++ b/src/routes/(console)/organization-[organization]/integrations/+page.svelte @@ -0,0 +1,15 @@ + + + + + + + diff --git a/src/routes/(console)/organization-[organization]/integrations/+page.ts b/src/routes/(console)/organization-[organization]/integrations/+page.ts new file mode 100644 index 0000000000..9d2a0b1abc --- /dev/null +++ b/src/routes/(console)/organization-[organization]/integrations/+page.ts @@ -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 + }; +}; diff --git a/src/routes/(console)/organization-[organization]/integrations/apps.svelte b/src/routes/(console)/organization-[organization]/integrations/apps.svelte new file mode 100644 index 0000000000..8bd11461b6 --- /dev/null +++ b/src/routes/(console)/organization-[organization]/integrations/apps.svelte @@ -0,0 +1,23 @@ + + + + Applications + Create and manage apps to integrate your platform with Appwrite Cloud. + Learn more. + + + +
+ Coming soon +
+

+ This feature is currently in testing phase and will be available for public soon. +

+
+
+
diff --git a/src/routes/(console)/organization-[organization]/integrations/create-key/+page.svelte b/src/routes/(console)/organization-[organization]/integrations/create-key/+page.svelte new file mode 100644 index 0000000000..d2e7c201f9 --- /dev/null +++ b/src/routes/(console)/organization-[organization]/integrations/create-key/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/(console)/organization-[organization]/integrations/installations.svelte b/src/routes/(console)/organization-[organization]/integrations/installations.svelte new file mode 100644 index 0000000000..765e9ebd7c --- /dev/null +++ b/src/routes/(console)/organization-[organization]/integrations/installations.svelte @@ -0,0 +1,23 @@ + + + + Installations + Manage applications installed for your organizations, their access and permissions. + Learn more. + + + +
+ Coming soon +
+

+ This feature is currently in testing phase and will be available for public soon. +

+
+
+
diff --git a/src/routes/(console)/organization-[organization]/integrations/keys.svelte b/src/routes/(console)/organization-[organization]/integrations/keys.svelte new file mode 100644 index 0000000000..2b0a611569 --- /dev/null +++ b/src/routes/(console)/organization-[organization]/integrations/keys.svelte @@ -0,0 +1,20 @@ + + + + Organization keys + API keys to perform actions on your organization using Platform API and SDK. + Learn more. + + +
+ + diff --git a/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte b/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte index 4c9bb81508..3095ebe715 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/create.svelte @@ -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 { + 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 { + 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 { + 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}` - ); + + 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: `API key has been created`, + message: `${resource} 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); - } - } - ] + 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'; + } + } - 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. - + diff --git a/src/routes/(console)/project-[region]-[project]/overview/(components)/deleteBatch.svelte b/src/routes/(console)/project-[region]-[project]/overview/(components)/deleteBatch.svelte index 3a742d94cc..e639f60340 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/(components)/deleteBatch.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/deleteBatch.svelte @@ -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; - 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; + 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() { 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); diff --git a/src/routes/(console)/project-[region]-[project]/overview/(components)/table.svelte b/src/routes/(console)/project-[region]-[project]/overview/(components)/table.svelte index 9cfb56a832..2142efb4e0 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/(components)/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/(components)/table.svelte @@ -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; + } } @@ -64,9 +181,9 @@ { showDeleteModal = true; selectedKeys = selectedRows; @@ -108,7 +225,7 @@ {/if} - {#if isApiKey} + {#if hasScopes()} {getApiKeyScopeCount(key)} Scopes @@ -120,19 +237,20 @@ {:else} { - if (isApiKey) { - await goto( - `${base}/project-${page.params.region}-${page.params.project}/overview/${slug}/create` - ); - } else { - $showDevKeysCreateModal = true; - } - }} /> + on:click={onKeyCreate} /> {/if} + +{#if showCreateButton && keys.total} +
+ +
+{/if} diff --git a/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte index 48485cc319..dbfbf08e08 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/api-keys/scopes.svelte @@ -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, category: Category) { + function onCategoryChange(event: CustomEvent, category: string) { if (event.detail === 'indeterminate') return; filteredScopes.forEach((s) => { if (s.category === category) { diff --git a/src/routes/(console)/project-[region]-[project]/overview/store.ts b/src/routes/(console)/project-[region]-[project]/overview/store.ts index 2e3171c0f5..dc9f8c9fd9 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/store.ts +++ b/src/routes/(console)/project-[region]-[project]/overview/store.ts @@ -37,3 +37,7 @@ export const keyColumns = readable([ ...get(devKeyColumns), { id: 'scopes', title: 'Scopes', type: 'string', width: { min: 120 } } ]); + +export const organizationKeyColumns = readable([...get(keyColumns)]); + +export const accountKeyColumns = readable([...get(keyColumns)]);