diff --git a/package-lock.json b/package-lock.json index 058515466..8154711ad 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 9ae2f78f9..d7c3234f5 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 3647c2ec8..245eec781 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 000000000..5caa6caf3 --- /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 9bfb631fb..363905ec0 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'; @@ -27,3 +28,4 @@ export { default as GridItem1 } from './gridItem1.svelte'; export { default as Steps } from './steps.svelte'; export { default as Step } from './step.svelte'; export { default as CustomId } from './customId.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 000000000..e44499b89 --- /dev/null +++ b/src/lib/components/secret.svelte @@ -0,0 +1,28 @@ + + +
    + {#if show} + {value} + {:else} + •••••••••••• + {/if} +
    + + + + +
    +
    diff --git a/src/lib/constants.ts b/src/lib/constants.ts index aeaa77c21..08fad97b4 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 7ed7d656f..b0b7b47b7 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 000000000..3869f498b --- /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 29247c421..000000000 --- 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 05f59eee2..000000000 --- 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 055b5349c..000000000 --- a/src/routes/console/project-[project]/keys/_create.svelte +++ /dev/null @@ -1,69 +0,0 @@ - - -
    - - Add API Key -

    - Select AllUnselect All -

    - - {#each scopes as scope} - - {/each} - - - - -
    -
    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 691763677..000000000 --- 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/+layout.svelte b/src/routes/console/project-[project]/overview/+layout.svelte index df20f7baf..2d9a9c83c 100644 --- a/src/routes/console/project-[project]/overview/+layout.svelte +++ b/src/routes/console/project-[project]/overview/+layout.svelte @@ -276,7 +276,7 @@
    -
    10
    +
    XX
    Realtime Connections
    diff --git a/src/routes/console/project-[project]/overview/keys/+page.svelte b/src/routes/console/project-[project]/overview/keys/+page.svelte index ebe4a4379..5f611048c 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 a36510ad3..e84332823 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,10 @@ import { afterNavigate } from '$app/navigation'; import { base } from '$app/paths'; import { page } from '$app/stores'; - import { CardGrid } from '$lib/components'; - import { scopes } from '$lib/constants'; - import { - Button, - Form, - FormList, - InputCheckbox, - 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'; import { Container } from '$lib/layout'; import { updateLayout } from '$lib/stores/layout'; @@ -19,38 +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] = 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) { - if ($key?.$id !== keyId) { - await key.load(projectId, keyId); - } + 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, @@ -117,12 +103,8 @@ async function updateScopes() { try { - await sdkForConsole.projects.updateKey( - $project.$id, - $key.$id, - $key.name, - scopes.filter((scope) => activeScopes[scope]) - ); + await sdkForConsole.projects.updateKey($project.$id, $key.$id, $key.name, scopes); + $key.scopes = scopes; addNotification({ type: 'success', message: 'API Key scopes has been updated' @@ -134,12 +116,6 @@ }); } } - - function unselectAll() { - for (const scope in activeScopes) { - activeScopes[scope] = false; - } - } @@ -148,13 +124,14 @@ {#if loaded} + {@const accessedAt = $key.accessedAt ? toLocaleDateTime($key.accessedAt) : 'never'}
    {$key.name}

    - Last accessed: {toLocaleDateTime($key.$updatedAt)}
    + Last accessed: {accessedAt}
    Scopes granted: {$key.scopes.length}

    @@ -164,18 +141,9 @@
    API Key Secret
    - + - - - -
    @@ -206,18 +174,16 @@ practice to allow only the permissions you need to meet your project goals.

    - - {#each scopes as scope} - - {/each} - + - +
    @@ -227,11 +193,7 @@

    Choose any name that will help you distinguish between API keys.

    - + @@ -250,7 +212,7 @@
    {$key.name}
    -

    Last accessed: {toLocaleDateTime($key.$updatedAt)}

    +

    Last accessed: {accessedAt}

    diff --git a/src/routes/console/project-[project]/overview/keys/create.svelte b/src/routes/console/project-[project]/overview/keys/create.svelte index 80765d20c..043cae7cb 100644 --- a/src/routes/console/project-[project]/overview/keys/create.svelte +++ b/src/routes/console/project-[project]/overview/keys/create.svelte @@ -1,32 +1,21 @@
    - + Create API Key - {#each scopes as scope} - - {/each} + diff --git a/src/routes/console/project-[project]/overview/keys/scopes.svelte b/src/routes/console/project-[project]/overview/keys/scopes.svelte new file mode 100644 index 000000000..d60f14730 --- /dev/null +++ b/src/routes/console/project-[project]/overview/keys/scopes.svelte @@ -0,0 +1,72 @@ + + +
    + + +
    + + {#each ['Authentication', 'Database', 'Functions', 'Storage', 'Other'] as category} + + {category} + + {#each allScopes.filter((s) => s.category === category) as scope} + + {scope.description} + + {/each} + + + {/each} + diff --git a/tests/unit/components/secret.test.ts b/tests/unit/components/secret.test.ts new file mode 100644 index 000000000..7225ede9a --- /dev/null +++ b/tests/unit/components/secret.test.ts @@ -0,0 +1,48 @@ +import '@testing-library/jest-dom'; +import { vi } from 'vitest'; +import { render, fireEvent } from '@testing-library/svelte'; +import { Secret } from '../../../src/lib/components'; + +const value = 'This is a secret'; + +test('shows Secret component', () => { + const { container } = render(Secret, { value }); + const secret = container.querySelector('span.text'); + const toggle = container.querySelector('[aria-label="show hidden text"]'); + const copy = container.querySelector('[aria-label="copy text"]'); + + expect(secret).toBeInTheDocument(); + expect(toggle).toBeInTheDocument(); + expect(copy).toBeInTheDocument(); +}); + +test('toggle secret', async () => { + const { container } = render(Secret, { value }); + const toggle = container.querySelector('[aria-label="show hidden text"]'); + + let secret = container.querySelector('span.text'); + expect(secret).not.toContainEqual(value); + await fireEvent.click(toggle); + + secret = container.querySelector('span.text'); + expect(secret.textContent).toEqual(value); + await fireEvent.click(toggle); + + secret = container.querySelector('span.text'); + expect(secret.textContent).not.toEqual(value); +}); + +test('copy to clipboard on click', async () => { + const { container } = render(Secret, { value }); + const copy = container.querySelector('[aria-label="copy text"]'); + + Object.assign(window.navigator, { + clipboard: { + writeText: vi.fn().mockImplementation(() => Promise.resolve()) + } + }); + + await fireEvent.click(copy); + + expect(window.navigator.clipboard.writeText).toHaveBeenCalledWith(value); +});