diff --git a/src/routes/(console)/project-[region]-[project]/+layout.ts b/src/routes/(console)/project-[region]-[project]/+layout.ts index 1bec6520ec..8da91fdd80 100644 --- a/src/routes/(console)/project-[region]-[project]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/+layout.ts @@ -106,7 +106,7 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { // Track console access for cloud projects (fire-and-forget, backend has 6-day cooldown). // Skip if paused — user must explicitly resume via the paused project modal. - if (isCloud && browser && project.status !== 'paused') { + if (isCloud && browser && project.status !== 'paused' && scopes.includes('projects.write')) { generateFingerprintToken() .then((fingerprint) => { sdk.forConsole.client.headers['X-Appwrite-Console-Fingerprint'] = fingerprint; diff --git a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte index 7e63a523be..3961307590 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte @@ -18,14 +18,12 @@ import UpdateVariables from '../updateVariables.svelte'; import { page } from '$app/state'; import UpdateLabels from './updateLabels.svelte'; + import type { PageData } from './$types'; + import { Alert } from '@appwrite.io/pink-svelte'; - export let data; - - let teamId: string = null; + let { data }: { data: PageData } = $props(); onMount(() => { - teamId ??= $project.teamId; - const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); @@ -87,25 +85,31 @@ {#if $project} - {#if $canWriteProjects} - - - - - - - - + {#if !$canWriteProjects} + + You can open this settings area, but editing project-level settings requires the + projects.write scope. + {/if} + + + + + + + + + {/if} diff --git a/src/routes/(console)/project-[region]-[project]/settings/+page.ts b/src/routes/(console)/project-[region]-[project]/settings/+page.ts index 321f06adaa..9c2b7955fb 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/settings/+page.ts @@ -3,14 +3,20 @@ import { sdk } from '$lib/stores/sdk'; import { Query } from '@appwrite.io/console'; import type { PageLoad } from './$types'; +function isReadonlySettingsPermissionError(error: unknown): boolean { + const code = (error as { code?: number } | null)?.code; + return code === 401 || code === 403; +} + export const load: PageLoad = async ({ depends, url, params }) => { depends(Dependencies.PROJECT_VARIABLES); depends(Dependencies.PROJECT_INSTALLATIONS); + const limit = PAGE_LIMIT; const offset = Number(url.searchParams.get('offset') ?? 0); const variablesOffset = Number(url.searchParams.get('variablesOffset') ?? 0); const projectSdk = sdk.forProject(params.region, params.project); - const [variables, installations] = await Promise.all([ + const [variablesResult, installationsResult] = await Promise.allSettled([ projectSdk.projectApi.listVariables({ queries: [Query.limit(limit), Query.offset(variablesOffset)] }), @@ -19,6 +25,38 @@ export const load: PageLoad = async ({ depends, url, params }) => { }) ]); + const variables = + variablesResult.status === 'fulfilled' + ? variablesResult.value + : (() => { + // Read-only users can be blocked from write-adjacent settings APIs. + // Only silence those permission errors so genuine load failures still surface. + if (!isReadonlySettingsPermissionError(variablesResult.reason)) { + throw variablesResult.reason; + } + + return { + total: 0, + variables: [] + }; + })(); + + const installations = + installationsResult.status === 'fulfilled' + ? installationsResult.value + : (() => { + // Read-only users can be blocked from write-adjacent settings APIs. + // Only silence those permission errors so genuine load failures still surface. + if (!isReadonlySettingsPermissionError(installationsResult.reason)) { + throw installationsResult.reason; + } + + return { + total: 0, + installations: [] + }; + })(); + return { limit, offset, diff --git a/src/routes/(console)/project-[region]-[project]/settings/changeOrganization.svelte b/src/routes/(console)/project-[region]-[project]/settings/changeOrganization.svelte index eb3fb4a205..5c4fc8a36d 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/changeOrganization.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/changeOrganization.svelte @@ -4,6 +4,7 @@ import { organizationList } from '$lib/stores/organization'; import { project } from '../store'; import TransferProjectModal from './transferProjectModal.svelte'; + import { canWriteProjects } from '$lib/stores/roles'; let teamId: string; let showTransfer = false; @@ -18,6 +19,7 @@ id="organization" placeholder="Select destination" label="Move to" + disabled={!$canWriteProjects} bind:value={teamId} options={$organizationList.teams .filter((team) => team.$id !== $project.teamId) @@ -30,7 +32,7 @@ diff --git a/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte b/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte index 0af70f0083..a39f59bf68 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/deleteProject.svelte @@ -11,6 +11,7 @@ import { project, projectRegion } from '../store'; import { organization } from '$lib/stores/organization'; import { Dependencies } from '$lib/constants'; + import { canWriteProjects } from '$lib/stores/roles'; let error: string; let showDelete = false; let name: string = null; @@ -59,7 +60,9 @@ - + diff --git a/src/routes/(console)/project-[region]-[project]/settings/updateInstallations.svelte b/src/routes/(console)/project-[region]-[project]/settings/updateInstallations.svelte index 5d61cf6a46..ca13336779 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/updateInstallations.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/updateInstallations.svelte @@ -30,6 +30,7 @@ import type { ComponentType } from 'svelte'; import { Link } from '$lib/elements'; import { regionalConsoleVariables, mcpTools } from '../store'; + import { canWriteProjects } from '$lib/stores/roles'; export let total: number; export let limit: number; @@ -87,6 +88,7 @@
{ trackEvent(Click.SettingsInstallProviderClick); @@ -131,6 +133,7 @@ type="button" class="button is-text is-only-icon" aria-label="more options" + disabled={!$canWriteProjects} on:click={toggle}> @@ -186,7 +189,11 @@ title="No installation was added to the project yet" description="Add an installation to connect repositories"> - + Connect to GitHub diff --git a/src/routes/(console)/project-[region]-[project]/settings/updateLabels.svelte b/src/routes/(console)/project-[region]-[project]/settings/updateLabels.svelte index cd12d50fd3..ef87a01f63 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/updateLabels.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/updateLabels.svelte @@ -11,6 +11,7 @@ import { project } from '../store'; import { Tag, Input, Layout, Icon } from '@appwrite.io/pink-svelte'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; + import { canWriteProjects } from '$lib/stores/roles'; const alphaNumericRegExp = /^[a-zA-Z0-9]+$/; let suggestedLabels = ['live', 'stage', 'internal']; @@ -73,6 +74,7 @@ id="project-labels" label="Labels" placeholder="Select or type project labels" + disabled={!$canWriteProjects} bind:tags={labels} /> {/key} @@ -81,6 +83,8 @@ size="s" selected={labels.includes(suggestedLabel)} on:click={() => { + if (!$canWriteProjects) return; + if (!labels.includes(suggestedLabel)) { labels = [...labels, suggestedLabel]; } else { @@ -98,7 +102,7 @@ - + diff --git a/src/routes/(console)/project-[region]-[project]/settings/updateName.svelte b/src/routes/(console)/project-[region]-[project]/settings/updateName.svelte index 7b9091cce7..4db3c2cf3b 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/updateName.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/updateName.svelte @@ -58,22 +58,21 @@ -{#if $canWriteProjects} -
- - Name - - - + + + Name + + + - - - - - -{/if} + + + +
+ diff --git a/src/routes/(console)/project-[region]-[project]/settings/updateProtocols.svelte b/src/routes/(console)/project-[region]-[project]/settings/updateProtocols.svelte index f1098d22a6..e285400159 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/updateProtocols.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/updateProtocols.svelte @@ -13,6 +13,7 @@ import { SvelteSet } from 'svelte/reactivity'; import { ProtocolId } from '@appwrite.io/console'; import { get } from 'svelte/store'; + import { canWriteProjects } from '$lib/stores/roles'; let isUpdatingAllProtocols = $state(false); let showUpdateProtocolDialog = $state(false); @@ -139,20 +140,26 @@ + disabled={!$canWriteProjects || shouldDisableEnableAllButton}> + Enable all + + disabled={!$canWriteProjects || shouldDisableDisableAllButton}> + Disable all +
@@ -167,7 +174,8 @@ description={protocolDescriptions[protocol.method]} bind:value={protocol.value} on:change={() => protocolUpdate(protocol)} - disabled={apiProtocolUpdates.has(protocol.method)} /> + disabled={!$canWriteProjects || + apiProtocolUpdates.has(protocol.method)} /> {#if apiProtocolUpdates.has(protocol.method)} @@ -196,7 +204,7 @@ + disabled={!$canWriteProjects || shouldDisableEnableAllButton} + >Enable all + disabled={!$canWriteProjects || shouldDisableDisableAllButton}> + Disable all + @@ -163,7 +169,8 @@ label={service.label} bind:value={service.value} on:change={() => serviceUpdate(service)} - disabled={apiServiceUpdates.has(service.method)} /> + disabled={!$canWriteProjects || + apiServiceUpdates.has(service.method)} /> {#if apiServiceUpdates.has(service.method)} @@ -188,7 +195,7 @@