From e7420969c23b7c0a4147f0f81735b735e484cc2c Mon Sep 17 00:00:00 2001
From: Torsten Dittmann
Date: Wed, 5 Oct 2022 15:03:05 +0200
Subject: [PATCH 1/6] feat: api keys detail
---
package-lock.json | 28 +--
package.json | 4 +-
src/lib/components/collapsible.svelte | 20 +-
src/lib/components/collapsibleItem.svelte | 14 ++
src/lib/components/index.ts | 1 +
src/lib/constants.ts | 206 +++++++++++-------
src/lib/elements/forms/index.ts | 1 +
src/lib/elements/forms/inputDateTime.svelte | 58 +++++
.../project-[project]/keys/+layout.svelte | 22 --
.../project-[project]/keys/+page.svelte | 62 ------
.../project-[project]/keys/_create.svelte | 69 ------
.../keys/key/[key]/+page.svelte | 35 ---
.../keys/[key]/+page@project-[project].svelte | 68 ++++--
13 files changed, 267 insertions(+), 321 deletions(-)
create mode 100644 src/lib/components/collapsibleItem.svelte
create mode 100644 src/lib/elements/forms/inputDateTime.svelte
delete mode 100644 src/routes/console/project-[project]/keys/+layout.svelte
delete mode 100644 src/routes/console/project-[project]/keys/+page.svelte
delete mode 100644 src/routes/console/project-[project]/keys/_create.svelte
delete mode 100644 src/routes/console/project-[project]/keys/key/[key]/+page.svelte
diff --git a/package-lock.json b/package-lock.json
index 0585154663..8154711ade 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,8 +9,8 @@
"version": "0.0.1",
"dependencies": {
"@aw-labs/appwrite-console": "^6.0.0",
- "@aw-labs/icons": "0.0.0-57",
- "@aw-labs/ui": "0.0.0-57",
+ "@aw-labs/icons": "0.0.0-58",
+ "@aw-labs/ui": "0.0.0-58",
"echarts": "^5.4.0",
"tippy.js": "^6.3.7",
"web-vitals": "^2.1.4"
@@ -77,14 +77,14 @@
}
},
"node_modules/@aw-labs/icons": {
- "version": "0.0.0-57",
- "resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-57.tgz",
- "integrity": "sha512-HmXSTSP3GEBi5awFk0APTTNdM10DDIBCTnxi3BJGlsXo+SXnpVikoMk6RZltNLH7MRfPQNx+VfJ2bOC+hlW8ZQ=="
+ "version": "0.0.0-58",
+ "resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-58.tgz",
+ "integrity": "sha512-xUD5DQcYVNiKhDSpxMO24G/4l7txgAdwochK01tJA6wXhuPAcp5Yjuofjm2bGkMzCuze4Vs+SVvhsBxzVXRPrA=="
},
"node_modules/@aw-labs/ui": {
- "version": "0.0.0-57",
- "resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-57.tgz",
- "integrity": "sha512-a/nKqu9nHysTF7bIOLo+ZN1zKpCHfv+jHGixU0SGXchTNAmj2OXClZNk3XfMmwnPD6CiF4cG6YRdbmpx+TTXgA==",
+ "version": "0.0.0-58",
+ "resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-58.tgz",
+ "integrity": "sha512-W3bTvAPX4ig5qLBvGn/VpNaDk62RqO4oDa0poube2WuElkVvaGwFAR0wCwmisdi2NeXyGJqt20g3Y/Mz4K2yMA==",
"dependencies": {
"@aw-labs/icons": "*"
}
@@ -8134,14 +8134,14 @@
}
},
"@aw-labs/icons": {
- "version": "0.0.0-57",
- "resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-57.tgz",
- "integrity": "sha512-HmXSTSP3GEBi5awFk0APTTNdM10DDIBCTnxi3BJGlsXo+SXnpVikoMk6RZltNLH7MRfPQNx+VfJ2bOC+hlW8ZQ=="
+ "version": "0.0.0-58",
+ "resolved": "https://registry.npmjs.org/@aw-labs/icons/-/icons-0.0.0-58.tgz",
+ "integrity": "sha512-xUD5DQcYVNiKhDSpxMO24G/4l7txgAdwochK01tJA6wXhuPAcp5Yjuofjm2bGkMzCuze4Vs+SVvhsBxzVXRPrA=="
},
"@aw-labs/ui": {
- "version": "0.0.0-57",
- "resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-57.tgz",
- "integrity": "sha512-a/nKqu9nHysTF7bIOLo+ZN1zKpCHfv+jHGixU0SGXchTNAmj2OXClZNk3XfMmwnPD6CiF4cG6YRdbmpx+TTXgA==",
+ "version": "0.0.0-58",
+ "resolved": "https://registry.npmjs.org/@aw-labs/ui/-/ui-0.0.0-58.tgz",
+ "integrity": "sha512-W3bTvAPX4ig5qLBvGn/VpNaDk62RqO4oDa0poube2WuElkVvaGwFAR0wCwmisdi2NeXyGJqt20g3Y/Mz4K2yMA==",
"requires": {
"@aw-labs/icons": "*"
}
diff --git a/package.json b/package.json
index 9ae2f78f9f..d7c3234f5f 100644
--- a/package.json
+++ b/package.json
@@ -19,8 +19,8 @@
},
"dependencies": {
"@aw-labs/appwrite-console": "^6.0.0",
- "@aw-labs/icons": "0.0.0-57",
- "@aw-labs/ui": "0.0.0-57",
+ "@aw-labs/icons": "0.0.0-58",
+ "@aw-labs/ui": "0.0.0-58",
"echarts": "^5.4.0",
"tippy.js": "^6.3.7",
"web-vitals": "^2.1.4"
diff --git a/src/lib/components/collapsible.svelte b/src/lib/components/collapsible.svelte
index 3647c2ec8b..245eec7810 100644
--- a/src/lib/components/collapsible.svelte
+++ b/src/lib/components/collapsible.svelte
@@ -1,21 +1,3 @@
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/lib/components/collapsibleItem.svelte b/src/lib/components/collapsibleItem.svelte
new file mode 100644
index 0000000000..5caa6caf34
--- /dev/null
+++ b/src/lib/components/collapsibleItem.svelte
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts
index b6f4ffb8bb..bd3ebd58eb 100644
--- a/src/lib/components/index.ts
+++ b/src/lib/components/index.ts
@@ -16,6 +16,7 @@ export { default as DropList } from './dropList.svelte';
export { default as DropListItem } from './dropListItem.svelte';
export { default as DropListLink } from './dropListLink.svelte';
export { default as Collapsible } from './collapsible.svelte';
+export { default as CollapsibleItem } from './collapsibleItem.svelte';
export { default as DropTabs } from './dropTabs.svelte';
export { default as DropTabsItem } from './dropTabsItem.svelte';
export { default as Avatar } from './avatar.svelte';
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index aeaa77c210..08fad97b46 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -1,80 +1,130 @@
export const scopes = [
- 'users.read',
- 'users.write',
- 'teams.read',
- 'teams.write',
- 'collections.read',
- 'collections.write',
- 'attributes.read',
- 'attributes.write',
- 'indexes.read',
- 'indexes.write',
- 'documents.read',
- 'documents.write',
- 'files.read',
- 'files.write',
- 'buckets.read',
- 'buckets.write',
- 'functions.read',
- 'functions.write',
- 'execution.read',
- 'execution.write',
- 'locale.read',
- 'avatars.read',
- 'health.read'
-];
-
-export const events = [
- 'account.create',
- 'account.update.email',
- 'account.update.name',
- 'account.update.password',
- 'users.update.email',
- 'users.update.name',
- 'users.update.password',
- 'account.update.prefs',
- 'account.recovery.create',
- 'account.recovery.update',
- 'account.verification.create',
- 'account.verification.update',
- 'account.delete',
- 'account.sessions.create',
- 'account.sessions.delete',
- 'account.sessions.update',
- 'database.collections.create',
- 'database.collections.update',
- 'database.collections.delete',
- 'database.attributes.create',
- 'database.attributes.delete',
- 'database.indexes.create',
- 'database.indexes.delete',
- 'database.documents.create',
- 'database.documents.update',
- 'database.documents.delete',
- 'functions.create',
- 'functions.update',
- 'functions.delete',
- 'functions.deployments.create',
- 'functions.deployments.update',
- 'functions.deployments.delete',
- 'functions.executions.create',
- 'functions.executions.update',
- 'storage.files.create',
- 'storage.files.update',
- 'storage.files.delete',
- 'storage.buckets.create',
- 'storage.buckets.update',
- 'storage.buckets.delete',
- 'users.create',
- 'users.update.prefs',
- 'users.update.status',
- 'users.delete',
- 'users.sessions.delete',
- 'teams.create',
- 'teams.update',
- 'teams.delete',
- 'teams.memberships.create',
- 'teams.memberships.update',
- 'teams.memberships.update.status',
- 'teams.memberships.delete'
+ {
+ scope: 'users.read',
+ description: "Access to read your project's users",
+ category: 'Authentication'
+ },
+ {
+ scope: 'users.write',
+ description: "Access to create, update, and delete your project's users",
+ category: 'Authentication'
+ },
+ {
+ scope: 'teams.read',
+ description: "Access to read your project's teams",
+ category: 'Authentication'
+ },
+ {
+ scope: 'teams.write',
+ description: "Access to create, update, and delete your project's teams",
+ category: 'Authentication'
+ },
+ {
+ scope: 'databases.read',
+ description: "Access to read your project's databases",
+ category: 'Database'
+ },
+ {
+ scope: 'databases.write',
+ description: "Access to create, update, and delete your project's databases",
+ category: 'Database'
+ },
+ {
+ scope: 'collections.read',
+ description: "Access to read your project's database collections",
+ category: 'Database'
+ },
+ {
+ scope: 'collections.write',
+ description: "Access to create, update, and delete your project's database collections",
+ category: 'Database'
+ },
+ {
+ scope: 'attributes.read',
+ description: "Access to read your project's database collection's attributes",
+ category: 'Database'
+ },
+ {
+ scope: 'attributes.write',
+ description:
+ "Access to create, update, and delete your project's database collection's attributes",
+ category: 'Database'
+ },
+ {
+ scope: 'indexes.read',
+ description: "Access to read your project's database collection's indexes",
+ category: 'Database'
+ },
+ {
+ scope: 'indexes.write',
+ description:
+ "Access to create, update, and delete your project's database collection's indexes",
+ category: 'Database'
+ },
+ {
+ scope: 'documents.read',
+ description: "Access to read your project's database documents",
+ category: 'Database'
+ },
+ {
+ scope: 'documents.write',
+ description: "Access to create, update, and delete your project's database documents",
+ category: 'Database'
+ },
+ {
+ scope: 'files.read',
+ description: "Access to read your project's storage files and preview images",
+ category: 'Storage'
+ },
+ {
+ scope: 'files.write',
+ description: "Access to create, update, and delete your project's storage files",
+ category: 'Storage'
+ },
+ {
+ scope: 'buckets.read',
+ description: "Access to read your project's storage buckets",
+ category: 'Storage'
+ },
+ {
+ scope: 'buckets.write',
+ description: "Access to create, update, and delete your project's storage buckets",
+ category: 'Storage'
+ },
+ {
+ scope: 'functions.read',
+ description: "Access to read your project's functions and code deployments",
+ category: 'Functions'
+ },
+ {
+ scope: 'functions.write',
+ description:
+ "Access to create, update, and delete your project's functions and code deployments",
+ category: 'Functions'
+ },
+ {
+ scope: 'execution.read',
+ description: "Access to read your project's execution logs",
+ category: 'Functions'
+ },
+ {
+ scope: 'execution.write',
+ description: "Access to execute your project's functions",
+ category: 'Functions'
+ },
+ {
+ scope: 'locale.read',
+ description: "Access to access your project's Locale service",
+ category: 'Other'
+ },
+ {
+ scope: 'avatars.read',
+ description: "Access to access your project's Avatars service",
+ category: 'Other'
+ },
+ {
+ scope: 'health.read',
+ description: "Access to read your project's health status",
+ category: 'Other'
+ }
];
diff --git a/src/lib/elements/forms/index.ts b/src/lib/elements/forms/index.ts
index 7ed7d656fe..b0b7b47b7b 100644
--- a/src/lib/elements/forms/index.ts
+++ b/src/lib/elements/forms/index.ts
@@ -11,6 +11,7 @@ export { default as InputSwitch } from './inputSwitch.svelte';
export { default as InputTags } from './inputTags.svelte';
export { default as InputFile } from './inputFile.svelte';
export { default as InputCustomId } from './inputCustomId.svelte';
+export { default as InputDateTime } from './inputDateTime.svelte';
export { default as InputSearch } from './inputSearch.svelte';
export { default as InputRadio } from './inputRadio.svelte';
export { default as InputSelect } from './inputSelect.svelte';
diff --git a/src/lib/elements/forms/inputDateTime.svelte b/src/lib/elements/forms/inputDateTime.svelte
new file mode 100644
index 0000000000..3869f498bf
--- /dev/null
+++ b/src/lib/elements/forms/inputDateTime.svelte
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+ {#if error}
+ {error}
+ {/if}
+
diff --git a/src/routes/console/project-[project]/keys/+layout.svelte b/src/routes/console/project-[project]/keys/+layout.svelte
deleted file mode 100644
index 29247c421c..0000000000
--- a/src/routes/console/project-[project]/keys/+layout.svelte
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
diff --git a/src/routes/console/project-[project]/keys/+page.svelte b/src/routes/console/project-[project]/keys/+page.svelte
deleted file mode 100644
index 05f59eee2d..0000000000
--- a/src/routes/console/project-[project]/keys/+page.svelte
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
- Appwrite - API Keys
-
-
- {#if $project}
- {#if $project.keys}
-
-
- Name
- Scopes
-
-
- {#each $project.keys as key}
-
-
- {key.name}
-
- {key.scopes.length}
-
- {/each}
-
-
- {:else}
-
-
-
No API Keys Found
-
- You haven't created any API keys for your project yet.
-
-
-
- {/if}
-
- {/if}
-
-
diff --git a/src/routes/console/project-[project]/keys/_create.svelte b/src/routes/console/project-[project]/keys/_create.svelte
deleted file mode 100644
index 055b5349c6..0000000000
--- a/src/routes/console/project-[project]/keys/_create.svelte
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
diff --git a/src/routes/console/project-[project]/keys/key/[key]/+page.svelte b/src/routes/console/project-[project]/keys/key/[key]/+page.svelte
deleted file mode 100644
index 691763677d..0000000000
--- a/src/routes/console/project-[project]/keys/key/[key]/+page.svelte
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
- {#await request}
- loading
- {:then response}
- {response.name}
-
- {/await}
-
-
diff --git a/src/routes/console/project-[project]/overview/keys/[key]/+page@project-[project].svelte b/src/routes/console/project-[project]/overview/keys/[key]/+page@project-[project].svelte
index a36510ad32..a168fd26fe 100644
--- a/src/routes/console/project-[project]/overview/keys/[key]/+page@project-[project].svelte
+++ b/src/routes/console/project-[project]/overview/keys/[key]/+page@project-[project].svelte
@@ -2,16 +2,18 @@
import { afterNavigate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
- import { CardGrid } from '$lib/components';
+ import { CardGrid, Collapsible, CollapsibleItem } from '$lib/components';
import { scopes } from '$lib/constants';
import {
Button,
Form,
FormList,
- InputCheckbox,
+ InputChoice,
InputPassword,
InputText
} from '$lib/elements/forms';
+ import InputDateTime from '$lib/elements/forms/inputDateTime.svelte';
+ import { difference } from '$lib/helpers/array';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container } from '$lib/layout';
import { updateLayout } from '$lib/stores/layout';
@@ -25,7 +27,7 @@
const projectId = $page.params.project;
const keyId = $page.params.key;
const activeScopes = scopes.reduce((prev, next) => {
- prev[next] = false;
+ prev[next.scope] = false;
return prev;
}, {});
@@ -40,8 +42,9 @@
afterNavigate(handle);
async function handle(event = null) {
+ const promise = key.load(projectId, keyId);
if ($key?.$id !== keyId) {
- await key.load(projectId, keyId);
+ await promise;
}
name ??= $key.name;
@@ -121,8 +124,9 @@
$project.$id,
$key.$id,
$key.name,
- scopes.filter((scope) => activeScopes[scope])
+ selectedScoped
);
+ $key.scopes = selectedScoped;
addNotification({
type: 'success',
message: 'API Key scopes has been updated'
@@ -135,11 +139,21 @@
}
}
+ function selectAll() {
+ for (const scope in activeScopes) {
+ activeScopes[scope] = true;
+ }
+ }
+
function unselectAll() {
for (const scope in activeScopes) {
activeScopes[scope] = false;
}
}
+
+ $: selectedScoped = scopes
+ .filter((scope) => activeScopes[scope.scope])
+ .map(({ scope }) => scope);
@@ -154,7 +168,7 @@
- Last accessed: {toLocaleDateTime($key.$updatedAt)}
+ Last accessed: {toLocaleDateTime($key.accessedAt)}
Scopes granted: {$key.scopes.length}
@@ -206,18 +220,36 @@
practice to allow only the permissions you need to meet your project goals.
-
- {#each scopes as scope}
-
+
+
+
+
+
+ {#each ['Authentication', 'Database', 'Functions', 'Storage', 'Other'] as category}
+
+ {category}
+
+ {#each scopes.filter((s) => s.category === category) as scope}
+
+ {scope.description}
+
+ {/each}
+
+
{/each}
-
+
-
+
@@ -227,11 +259,7 @@
Choose any name that will help you distinguish between API keys.
-
+
@@ -250,7 +278,7 @@
{$key.name}
-
Last accessed: {toLocaleDateTime($key.$updatedAt)}
+
Last accessed: {toLocaleDateTime($key.accessedAt)}
From f802afb98e0d87800de6ebd3ffdf055e17776ee6 Mon Sep 17 00:00:00 2001
From: Torsten Dittmann
Date: Wed, 5 Oct 2022 17:40:55 +0200
Subject: [PATCH 2/6] fix: re-usable scopes and secret
---
src/lib/components/index.ts | 1 +
src/lib/components/secret.svelte | 28 ++++++
.../overview/keys/+page.svelte | 2 +-
.../keys/[key]/+page@project-[project].svelte | 96 +++----------------
.../overview/keys/create.svelte | 27 ++----
.../overview/keys/scopes.svelte | 72 ++++++++++++++
6 files changed, 124 insertions(+), 102 deletions(-)
create mode 100644 src/lib/components/secret.svelte
create mode 100644 src/routes/console/project-[project]/overview/keys/scopes.svelte
diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts
index bd3ebd58eb..96e13637bd 100644
--- a/src/lib/components/index.ts
+++ b/src/lib/components/index.ts
@@ -27,3 +27,4 @@ export { default as Search } from './search.svelte';
export { default as GridItem1 } from './gridItem1.svelte';
export { default as Steps } from './steps.svelte';
export { default as Step } from './step.svelte';
+export { default as Secret } from './secret.svelte';
diff --git a/src/lib/components/secret.svelte b/src/lib/components/secret.svelte
new file mode 100644
index 0000000000..458005a93e
--- /dev/null
+++ b/src/lib/components/secret.svelte
@@ -0,0 +1,28 @@
+
+
+
+ {#if show}
+
{value}
+ {:else}
+
••••••
+ {/if}
+
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/overview/keys/+page.svelte b/src/routes/console/project-[project]/overview/keys/+page.svelte
index ebe4a4379b..5f611048c5 100644
--- a/src/routes/console/project-[project]/overview/keys/+page.svelte
+++ b/src/routes/console/project-[project]/overview/keys/+page.svelte
@@ -39,7 +39,7 @@
{key.name}
- {toLocaleDateTime(key.$createdAt)}
+ {key.accessedAt ? toLocaleDateTime(key.accessedAt) : 'never'}
{toLocaleDateTime(key.$updatedAt)}
diff --git a/src/routes/console/project-[project]/overview/keys/[key]/+page@project-[project].svelte b/src/routes/console/project-[project]/overview/keys/[key]/+page@project-[project].svelte
index a168fd26fe..e843328238 100644
--- a/src/routes/console/project-[project]/overview/keys/[key]/+page@project-[project].svelte
+++ b/src/routes/console/project-[project]/overview/keys/[key]/+page@project-[project].svelte
@@ -2,16 +2,8 @@
import { afterNavigate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
- import { CardGrid, Collapsible, CollapsibleItem } from '$lib/components';
- import { scopes } from '$lib/constants';
- import {
- Button,
- Form,
- FormList,
- InputChoice,
- InputPassword,
- InputText
- } from '$lib/elements/forms';
+ import { CardGrid, Secret } from '$lib/components';
+ import { Button, Form, FormList, InputText } from '$lib/elements/forms';
import InputDateTime from '$lib/elements/forms/inputDateTime.svelte';
import { difference } from '$lib/helpers/array';
import { toLocaleDateTime } from '$lib/helpers/date';
@@ -21,39 +13,30 @@
import { sdkForConsole } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { project } from '../../../store';
+ import Scopes from '../scopes.svelte';
import Delete from './delete.svelte';
import { key } from './store';
const projectId = $page.params.project;
const keyId = $page.params.key;
- const activeScopes = scopes.reduce((prev, next) => {
- prev[next.scope] = false;
-
- return prev;
- }, {});
let loaded = false;
let showDelete = false;
let name: string = null;
let secret: string = null;
let expire: string = null;
+ let scopes: string[] = null;
onMount(handle);
afterNavigate(handle);
async function handle(event = null) {
- const promise = key.load(projectId, keyId);
- if ($key?.$id !== keyId) {
- await promise;
- }
+ await key.load(projectId, keyId);
name ??= $key.name;
secret ??= $key.secret;
expire ??= $key.expire;
- unselectAll();
- $key.scopes.forEach((scope) => {
- activeScopes[scope] = true;
- });
+ scopes ??= $key.scopes;
updateLayout({
navigate: event,
@@ -120,13 +103,8 @@
async function updateScopes() {
try {
- await sdkForConsole.projects.updateKey(
- $project.$id,
- $key.$id,
- $key.name,
- selectedScoped
- );
- $key.scopes = selectedScoped;
+ await sdkForConsole.projects.updateKey($project.$id, $key.$id, $key.name, scopes);
+ $key.scopes = scopes;
addNotification({
type: 'success',
message: 'API Key scopes has been updated'
@@ -138,22 +116,6 @@
});
}
}
-
- function selectAll() {
- for (const scope in activeScopes) {
- activeScopes[scope] = true;
- }
- }
-
- function unselectAll() {
- for (const scope in activeScopes) {
- activeScopes[scope] = false;
- }
- }
-
- $: selectedScoped = scopes
- .filter((scope) => activeScopes[scope.scope])
- .map(({ scope }) => scope);
@@ -162,13 +124,14 @@
{#if loaded}
+ {@const accessedAt = $key.accessedAt ? toLocaleDateTime($key.accessedAt) : 'never'}
{$key.name}
- Last accessed: {toLocaleDateTime($key.accessedAt)}
+ Last accessed: {accessedAt}
Scopes granted: {$key.scopes.length}
@@ -178,18 +141,9 @@
API Key Secret
-
+
-
-
-
-