mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #3067 from appwrite/feat-oauth2server-settings
feat: add OAuth2 server settings card and update SDK
This commit is contained in:
@@ -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=="],
|
||||
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
+2
-1
@@ -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')
|
||||
};
|
||||
|
||||
@@ -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 @@
|
||||
<UpdateName />
|
||||
<UpdateLabels />
|
||||
<UpdateProtocols />
|
||||
{#if flags.oauth2Server({ account: $user, organization: $organization })}
|
||||
<UpdateOAuth2Server />
|
||||
{/if}
|
||||
<UpdateServices />
|
||||
<UpdateInstallations {...data.installations} limit={data.limit} offset={data.offset} />
|
||||
<UpdateVariables
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
<script lang="ts">
|
||||
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<TimeUnit, number> = {
|
||||
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<string[]>([]);
|
||||
|
||||
let accessTokenValue = $state<number | null>(null);
|
||||
let accessTokenUnit = $state<TimeUnit>('hours');
|
||||
let refreshTokenValue = $state<number | null>(null);
|
||||
let refreshTokenUnit = $state<TimeUnit>('days');
|
||||
let publicAccessTokenValue = $state<number | null>(null);
|
||||
let publicAccessTokenUnit = $state<TimeUnit>('hours');
|
||||
let publicRefreshTokenValue = $state<number | null>(null);
|
||||
let publicRefreshTokenUnit = $state<TimeUnit>('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;
|
||||
});
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">OAuth2 server</svelte:fragment>
|
||||
Configure your project as an OAuth2 authorization server. When enabled, external applications
|
||||
can authenticate users through your project using the OAuth2 protocol.
|
||||
<svelte:fragment slot="aside">
|
||||
<Selector.Switch
|
||||
id="oauth2-server-enabled"
|
||||
bind:checked={enabled}
|
||||
label="Enable OAuth2 server"
|
||||
description="Allow external applications to authenticate users through your project."
|
||||
disabled={!$canWriteProjects} />
|
||||
|
||||
{#if enabled}
|
||||
<InputText
|
||||
id="oauth2-authorization-url"
|
||||
label="Authorization URL"
|
||||
bind:value={authorizationUrl}
|
||||
required
|
||||
placeholder="https://example.com/consent"
|
||||
disabled={!$canWriteProjects}>
|
||||
<Tooltip slot="info">
|
||||
<Icon icon={IconInfo} size="s" />
|
||||
<span slot="tooltip"
|
||||
>The consent screen URL shown to users during the OAuth2 authorization
|
||||
flow.</span>
|
||||
</Tooltip>
|
||||
</InputText>
|
||||
|
||||
<InputTags
|
||||
id="oauth2-scopes"
|
||||
label="Scopes"
|
||||
bind:tags={scopes}
|
||||
placeholder="e.g. profile"
|
||||
max={100}
|
||||
disabled={!$canWriteProjects}>
|
||||
<Tooltip slot="info">
|
||||
<Icon icon={IconInfo} size="s" />
|
||||
<span slot="tooltip"
|
||||
>OAuth2 scopes this server will accept. Up to 100 scopes, each up to 128
|
||||
characters long.</span>
|
||||
</Tooltip>
|
||||
</InputTags>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Layout.Stack gap="xs">
|
||||
<Typography.Text variant="m-500">Confidential clients</Typography.Text>
|
||||
<Typography.Caption variant="400">
|
||||
Server-side apps that authenticate with a client secret.
|
||||
</Typography.Caption>
|
||||
</Layout.Stack>
|
||||
|
||||
<div class="duration-field">
|
||||
<InputNumber
|
||||
id="oauth2-access-token-duration"
|
||||
label="Access token duration"
|
||||
bind:value={accessTokenValue}
|
||||
placeholder="8"
|
||||
min={1}
|
||||
disabled={!$canWriteProjects} />
|
||||
<InputSelect
|
||||
id="oauth2-access-token-unit"
|
||||
required
|
||||
bind:value={accessTokenUnit}
|
||||
options={unitOptions}
|
||||
disabled={!$canWriteProjects} />
|
||||
</div>
|
||||
|
||||
<div class="duration-field">
|
||||
<InputNumber
|
||||
id="oauth2-refresh-token-duration"
|
||||
label="Refresh token duration"
|
||||
bind:value={refreshTokenValue}
|
||||
placeholder="365"
|
||||
min={1}
|
||||
disabled={!$canWriteProjects} />
|
||||
<InputSelect
|
||||
id="oauth2-refresh-token-unit"
|
||||
required
|
||||
bind:value={refreshTokenUnit}
|
||||
options={unitOptions}
|
||||
disabled={!$canWriteProjects} />
|
||||
</div>
|
||||
|
||||
<Selector.Switch
|
||||
id="oauth2-confidential-pkce"
|
||||
bind:checked={confidentialPkce}
|
||||
label="Require PKCE"
|
||||
description="When enabled, confidential clients must use PKCE in addition to their client secret. Public clients always require PKCE."
|
||||
disabled={!$canWriteProjects} />
|
||||
|
||||
<Divider />
|
||||
|
||||
<Layout.Stack gap="xs">
|
||||
<Typography.Text variant="m-500">Public clients</Typography.Text>
|
||||
<Typography.Caption variant="400">
|
||||
SPAs, mobile, and native apps that cannot keep a client secret.
|
||||
</Typography.Caption>
|
||||
</Layout.Stack>
|
||||
|
||||
<div class="duration-field">
|
||||
<InputNumber
|
||||
id="oauth2-public-access-token-duration"
|
||||
label="Access token duration"
|
||||
bind:value={publicAccessTokenValue}
|
||||
placeholder="1"
|
||||
min={1}
|
||||
disabled={!$canWriteProjects} />
|
||||
<InputSelect
|
||||
id="oauth2-public-access-token-unit"
|
||||
required
|
||||
bind:value={publicAccessTokenUnit}
|
||||
options={unitOptions}
|
||||
disabled={!$canWriteProjects} />
|
||||
</div>
|
||||
|
||||
<div class="duration-field">
|
||||
<InputNumber
|
||||
id="oauth2-public-refresh-token-duration"
|
||||
label="Refresh token duration"
|
||||
bind:value={publicRefreshTokenValue}
|
||||
placeholder="30"
|
||||
min={1}
|
||||
disabled={!$canWriteProjects} />
|
||||
<InputSelect
|
||||
id="oauth2-public-refresh-token-unit"
|
||||
required
|
||||
bind:value={publicRefreshTokenUnit}
|
||||
options={unitOptions}
|
||||
disabled={!$canWriteProjects} />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button submit disabled={isButtonDisabled}>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<style>
|
||||
.duration-field {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 8rem;
|
||||
gap: var(--space-4);
|
||||
align-items: end;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user