diff --git a/bun.lock b/bun.lock index 59441522cf..35dd0aa42f 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@9786d91", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@5c7f8e5", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", @@ -125,7 +125,7 @@ "@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="], - "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@9786d91", { "dependencies": { "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@5c7f8e5", { "dependencies": { "json-bigint": "1.0.0" } }], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], diff --git a/package.json b/package.json index 849d235d4a..68435fd1a0 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@9786d91", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@5c7f8e5", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index a03b956e4c..63d5cf42d4 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -255,6 +255,7 @@ export enum Submit { ProjectUpdateLabels = 'submit_project_update_labels', ProjectService = 'submit_project_service', ProjectUpdateSMTP = 'submit_project_update_smtp', + ProjectUpdateOAuth2Server = 'submit_project_update_oauth2_server', ProjectResume = 'submit_project_resume', MemberCreate = 'submit_member_create', MemberDelete = 'submit_member_delete', diff --git a/src/lib/flags.ts b/src/lib/flags.ts index 9f8c57a574..3a0cf0d85e 100644 --- a/src/lib/flags.ts +++ b/src/lib/flags.ts @@ -20,5 +20,6 @@ function isFlagEnabled(name: string) { export const flags = { multiDb: isFlagEnabled('multi-db'), - granularProjectAccess: isFlagEnabled('granular-project-access') + granularProjectAccess: isFlagEnabled('granular-project-access'), + oauth2Server: isFlagEnabled('oauth2-server') }; diff --git a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte index 3961307590..77ae89946d 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte @@ -18,8 +18,12 @@ import UpdateVariables from '../updateVariables.svelte'; import { page } from '$app/state'; import UpdateLabels from './updateLabels.svelte'; + import UpdateOAuth2Server from './updateOAuth2Server.svelte'; import type { PageData } from './$types'; import { Alert } from '@appwrite.io/pink-svelte'; + import { flags } from '$lib/flags'; + import { user } from '$lib/stores/user'; + import { organization } from '$lib/stores/organization'; let { data }: { data: PageData } = $props(); @@ -95,6 +99,9 @@ + {#if flags.oauth2Server({ account: $user, organization: $organization })} + + {/if} + import { invalidate } from '$app/navigation'; + import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; + import { CardGrid } from '$lib/components'; + import { Dependencies } from '$lib/constants'; + import { Button, Form, InputNumber, InputTags, InputText } from '$lib/elements/forms'; + import InputSelect from '$lib/elements/forms/inputSelect.svelte'; + import { addNotification } from '$lib/stores/notifications'; + import { canWriteProjects } from '$lib/stores/roles'; + import { sdk } from '$lib/stores/sdk'; + import { Divider, Icon, Layout, Selector, Tooltip, Typography } from '@appwrite.io/pink-svelte'; + import { IconInfo } from '@appwrite.io/pink-icons-svelte'; + import deepEqual from 'deep-equal'; + import { project } from '../store'; + + type TimeUnit = 'seconds' | 'minutes' | 'hours' | 'days'; + + const multipliers: Record = { + seconds: 1, + minutes: 60, + hours: 3600, + days: 86400 + }; + + const unitOptions: { value: TimeUnit; label: string }[] = [ + { value: 'seconds', label: 'Seconds' }, + { value: 'minutes', label: 'Minutes' }, + { value: 'hours', label: 'Hours' }, + { value: 'days', label: 'Days' } + ]; + + function fromSeconds( + s: number | null, + defaultUnit: TimeUnit = 'hours' + ): { value: number | null; unit: TimeUnit } { + if (s === null) return { value: null, unit: defaultUnit }; + if (s % 86400 === 0) return { value: s / 86400, unit: 'days' }; + if (s % 3600 === 0) return { value: s / 3600, unit: 'hours' }; + if (s % 60 === 0) return { value: s / 60, unit: 'minutes' }; + return { value: s, unit: 'seconds' }; + } + + function toSeconds(value: number | null, unit: TimeUnit): number | null { + return value !== null ? value * multipliers[unit] : null; + } + + let enabled = $state(false); + let authorizationUrl = $state(''); + let scopes = $state([]); + + let accessTokenValue = $state(null); + let accessTokenUnit = $state('hours'); + let refreshTokenValue = $state(null); + let refreshTokenUnit = $state('days'); + let publicAccessTokenValue = $state(null); + let publicAccessTokenUnit = $state('hours'); + let publicRefreshTokenValue = $state(null); + let publicRefreshTokenUnit = $state('days'); + let confidentialPkce = $state(false); + + const accessTokenDuration = $derived(toSeconds(accessTokenValue, accessTokenUnit)); + const refreshTokenDuration = $derived(toSeconds(refreshTokenValue, refreshTokenUnit)); + const publicAccessTokenDuration = $derived( + toSeconds(publicAccessTokenValue, publicAccessTokenUnit) + ); + const publicRefreshTokenDuration = $derived( + toSeconds(publicRefreshTokenValue, publicRefreshTokenUnit) + ); + + const isButtonDisabled = $derived( + !$canWriteProjects || + deepEqual( + { + enabled, + authorizationUrl, + scopes, + accessTokenDuration, + refreshTokenDuration, + publicAccessTokenDuration, + publicRefreshTokenDuration, + confidentialPkce + }, + { + enabled: $project.oAuth2ServerEnabled ?? false, + authorizationUrl: $project.oAuth2ServerAuthorizationUrl ?? '', + scopes: $project.oAuth2ServerScopes ?? [], + accessTokenDuration: $project.oAuth2ServerAccessTokenDuration ?? null, + refreshTokenDuration: $project.oAuth2ServerRefreshTokenDuration ?? null, + publicAccessTokenDuration: + $project.oAuth2ServerPublicAccessTokenDuration ?? null, + publicRefreshTokenDuration: + $project.oAuth2ServerPublicRefreshTokenDuration ?? null, + confidentialPkce: $project.oAuth2ServerConfidentialPkce ?? false + } + ) + ); + + async function update() { + try { + await sdk.forProject($project.region, $project.$id).project.updateOAuth2Server({ + enabled, + authorizationUrl, + scopes, + accessTokenDuration: accessTokenDuration ?? undefined, + refreshTokenDuration: refreshTokenDuration ?? undefined, + publicAccessTokenDuration: publicAccessTokenDuration ?? undefined, + publicRefreshTokenDuration: publicRefreshTokenDuration ?? undefined, + confidentialPkce + }); + + await invalidate(Dependencies.PROJECT); + + addNotification({ + type: 'success', + message: 'OAuth2 server settings have been updated.' + }); + trackEvent(Submit.ProjectUpdateOAuth2Server); + } catch (error) { + addNotification({ type: 'error', message: error.message }); + trackError(error, Submit.ProjectUpdateOAuth2Server); + } + } + + $effect(() => { + enabled = $project.oAuth2ServerEnabled ?? false; + authorizationUrl = $project.oAuth2ServerAuthorizationUrl ?? ''; + scopes = $project.oAuth2ServerScopes ?? []; + + const at = fromSeconds($project.oAuth2ServerAccessTokenDuration ?? null, 'hours'); + accessTokenValue = at.value; + accessTokenUnit = at.unit; + + const rt = fromSeconds($project.oAuth2ServerRefreshTokenDuration ?? null, 'days'); + refreshTokenValue = rt.value; + refreshTokenUnit = rt.unit; + + const pat = fromSeconds($project.oAuth2ServerPublicAccessTokenDuration ?? null, 'hours'); + publicAccessTokenValue = pat.value; + publicAccessTokenUnit = pat.unit; + + const prt = fromSeconds($project.oAuth2ServerPublicRefreshTokenDuration ?? null, 'days'); + publicRefreshTokenValue = prt.value; + publicRefreshTokenUnit = prt.unit; + + confidentialPkce = $project.oAuth2ServerConfidentialPkce ?? false; + }); + + +
+ + OAuth2 server + Configure your project as an OAuth2 authorization server. When enabled, external applications + can authenticate users through your project using the OAuth2 protocol. + + + + {#if enabled} + + + + The consent screen URL shown to users during the OAuth2 authorization + flow. + + + + + + + OAuth2 scopes this server will accept. Up to 100 scopes, each up to 128 + characters long. + + + + + + + Confidential clients + + Server-side apps that authenticate with a client secret. + + + +
+ + +
+ +
+ + +
+ + + + + + + Public clients + + SPAs, mobile, and native apps that cannot keep a client secret. + + + +
+ + +
+ +
+ + +
+ {/if} +
+ + + +
+
+ +