Merge pull request #2946 from appwrite/codex/render-deprecated-api-key-scopes

[codex] Render deprecated API key scopes
This commit is contained in:
Chirag Aggarwal
2026-03-30 09:28:52 +05:30
committed by GitHub
4 changed files with 46 additions and 84 deletions
+18 -9
View File
@@ -155,12 +155,15 @@ export const defaultRoles: string[] = ['owner'];
// these are kept for backwards compatibility with keys and events that already exists.
// for the new ones, we use the new terminology.
export const scopes: {
export type ScopeDefinition = {
scope: string;
description: string;
category: string;
icon: string;
}[] = [
deprecated?: boolean;
};
export const scopes: ScopeDefinition[] = [
{
scope: 'sessions.write',
description: "Access to create, update and delete your project's sessions",
@@ -207,13 +210,15 @@ export const scopes: {
scope: 'collections.read',
description: "Access to read your project's database collections",
category: 'Database',
icon: 'database'
icon: 'database',
deprecated: true
},
{
scope: 'collections.write',
description: "Access to create, update, and delete your project's database collections",
category: 'Database',
icon: 'database'
icon: 'database',
deprecated: true
},
{
scope: 'tables.read',
@@ -231,14 +236,16 @@ export const scopes: {
scope: 'attributes.read',
description: "Access to read your project's database collection's attributes",
category: 'Database',
icon: 'database'
icon: 'database',
deprecated: true
},
{
scope: 'attributes.write',
description:
"Access to create, update, and delete your project's database collection's attributes",
category: 'Database',
icon: 'database'
icon: 'database',
deprecated: true
},
{
scope: 'columns.read',
@@ -268,13 +275,15 @@ export const scopes: {
scope: 'documents.read',
description: "Access to read your project's database documents",
category: 'Database',
icon: 'database'
icon: 'database',
deprecated: true
},
{
scope: 'documents.write',
description: "Access to create, update, and delete your project's database documents",
category: 'Database',
icon: 'database'
icon: 'database',
deprecated: true
},
{
scope: 'rows.read',
@@ -466,7 +475,7 @@ export const scopes: {
}
];
export const cloudOnlyBackupScopes = [
export const cloudOnlyBackupScopes: ScopeDefinition[] = [
{
scope: 'policies.read',
description: 'Access to read your database backup policies',
@@ -16,7 +16,6 @@
import { symmetricDifference } from '$lib/helpers/array';
import Scopes from '../api-keys/scopes.svelte';
import { InteractiveText, Layout, Typography } from '@appwrite.io/pink-svelte';
import { getEffectiveScopes } from '../api-keys/scopes.svelte';
export let key: Models.DevKey | Models.Key;
export let keyType: 'api' | 'dev' = 'api';
@@ -163,8 +162,6 @@
{#if isApiKey}
<Form onSubmit={updateScopes}>
{@const apiKey = asApiKey(key)}
{@const apiKeyCorrectScopes = getEffectiveScopes(apiKey.scopes)}
{@const currentEffective = scopes ? getEffectiveScopes(scopes) : null}
<CardGrid>
<svelte:fragment slot="title">Scopes</svelte:fragment>
You can choose which permission scope to grant your application. It is a best practice
@@ -178,8 +175,7 @@
<svelte:fragment slot="actions">
<Button
submit
disabled={scopes &&
!symmetricDifference(currentEffective, apiKeyCorrectScopes).length}
disabled={scopes && !symmetricDifference(scopes, apiKey.scopes).length}
>Update</Button>
</svelte:fragment>
</CardGrid>
@@ -11,7 +11,6 @@
import { Badge, 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';
let {
keyType = 'api',
@@ -31,7 +30,7 @@
function getApiKeyScopeCount(key: Models.Key | Models.DevKey) {
const apiKey = key as Models.Key;
return getEffectiveScopes(apiKey.scopes).length;
return apiKey.scopes.length;
}
function getExpiryDetails(key: Models.Key | Models.DevKey): {
@@ -31,29 +31,21 @@
import { Button } from '$lib/elements/forms';
import { symmetricDifference } from '$lib/helpers/array';
import { scopes as allScopes, cloudOnlyBackupScopes } from '$lib/constants';
import { Accordion, Divider, Layout, Selector } from '@appwrite.io/pink-svelte';
import { Accordion, Badge, Divider, Layout, Selector } from '@appwrite.io/pink-svelte';
import type { Scopes } from '@appwrite.io/console';
export let scopes: Scopes[];
const baseFilteredScopes = allScopes.filter((scope) => {
const val = scope.scope;
if (!val) return false;
const legacyPrefixes = ['collections.', 'attributes.', 'documents.'];
return !legacyPrefixes.some((prefix) => val.startsWith(prefix));
});
// insert cloud-only scopes right after databases.write
const databasesWriteIndex = baseFilteredScopes.findIndex((s) => s.scope === 'databases.write');
const databasesWriteIndex = allScopes.findIndex((s) => s.scope === 'databases.write');
const filteredScopes =
isCloud && databasesWriteIndex !== -1
? [
...baseFilteredScopes.slice(0, databasesWriteIndex + 1),
...allScopes.slice(0, databasesWriteIndex + 1),
...cloudOnlyBackupScopes,
...baseFilteredScopes.slice(databasesWriteIndex + 1)
...allScopes.slice(databasesWriteIndex + 1)
]
: baseFilteredScopes;
: allScopes;
// include all scopes
const scopeCatalog = new Set([
@@ -90,9 +82,8 @@
onMount(() => {
scopes.forEach((scope) => {
const newerScope = toNewerScope(scope);
if (newerScope in activeScopes) {
activeScopes[newerScope] = true;
if (scope in activeScopes) {
activeScopes[scope] = true;
}
});
@@ -111,36 +102,11 @@
}
}
function toNewerScope(scope: string): string {
for (const pair of compatPairs) {
if (scope.startsWith(pair.legacy)) {
return scope.replace(pair.legacy, pair.newer);
}
}
return scope;
}
function getAllScopeVariants(scope: string): string[] {
const variants = new Set([scope]);
for (const pair of compatPairs) {
if (scope.startsWith(pair.newer)) {
variants.add(scope.replace(pair.newer, pair.legacy));
} else if (scope.startsWith(pair.legacy)) {
variants.add(scope.replace(pair.legacy, pair.newer));
}
}
return Array.from(variants);
}
function categoryState(category: string, s: string[]): boolean | 'indeterminate' {
const scopesByCategory = filteredScopes.filter((n) => n.category === category);
const activeInCategory = scopesByCategory.filter((scopeItem) => {
const newerScope = scopeItem.scope;
return s.some((scope) => toNewerScope(scope) === newerScope);
});
const activeInCategory = scopesByCategory.filter((scopeItem) =>
s.includes(scopeItem.scope as Scopes)
);
if (activeInCategory.length === 0) {
return false;
@@ -154,27 +120,16 @@
function onCategoryChange(event: CustomEvent<boolean | 'indeterminate'>, category: Category) {
if (event.detail === 'indeterminate') return;
filteredScopes.forEach((s) => {
if (s.category === category) {
if (s.category === category && !s.deprecated) {
activeScopes[s.scope] = event.detail;
}
});
}
function generateSyncedScopes(activeScopesObj: Record<string, boolean>): Scopes[] {
const result = new Set<string>();
Object.entries(activeScopesObj).forEach(([scope, isActive]) => {
if (isActive) {
const variants = getAllScopeVariants(scope);
variants.forEach((variant) => {
if (scopeCatalog.has(variant)) {
result.add(variant);
}
});
}
});
return Array.from(result) as Scopes[];
return Object.entries(activeScopesObj)
.filter(([scope, isActive]) => isActive && scopeCatalog.has(scope))
.map(([scope]) => scope as Scopes);
}
$: {
@@ -203,9 +158,7 @@
{@const checked = categoryState(category, scopes)}
{@const isLastItem = index === categories.length - 1}
{@const scopesLength = filteredScopes.filter(
(n) =>
n.category === category &&
scopes.some((scope) => toNewerScope(scope) === n.scope)
(n) => n.category === category && scopes.includes(n.scope as Scopes)
).length}
<Accordion
selectable
@@ -216,12 +169,17 @@
on:change={(event) => onCategoryChange(event, category)}>
<Layout.Stack>
{#each filteredScopes.filter((s) => s.category === category) as scope}
<Selector.Checkbox
size="s"
id={scope.scope}
label={scope.scope}
description={scope.description}
bind:checked={activeScopes[scope.scope]} />
<Layout.Stack direction="row" alignItems="center" gap="s">
<Selector.Checkbox
size="s"
id={scope.scope}
label={`${scope.scope}${scope.deprecated ? ' (Deprecated)' : ''}`}
description={scope.description}
bind:checked={activeScopes[scope.scope]} />
{#if scope.deprecated}
<Badge size="xs" variant="secondary" content="Deprecated" />
{/if}
</Layout.Stack>
{/each}
</Layout.Stack>
</Accordion>