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
5 changes: 3 additions & 2 deletions src/lib/stores/oauth-providers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Component } from 'svelte';
import Main from '$routes/(console)/project-[region]-[project]/auth/(providers)/mainOAuth.svelte';
import Google from '$routes/(console)/project-[region]-[project]/auth/(providers)/googleOAuth.svelte';

export type Provider = {
name: string;
Expand Down Expand Up @@ -129,13 +130,13 @@ export const oAuthProviders: Record<string, Provider> = {
name: 'Google',
icon: 'google',
docs: 'https://support.google.com/googleapi/answer/6158849',
component: Main
component: Google
},
googleImagine: {
name: 'Google',
icon: 'google',
docs: 'https://support.google.com/googleapi/answer/6158849',
component: Main,
component: Google,
internal: true
},
keycloak: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<script lang="ts">
import { page } from '$app/state';
import { CopyInput, Modal } from '$lib/components';
import { Button, InputPassword, InputSwitch, InputText } from '$lib/elements/forms';
import { updateOAuth } from '../updateOAuth';
import {
OAuthProvider,
ProjectOAuth2GooglePrompt,
type Models as ConsoleModels
} from '@appwrite.io/console';
import type { AuthProvider } from '../updateOAuth';
import { oAuthProviders } from '$lib/stores/oauth-providers';
import {
Accordion,
Alert,
Card,
Divider,
Icon,
Layout,
Link,
Tag,
Typography
} from '@appwrite.io/pink-svelte';
import { IconPencil, IconX } from '@appwrite.io/pink-icons-svelte';
import { getApiEndpoint } from '$lib/stores/sdk';
import GooglePromptPicker from './googlePromptPicker.svelte';

const projectId = page.params.project;
const region = page.params.region;

export let provider: AuthProvider;
export let show = false;
export let parameters: ConsoleModels.ConsoleOAuth2ProviderParameter[] = [];

let enabled: boolean = provider.enabled;
let appId: string | null = null;
let clientSecret: string | null = null;
let showSecretInput = false;
let prompt: ProjectOAuth2GooglePrompt[] = [];
let error: string;
let initializedProvider: AuthProvider | null = null;
let initialEnabled = false;
let initialAppId: string | null = null;
let initialPrompt: ProjectOAuth2GooglePrompt[] = [];

$: appIdParam = parameters.length >= 1 ? parameters[0] : null;
$: hasSecretInput = clientSecret !== null && clientSecret !== '';

$: nothingChanged =
enabled === initialEnabled &&
normalizeFieldValue(appId) === normalizeFieldValue(initialAppId) &&
!hasSecretInput &&
[...prompt].sort().join(',') === [...initialPrompt].sort().join(',');

$: oAuthProvider = oAuthProviders[provider.key];

$: if (provider && provider !== initializedProvider) {
initializedProvider = provider;
initialEnabled = provider.enabled;
initialAppId = getInitialAppId();
enabled = initialEnabled;
appId = initialAppId;
clientSecret = null;
showSecretInput = !appId;
error = undefined;
const rawPrompt = (provider as Record<string, unknown>)['prompt'];
initialPrompt = Array.isArray(rawPrompt)
? ([...rawPrompt] as ProjectOAuth2GooglePrompt[])
: [];
prompt = [...initialPrompt];
}

function getInitialAppId(): string | null {
const value = (provider as Record<string, unknown>)['clientId'];
return typeof value === 'string' ? value : null;
}

function normalizeFieldValue(value: string | null | undefined): string {
return value ?? '';
}

function helperText(hint?: string | null): string | undefined {
const value = hint?.trim();
return value || undefined;
}

function resetSecretInput() {
showSecretInput = false;
clientSecret = null;
}

const update = async () => {
const result = await updateOAuth({
region,
projectId,
provider,
appId,
secret: clientSecret ? JSON.stringify({ clientSecret }) : null,
details: {},
promptValues: prompt,
enabled
});

if (result.status === 'error') {
error = result.message;
} else {
provider = null;
}
};
</script>

<Modal {error} bind:show onSubmit={update} on:close title="Google OAuth2 settings">
<p slot="description">
To use Google authentication in your application, first fill in this form. For more info you
can
<Link.Anchor
class="link"
href={oAuthProvider?.docs}
target="_blank"
rel="noopener noreferrer">visit the docs.</Link.Anchor>
</p>

<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />

{#if appIdParam}
<InputText
id="appID"
label={appIdParam.name.trim()}
autofocus={true}
placeholder={appIdParam.example || ''}
helper={helperText(appIdParam.hint)}
required={enabled && !!appIdParam.example}
bind:value={appId} />
{/if}

{#if !showSecretInput}
<div>
<Tag size="s" on:click={() => (showSecretInput = true)}>
<Icon icon={IconPencil} size="s" /> Update Client Secret
</Tag>
</div>
{:else}
<Card.Base
variant="secondary"
padding="s"
--input-background-color="var(--bgcolor-neutral-primary)">
<Layout.Stack gap="xl">
<Layout.Stack gap="s">
<Layout.Stack
direction="row"
justifyContent="space-between"
alignContent="center">
<Typography.Text variant="m-600">Client Secret</Typography.Text>
<Button extraCompact on:click={resetSecretInput}>
<Icon icon={IconX} size="s" />
</Button>
</Layout.Stack>
<Typography.Text>
This field is write-only. Enter a new value to update it.
</Typography.Text>
</Layout.Stack>
<span
style="margin-left: calc(-1 * var(--space-7)); margin-right: calc(-1 * var(--space-7)); width: auto;">
<Divider />
</span>
<Layout.Stack>
<InputPassword
id="clientSecret"
label="Client Secret"
placeholder="Client Secret"
minlength={0}
bind:value={clientSecret} />
</Layout.Stack>
</Layout.Stack>
</Card.Base>
{/if}

<Accordion title="Advanced" badge="Optional" hideDivider>
<GooglePromptPicker bind:value={prompt} />
</Accordion>

<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your Google app configuration.
</Alert.Inline>
<CopyInput
label="URI"
value={`${getApiEndpoint(page.params.region)}/account/sessions/oauth2/callback/${OAuthProvider.Google}/${projectId}`} />

<svelte:fragment slot="footer">
<Button secondary on:click={() => (provider = null)}>Cancel</Button>
<Button disabled={nothingChanged} submit>Update</Button>
</svelte:fragment>
</Modal>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script lang="ts">
import { ProjectOAuth2GooglePrompt } from '@appwrite.io/console';
import { Layout, Tag, Typography } from '@appwrite.io/pink-svelte';

type Props = {
value: ProjectOAuth2GooglePrompt[];
onchange?: (value: ProjectOAuth2GooglePrompt[]) => void;
};

let { value = $bindable([]), onchange }: Props = $props();

const options: { val: ProjectOAuth2GooglePrompt; label: string }[] = [
{ val: ProjectOAuth2GooglePrompt.None, label: 'None' },
{ val: ProjectOAuth2GooglePrompt.Consent, label: 'Consent' },
{ val: ProjectOAuth2GooglePrompt.SelectAccount, label: 'Select account' }
];

function toggle(opt: ProjectOAuth2GooglePrompt): void {
let next: ProjectOAuth2GooglePrompt[];
if (opt === ProjectOAuth2GooglePrompt.None) {
next = value.includes(opt) ? [] : [opt];
} else {
next = value.includes(opt)
? value.filter((v) => v !== opt)
: [...value.filter((v) => v !== ProjectOAuth2GooglePrompt.None), opt];
}
value = next;
onchange?.(next);
}
</script>

<Layout.Stack gap="xs">
<Typography.Text variant="m-500">Prompt</Typography.Text>
<Layout.Stack direction="row" gap="s" flexWrap="wrap">
{#each options as option (option.val)}
<Tag size="s" selected={value.includes(option.val)} on:click={() => toggle(option.val)}>
{option.label}
</Tag>
{/each}
</Layout.Stack>
</Layout.Stack>
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
}

function isOidcAdvancedParam(id: string): boolean {
return id === 'authorizationURL' || id === 'tokenUrl' || id === 'userInfoUrl';
return id !== 'wellKnownURL' && id.toLowerCase().includes('url');
}

async function handleP8FileUpload(id: string, event: Event) {
Expand Down Expand Up @@ -245,21 +245,6 @@
bind:value={fieldValues[param.$id]} />
{/each}

{#if advancedDetailParams.length > 0}
<Accordion title="Advanced" badge="Optional" hideDivider>
<Layout.Stack gap="l">
{#each advancedDetailParams as param}
<InputText
id={param.$id}
label={primaryName(param.name)}
placeholder={param.example || ''}
helper={helperText(param.hint)}
bind:value={fieldValues[param.$id]} />
{/each}
</Layout.Stack>
</Accordion>
{/if}

{#if secretParams.length > 0}
{#if !showSecretInput}
<div>
Expand Down Expand Up @@ -372,6 +357,21 @@
{/if}
{/if}

{#if advancedDetailParams.length > 0}
<Accordion title="Advanced" badge="Optional" hideDivider>
<Layout.Stack gap="l">
{#each advancedDetailParams as param}
<InputText
id={param.$id}
label={primaryName(param.name)}
placeholder={param.example || ''}
helper={helperText(param.hint)}
bind:value={fieldValues[param.$id]} />
{/each}
</Layout.Stack>
</Accordion>
{/if}

<Alert.Inline status="info">
To complete set up, add this OAuth2 redirect URI to your {provider.name} app configuration.
</Alert.Inline>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { Dependencies } from '$lib/constants';
import { isValueOfStringEnum } from '$lib/helpers/types';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { OAuthProvider, type Models as ConsoleModels } from '@appwrite.io/console';
import {
OAuthProvider,
ProjectOAuth2GooglePrompt,
type Models as ConsoleModels
} from '@appwrite.io/console';

type ProjectOAuthProvider = ConsoleModels.OAuth2ProviderList['providers'][number];

Expand All @@ -21,6 +25,7 @@ type Args = {
secret: string | null;
details: Record<string, string>;
enabled: boolean;
promptValues?: ProjectOAuth2GooglePrompt[];
};

type Return = {
Expand All @@ -45,7 +50,8 @@ async function updateProjectOAuth({
appId,
secret,
details,
enabled
enabled,
promptValues
}: Args) {
const projectSdk = sdk.forProject(region, projectId).project;
const parsedSecret = parseSecret(secret);
Expand Down Expand Up @@ -184,6 +190,7 @@ async function updateProjectOAuth({
return projectSdk.updateOAuth2Google({
clientId: getAppId(),
clientSecret: getSecret(),
prompt: promptValues ?? [],
enabled
});
case OAuthProvider.Keycloak:
Expand Down Expand Up @@ -345,14 +352,24 @@ export async function updateOAuth({
appId,
secret,
details,
enabled
enabled,
promptValues
}: Args): Promise<Return> {
try {
if (!isValueOfStringEnum(OAuthProvider, provider.key)) {
throw new Error(`Invalid OAuth2 provider: ${provider.key}`);
}

await updateProjectOAuth({ region, projectId, provider, appId, secret, details, enabled });
await updateProjectOAuth({
region,
projectId,
provider,
appId,
secret,
details,
enabled,
promptValues
});
await invalidate(Dependencies.PROJECT);

addNotification({
Expand Down
Loading