Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,12 @@ await sdk.forProject(region, projectId).tablesDB.listTables();

The databases feature unifies multiple database backends behind a polymorph API (`$database/(entity)/helpers/sdk.ts`):

| Type | Entity | Field | Record | Status |
| ------------- | ---------- | --------- | -------- | ------------------------ |
| `tablesdb` | table | column | row | Implemented |
| `documentsdb` | collection | attribute | document | Implemented |
| `vectorsdb` | -- | -- | -- | Not yet implemented |
| `dedicateddb` | table | column | row | Cross-repo (cloud/edge) |
| Type | Entity | Field | Record | Status |
| ------------- | ---------- | --------- | -------- | ----------------------- |
| `tablesdb` | table | column | row | Implemented |
| `documentsdb` | collection | attribute | document | Implemented |
| `vectorsdb` | -- | -- | -- | Not yet implemented |
| `dedicateddb` | table | column | row | Cross-repo (cloud/edge) |

- `useDatabaseSdk()` returns a unified interface regardless of backing type
- `useTerminology()` returns singular/plural names for the current database type
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
<!-- Loads AGENTS.md into AI assistant context -->

@AGENTS.md
48 changes: 47 additions & 1 deletion src/lib/helpers/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { get } from 'svelte/store';
import { sdk } from '$lib/stores/sdk';
import { projectRegion } from '$routes/(console)/project-[region]-[project]/store';
import type { Models } from '@appwrite.io/console';
import { error } from '@sveltejs/kit';

/**
* Returns the current project ID.
Expand Down Expand Up @@ -45,5 +46,50 @@ export function getProjectEndpoint(): string {
}

export function isProjectBlocked(project: Models.Project | null | undefined): boolean {
return project?.status !== 'paused' && !!project?.blocks?.length;
const hasGlobalProjectBlock = (project?.blocks ?? []).some((block) => {
const type = block.resourceType?.trim().toLowerCase();
const id = block.resourceId?.trim();

// Global project block:
// - legacy: both type and id empty
// - new: resourceType === 'projects' with no specific resourceId
const isLegacyGlobal = !type && !id;
const isProjectsGlobal = type === 'projects' && !id;

return isLegacyGlobal || isProjectsGlobal;
});

return project?.status !== 'paused' && hasGlobalProjectBlock;
}

export function isResourceBlocked(
project: Models.Project | null | undefined,
resourceType: string,
resourceId: string
): boolean {
const normalizedType = resourceType.trim().toLowerCase();
const normalizedId = resourceId.trim();

return (project?.blocks ?? []).some((block) => {
const type = block.resourceType?.trim().toLowerCase();
const id = block.resourceId?.trim();

return type === normalizedType && id === normalizedId;
});
Comment on lines +65 to +78
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 isResourceBlocked skips the paused-project guard

isProjectBlocked explicitly returns false for paused projects (project?.status !== 'paused') so that the "project paused" overlay is shown instead of the blocked-project overlay. isResourceBlocked has no equivalent guard, so a paused project that also carries a resource-level block will trigger a 403 error page instead of the paused overlay when a user navigates to that resource.

If paused projects should never surface resource-level 403s (and let the parent layout's paused UI take over), consider mirroring the status guard:

export function isResourceBlocked(
    project: Models.Project | null | undefined,
    resourceType: string,
    resourceId: string
): boolean {
    if (project?.status === 'paused') return false;

    const normalizedType = resourceType.trim().toLowerCase();
    const normalizedId = resourceId.trim();

    return (project?.blocks ?? []).some((block) => {
        const type = block.resourceType?.trim().toLowerCase();
        const id = block.resourceId?.trim();

        return type === normalizedType && id === normalizedId;
    });
}

}

export function guardResourceBlock(
project: Models.Project | null | undefined,
resourceType: string | string[],
resourceId: string
) {
const resourceTypes = Array.isArray(resourceType) ? resourceType : [resourceType];
const isBlocked = resourceTypes.some((type) => isResourceBlocked(project, type, resourceId));

if (isBlocked) {
error(403, {
type: 'general_resource_blocked',
message: 'This resource page cannot be accessed.'
});
}
}
Comment on lines +81 to 95
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded resource-type strings need to match backend values exactly

The resource type strings passed to guardResourceBlock across all layout files ('teams', 'users', 'databases', 'functions', 'buckets', 'files', 'messages', 'providers', 'topics', 'sites', 'tables', 'collections') are compared case-insensitively against block.resourceType. If the backend sends different casing or naming, the check silently passes and the block is never enforced.

Consider centralising these strings in an enum or const object alongside Dependencies in $lib/constants, e.g.:

export const BlockResourceType = {
    TEAMS: 'teams',
    USERS: 'users',
    DATABASES: 'databases',
    FUNCTIONS: 'functions',
    BUCKETS: 'buckets',
    FILES: 'files',
    MESSAGES: 'messages',
    PROVIDERS: 'providers',
    TOPICS: 'topics',
    SITES: 'sites',
    TABLES: 'tables',
    COLLECTIONS: 'collections',
} as const;

This makes them easy to audit against the API contract and prevents typos across the 11 call-sites.

Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import Header from './header.svelte';
import { sdk } from '$lib/stores/sdk';
import type { LayoutLoad } from './$types';
import { Dependencies } from '$lib/constants';
import { guardResourceBlock } from '$lib/helpers/project';

export const load: LayoutLoad = async ({ params, depends }) => {
export const load: LayoutLoad = async ({ params, depends, parent }) => {
depends(Dependencies.TEAM);
const { project } = await parent();
guardResourceBlock(project, 'teams', params.team);

return {
header: Header,
breadcrumbs: Breadcrumbs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import Breadcrumbs from './breadcrumbs.svelte';
import Header from './header.svelte';
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import { guardResourceBlock } from '$lib/helpers/project';

export const load: LayoutLoad = async ({ params, depends }) => {
export const load: LayoutLoad = async ({ params, depends, parent }) => {
depends(Dependencies.USER);
const { project } = await parent();
guardResourceBlock(project, 'users', params.user);

const [user, userFactors] = await Promise.all([
sdk.forProject(params.region, params.project).users.get({ userId: params.user }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import type { LayoutLoad } from './$types';
import Breadcrumbs from './breadcrumbs.svelte';
import Header from './header.svelte';
import SubNavigation from './subNavigation.svelte';
import { guardResourceBlock } from '$lib/helpers/project';

export const load: LayoutLoad = async ({ params, depends }) => {
export const load: LayoutLoad = async ({ params, depends, parent }) => {
depends(Dependencies.DATABASE);
const { project } = await parent();
guardResourceBlock(project, 'databases', params.database);

const database = await sdk.forProject(params.region, params.project).tablesDB.get({
databaseId: params.database
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import Header from './header.svelte';
import type { LayoutLoad } from './$types';
import { Dependencies } from '$lib/constants';
import { Breadcrumbs, useDatabaseSdk } from '$database/(entity)';
import { guardResourceBlock } from '$lib/helpers/project';

export const load: LayoutLoad = async ({ params, depends, parent }) => {
const { database } = await parent();
const { database, project } = await parent();
depends(Dependencies.TABLE);
guardResourceBlock(project, ['tables', 'collections'], params.table);

const databaseSdk = useDatabaseSdk(params.region, params.project, database.type);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import Breadcrumbs from './breadcrumbs.svelte';
import Header from './header.svelte';
import { Query } from '@appwrite.io/console';
import { RuleType } from '$lib/stores/sdk';
import { guardResourceBlock } from '$lib/helpers/project';

export const load: LayoutLoad = async ({ params, depends }) => {
export const load: LayoutLoad = async ({ params, depends, parent }) => {
depends(Dependencies.FUNCTION);
depends(Dependencies.DEPLOYMENTS);
const { project } = await parent();
guardResourceBlock(project, 'functions', params.function);

const func = await sdk
.forProject(params.region, params.project)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import Header from './header.svelte';
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import type { Models } from '@appwrite.io/console';
import { guardResourceBlock } from '$lib/helpers/project';

export const load: LayoutLoad = async ({ params, depends }) => {
export const load: LayoutLoad = async ({ params, depends, parent }) => {
depends(Dependencies.MESSAGING_MESSAGE);
const { project } = await parent();
guardResourceBlock(project, 'messages', params.message);

const message = await sdk
.forProject(params.region, params.project)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import Breadcrumbs from './breadcrumbs.svelte';
import Header from './header.svelte';
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import { guardResourceBlock } from '$lib/helpers/project';

export const load: LayoutLoad = async ({ params, depends }) => {
export const load: LayoutLoad = async ({ params, depends, parent }) => {
depends(Dependencies.MESSAGING_PROVIDER);
const { project } = await parent();
guardResourceBlock(project, 'providers', params.provider);

return {
header: Header,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import Breadcrumbs from './breadcrumbs.svelte';
import Header from './header.svelte';
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import { guardResourceBlock } from '$lib/helpers/project';

export const load: LayoutLoad = async ({ params, depends }) => {
export const load: LayoutLoad = async ({ params, depends, parent }) => {
depends(Dependencies.MESSAGING_TOPIC);
const { project } = await parent();
guardResourceBlock(project, 'topics', params.topic);

return {
header: Header,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import Breadcrumbs from './breadcrumbs.svelte';
import Header from './header.svelte';
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { guardResourceBlock } from '$lib/helpers/project';

export const load = async ({ depends, params }) => {
export const load = async ({ depends, params, parent }) => {
depends(Dependencies.SITE);
const { project } = await parent();
guardResourceBlock(project, 'sites', params.site);

const [site] = await Promise.all([
sdk.forProject(params.region, params.project).sites.get({ siteId: params.site })
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import Breadcrumbs from './breadcrumbs.svelte';
import Header from './header.svelte';
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import { guardResourceBlock } from '$lib/helpers/project';

export const load: LayoutLoad = async ({ params, depends }) => {
export const load: LayoutLoad = async ({ params, depends, parent }) => {
depends(Dependencies.BUCKET);
const { project } = await parent();
guardResourceBlock(project, 'buckets', params.bucket);

return {
header: Header,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import Header from './header.svelte';
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
import type { LayoutLoad } from './$types';
import { guardResourceBlock } from '$lib/helpers/project';

export const load: LayoutLoad = async ({ params, depends }) => {
export const load: LayoutLoad = async ({ params, depends, parent }) => {
depends(Dependencies.FILE);
depends(Dependencies.FILE_TOKENS);
const { project } = await parent();
guardResourceBlock(project, 'files', params.file);

return {
header: Header,
Expand Down
Loading